Agent hooks
Castellan supports two hook surfaces: governance lifecycle hooks (configured in castellan.toml) and multiplexer agent lifecycle reporting (socket API for editor agents).
Governance lifecycle hooks (castellan.toml)
Configure deterministic shell hooks in layered config (castellan.toml, ~/.castellan/config.toml, .castellan/local.toml):
[[hooks.hooks]]
event = "SessionStart"
command = "./scripts/hooks/session-start.sh"
[[hooks.hooks]]
event = "PreToolUse"
command = "./scripts/hooks/pre-tool.sh"
[[hooks.hooks]]
event = "PostToolUse"
command = "./scripts/hooks/post-tool.sh"
[[hooks.hooks]]
event = "SessionEnd"
command = "./scripts/hooks/session-end.sh"
Accepted event / kind values: SessionStart, SessionEnd, PreToolUse, PostToolUse (snake_case aliases also work).
Each command receives a single JSON payload line on stdin:
| Event | Payload fields |
|---|---|
SessionStart | goal_id, plugin |
SessionEnd | goal_id, status |
PreToolUse | tool, argv, agent_id |
PostToolUse | tool, argv, success |
Invocation paths
| Path | When hooks run |
|---|---|
castellan run | SessionStart/SessionEnd on episode boundaries; PreToolUse/PostToolUse on MCP tool calls and shell plugin execution via ToolGovernancePipeline |
castellan run --rlm | Same session hooks; mux delegate steps use nested sub_harness panes |
| Shell plugin | PreToolUse/PostToolUse with tool: "shell" and argv in payload |
Hook commands must exit 0; non-zero exits fail the surrounding operation.
Multiplexer agent lifecycle (socket API)
Agents (Claude Code, Codex, Cursor) can report lifecycle state to the multiplexer without relying on screen heuristics.
Socket API
{"id":"1","method":"pane.report_agent","params":{
"pane_id": "w1:p1",
"agent": "claude",
"state": "working",
"source": "hook"
}}
States: idle, working, blocked, done, unknown.
When source is hook, screen heuristics are ignored until the pane is reset.
Shell hook example
Install into your agent wrapper (~/.config/castellan/hooks/herdr-agent-state.sh):
#!/usr/bin/env bash
# Usage: herdr-agent-state.sh <pane_id> <agent> <state>
PANE_ID="${1:?pane_id}"
AGENT="${2:?agent}"
STATE="${3:?state}"
SOCKET="${CASTELLAN_SOCKET:-$HOME/.config/castellan/castellan.sock}"
printf '%s\n' "{\"id\":\"hook\",\"method\":\"pane.report_agent\",\"params\":{\"pane_id\":\"$PANE_ID\",\"agent\":\"$AGENT\",\"state\":\"$STATE\",\"source\":\"hook\"}}" \
| nc -U "$SOCKET"
Claude / Codex / Cursor integration
| Agent | Suggested hook point |
|---|---|
| Claude Code | Wrap claude in a shell function; call hook on tool-approval and turn-complete |
| Codex | Export CODEX_HOOK_CMD pointing to the script above |
| Cursor | Use cursor-agent lifecycle env callbacks if available; else poll is fallback |
CLI wait helper
castellan herd wait --pane w1:p1 --status idle --timeout-ms 60000
Equivalent to events.wait with pane_agent_status_changed.
Environment
| Variable | Purpose |
|---|---|
CASTELLAN_SOCKET | Override default ~/.config/castellan/castellan.sock |
CASTELLAN_ENV=1 | Set in spawned pane shells (detect castellan-managed sessions) |