Governance overview
Castellan ships two independent governance surfaces, and neither one routes decisions through an LLM: governance is deterministic policy plus operator-configured shell hooks.
Canon (cite; don’t redesign): dual-stack diagram in repo docs/GOVERNANCE.md — Tools (PermissionPolicy → plan_verify → hooks → execute) and Evolve (drift → Φ → fitness → immune).
- Manifest drift-check —
castellan drift-checkenforces required CI checks, style files, and deprecation notices against a single source of truth (VERIFICATION_MANIFEST). See Governance policies. - Runtime governance — permission presets, shell command policy, spend/risk caps, and lifecycle hooks configured in layered
castellan.toml. See Agent hooks.
If you’re deciding what an agent is allowed to run, why a tool call got denied, or whether your repo passes CI’s guardrail gate, this page is the starting point — the two linked pages below go deep on each surface.
Anatomy: the two surfaces
| Surface | Question it answers | Enforced by | Config |
|---|---|---|---|
| Manifest drift-check | Does the repo still satisfy required CI/style/doc guardrails? | castellan drift-check, scripts/drift-guard.sh | crates/castellan-governance/src/manifest.rs (code, not TOML) |
| Permission policy | Is this MCP tool call / shell command allowed right now? | ToolGovernancePipeline, ShellSandbox | [permissions], [shell] in castellan.toml |
| Spend / risk caps | Has this session exceeded budget or risk thresholds? | ToolGovernancePipeline | [governance] in castellan.toml |
| Lifecycle hooks | What deterministic shell command fires on session/tool events? | castellan run, MCP tool calls | [[hooks.hooks]] in castellan.toml |
flowchart TB
CFG[castellan.toml layers] --> PERM[permission preset]
CFG --> SHELL[shell allow/deny lists]
CFG --> CAP[spend + risk caps]
CFG --> HOOKS[lifecycle hooks]
PERM --> PIPE[ToolGovernancePipeline]
SHELL --> PIPE
CAP --> PIPE
HOOKS --> PIPE
PIPE --> RUN[castellan run / mcp]
RUN --> AUDIT[audit JSONL]
MANIFEST[VERIFICATION_MANIFEST] --> DRIFT[castellan drift-check]
DRIFT --> CI[CI gate]
classDef gate stroke:#1a7f37,stroke-width:2px
classDef runtime stroke:#0969da,stroke-width:2px
class PERM,SHELL,CAP,HOOKS,MANIFEST,DRIFT gate
class PIPE,RUN,AUDIT,CI runtime
Permission presets
[permissions] preset expands into a base policy before any [permissions.tools] overrides apply:
| Preset | Default mode | Use when |
|---|---|---|
cautious (aka always-ask) | Prompts on nearly everything, including meta-tools like castellan_discover_tools | First install, unfamiliar plugin, shared/untrusted repo |
balanced (aka write) | Allows read-ish tools, asks before writes/mutations | Daily-driver operator loop once you trust the plugin set |
permissive (aka yolo) | Allows by default | CI, sandboxed containers, fully scripted recipes |
Prompt mode UX
PermissionMode::Prompt is resolved through a host permission prompter seam — not a silent deny.
| Surface | Behavior |
|---|---|
castellan run on a TTY | Interactive [y/N] on stderr for each prompted tool |
castellan run non-TTY | Fail closed (deny) with a clear reason + PermissionDenied event |
| Scripted / CI | Set CASTELLAN_PERMISSION_PROMPT=allow to auto-allow (audited via reason/events), or =deny to force deny |
ACP permissions/request | Returns needs_prompt: true for the editor client to present UI |
ACP session/prompt engine path | Same finalize seam; headless default deny unless CASTELLAN_PERMISSION_PROMPT=allow |
Prompt decisions always emit observability (PermissionDenied / ToolDecision) — never a silent drop.
How operators use it
1. Check the current runtime governance posture:
castellan doctor --json | jq '.stages[] | select(.name=="governance_manifest")'
2. Run the manifest guardrail check (fast, no network, what CI runs):
castellan drift-check
3. Set a permission preset for daily-driver operation:
castellan config set permissions.preset balanced
4. Override a single tool’s policy without changing the whole preset:
[permissions.tools]
castellan_propose_mutation = "deny"
castellan_deposit_signal = "allow"
5. Cap spend and risk for a session in castellan.toml:
[governance]
session_spend_cap_usd = 12.5
max_risk_score = 0.85
denied_paths = ["/secret", "~/.ssh"]
6. Record every governed decision to an audit trail for a run:
castellan run --goal "reach target" --plugin shell --audit .castellan/audit.jsonl
Recipes
When onboarding a new repo or plugin, start with preset = "cautious" and loosen to balanced once you’ve watched a few runs and trust the shell/tool surface.
When running in CI or a disposable container, use preset = "permissive" but keep [shell] denied_commands non-empty — permissive changes the default, not the explicit denylist.
When you need a paper trail for a specific run (compliance, debugging a bad tool call), always pass --audit <path> — governance decisions aren’t persisted anywhere else by default.
When a PR touches CI, manifest.rs, or lint/toolchain config, run castellan drift-check locally before pushing — the same check gates CI and failures there block merge.
Failure paths / troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
castellan drift-check fails locally but code looks fine | A guardrail file (CI workflow, manifest.rs, toolchain pins) drifted from what the manifest expects | Read the check_id/message in the failure; fix the guardrail file, don’t suppress the check |
| Tool call unexpectedly denied | preset is cautious, or a [permissions.tools] override sets deny | Check castellan doctor --json for the active preset; add an explicit allow override if appropriate |
| Prompted tool denied with “no prompter” / “auto-denied” | Non-TTY run without CASTELLAN_PERMISSION_PROMPT=allow, or headless ACP engine path | Run on a TTY, set CASTELLAN_PERMISSION_PROMPT=allow for CI, or set the tool to allow in [permissions.tools] |
| Shell plugin command blocked | Matches [shell] denied_commands or denied_patterns | Confirm the command is actually safe, then adjust the denylist — don’t bypass via a wrapper script |
| Session aborts with a spend/risk error | session_spend_cap_usd or max_risk_score exceeded | Raise the cap deliberately in castellan.toml, or investigate why the run is spending/risking more than expected |
| Hook command fails the whole operation | Hook script exited non-zero | Hook commands must exit 0; fix the script or remove the hook if it’s non-critical |
Related
- Governance policies — CI guardrail manifest, PR requirements, single source of truth
- Agent hooks —
castellan.tomllifecycle hooks and multiplexer agent reporting castellan.tomlreference — full[governance],[shell],[permissions]key lists- CLI reference
- Cursor setup