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

Hooks

Hooks let you attach external logic to caliban's event stream — shell scripts, HTTP callbacks, or MCP tools — without modifying the agent or recompiling. Hooks run in-process (for the built-in PermissionsHook and audit hooks) or via an external HookRouter (for operator-configured handlers).

Event taxonomy

Caliban fires events at the following lifecycle points (ADR 0024):

EventWhen it fires
SessionStartOnce at startup, before the first turn
SessionEndOn clean exit
UserPromptSubmitBefore each user message is sent (including slash commands; payload includes is_slash)
PreCompactBefore context compaction begins
PostCompactAfter compaction completes
PreToolUseBefore each tool call; can gate or rewrite the call
PostToolUseAfter each tool call completes
PostToolUseFailureWhen a tool call errors
ConfigChangeWhen a settings file changes on disk (live reload)
CwdChangedWhen the working directory changes
FileChangedWhen a file the agent edited is detected to have changed
SubagentStart / SubagentStopWhen a sub-agent is spawned or exits
TaskCreated / TaskCompletedWhen a sub-agent task is enqueued or finishes
PermissionRequestWhen the agent requests permission for a tool call
PermissionDeniedWhen a tool call is denied
NotificationGeneral notification events
Stop / StopFailureWhen the agent loop stops (cleanly or with error)

Additional events (Setup, UserPromptExpansion, PostToolBatch, InstructionsLoaded, WorktreeCreate, WorktreeRemove, Elicitation, ElicitationResult, TeammateIdle) are reserved but not yet fired.

Handler types

Each hook entry declares one or more handlers. Two handler types are fully wired; three are stubs (see below).

TypeStatusDescription
commandFully wiredSpawn a child process; stdin is event JSON; decision via stdout or exit code
httpFully wiredPOST event JSON to a URL; decision via response JSON
mcpExperimental stubInvoke an MCP server tool with the event JSON
promptExperimental stubCall the model router with a classifier prompt
agentExperimental stubDelegate to a sub-agent (async only)

mcp / prompt / agent handlers are stubs

The mcp, prompt, and agent handler types are defined in the config schema and appear in /hooks output, but their dispatch logic is not yet wired. They will be activated as their upstream dependencies (ADR 0023 MCP wiring, ADR 0037 sub-agent fleet) land. Until then, any handler of these types is silently skipped at dispatch time.

Decision protocol

For PreToolUse and UserPromptSubmit, command and http handlers report their decision as:

Stdout JSON (preferred):

{
  "hookSpecificOutput": {
    "permissionDecision": "allow",
    "permissionDecisionReason": "matched allowlist",
    "updatedInput": {}
  }
}

permissionDecision values: allow, deny, ask. updatedInput lets the hook rewrite the tool input before dispatch (the rewritten input is validated against the tool's schema; validation failure is a hard deny).

Exit codes (shell-script shorthand):

  • 0 — Allow
  • 2 — Deny (stderr becomes the reason)
  • anything else — Allow with a logged warning

PostToolUse and observer-only hooks ignore the decision even when a handler provides one. Handlers marked async = true are fire-and-forget; their decisions are always ignored.

Config: settings hooks table (preferred)

Hooks live in the unified settings file under the hooks key. The table maps event names to arrays of handler groups. See Settings Layering for how scopes merge — hook arrays concatenate across scopes (project entries append to user entries).

# .caliban/settings.toml  — project scope

disable_all_hooks = false
allow_managed_hooks_only = false

allowed_http_hook_urls = [
  "https://hooks.example.com/*",
]
http_hook_allowed_env_vars = ["AUDIT_TOKEN"]

[[hooks.SessionStart]]
matcher = "*"
[[hooks.SessionStart.handlers]]
type    = "command"
command = "/usr/local/bin/caliban-audit"
args    = ["session-start"]
timeout = "5s"

[[hooks.PreToolUse]]
matcher = "Bash"
if      = "Bash:rm *"
[[hooks.PreToolUse.handlers]]
type    = "command"
command = "${CALIBAN_PROJECT_DIR}/.caliban/hooks/guard-rm.sh"
async   = false

[[hooks.PreToolUse]]
matcher = "WebFetch"
[[hooks.PreToolUse.handlers]]
type    = "http"
url     = "https://hooks.example.com/preflight"
headers = { Authorization = "Bearer ${AUDIT_TOKEN}" }
timeout = "3s"

[[hooks.PostToolUse]]
matcher = "*"
[[hooks.PostToolUse.handlers]]
type  = "mcp"
mcp   = "audit-server"
tool  = "log_tool_call"
async = true

Config: legacy hooks.toml (compat)

If no hooks key appears in any settings file, caliban falls back to loading:

  • <workspace>/.caliban/hooks.toml (project scope)
  • ~/.config/caliban/hooks.toml (user scope)

The legacy file uses the same TOML shape shown above (top-level keys plus [[hooks.<Event>]] arrays). The two scopes merge with project entries taking priority. This path is deprecated — prefer the unified settings file for new configurations.

Safety controls

Setting / flagEffect
disable_all_hooks = trueBypasses all external handlers; in-process hooks (permissions, audit) still run
allow_managed_hooks_only = trueOnly handlers from the managed settings scope fire
allowed_http_hook_urlsURL glob allowlist; HTTP handlers fail closed if the URL isn't listed
http_hook_allowed_env_varsEnv vars that may be expanded in HTTP handler headers
--no-hooksOne-off CLI override; mirrors disable_all_hooks for a single run
CALIBAN_NO_HOOKS=1Same, via environment variable

Audit without gating

Mark your audit hooks async = true. Async handlers observe the event but their decision is discarded, so they can never accidentally block a tool call. They run on a bounded task pool (default 16 concurrent) so they don't pile up under heavy load.