Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 (PermissionPolicyplan_verify → hooks → execute) and Evolve (drift → Φ → fitness → immune).

  1. Manifest drift-checkcastellan drift-check enforces required CI checks, style files, and deprecation notices against a single source of truth (VERIFICATION_MANIFEST). See Governance policies.
  2. 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.

Note: "Governance" in Castellan means repo guardrails (drift-check) and runtime tool/shell policy (permissions, hooks) — not content moderation or multi-agent arbitration.

Anatomy: the two surfaces

SurfaceQuestion it answersEnforced byConfig
Manifest drift-checkDoes the repo still satisfy required CI/style/doc guardrails?castellan drift-check, scripts/drift-guard.shcrates/castellan-governance/src/manifest.rs (code, not TOML)
Permission policyIs this MCP tool call / shell command allowed right now?ToolGovernancePipeline, ShellSandbox[permissions], [shell] in castellan.toml
Spend / risk capsHas this session exceeded budget or risk thresholds?ToolGovernancePipeline[governance] in castellan.toml
Lifecycle hooksWhat 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:

PresetDefault modeUse when
cautious (aka always-ask)Prompts on nearly everything, including meta-tools like castellan_discover_toolsFirst install, unfamiliar plugin, shared/untrusted repo
balanced (aka write)Allows read-ish tools, asks before writes/mutationsDaily-driver operator loop once you trust the plugin set
permissive (aka yolo)Allows by defaultCI, sandboxed containers, fully scripted recipes

Prompt mode UX

PermissionMode::Prompt is resolved through a host permission prompter seam — not a silent deny.

SurfaceBehavior
castellan run on a TTYInteractive [y/N] on stderr for each prompted tool
castellan run non-TTYFail closed (deny) with a clear reason + PermissionDenied event
Scripted / CISet CASTELLAN_PERMISSION_PROMPT=allow to auto-allow (audited via reason/events), or =deny to force deny
ACP permissions/requestReturns needs_prompt: true for the editor client to present UI
ACP session/prompt engine pathSame 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

SymptomLikely causeFix
castellan drift-check fails locally but code looks fineA guardrail file (CI workflow, manifest.rs, toolchain pins) drifted from what the manifest expectsRead the check_id/message in the failure; fix the guardrail file, don’t suppress the check
Tool call unexpectedly deniedpreset is cautious, or a [permissions.tools] override sets denyCheck 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 pathRun on a TTY, set CASTELLAN_PERMISSION_PROMPT=allow for CI, or set the tool to allow in [permissions.tools]
Shell plugin command blockedMatches [shell] denied_commands or denied_patternsConfirm the command is actually safe, then adjust the denylist — don’t bypass via a wrapper script
Session aborts with a spend/risk errorsession_spend_cap_usd or max_risk_score exceededRaise the cap deliberately in castellan.toml, or investigate why the run is spending/risking more than expected
Hook command fails the whole operationHook script exited non-zeroHook commands must exit 0; fix the script or remove the hook if it’s non-critical