Hooks
Shell commands, prompts, or HTTP calls that fire on lifecycle events — deterministic automation, not left to the model's discretion.
Overview
A hook is something Claudin runs automatically at a defined point in the session — after a file edit, before a tool call, when a session starts — instead of relying on the model to remember to do it. Hooks live under a hooks key in a settings file, keyed by event name.
Config
| Source | Path | Git |
|---|---|---|
| Project | .claudin/settings.json | Commit |
| Project-local | .claudin/settings.local.json | Gitignore |
| User | ~/.claudin/settings.json | — |
| Managed | OS-specific absolute path, e.g. /etc/claude-code/managed-settings.json on Linux | Enterprise |
Precedence when the same event/matcher shows up in more than one source: user < project < local < the --settings flag < managed.
Full settings reference (permissions, model config, output styles, and more) lives on the Configuration page.
{
"hooks": {
"<EventName>": [
{ "matcher": "<pattern>", "hooks": [ { "type": "command", "command": "..." } ] }
]
}
}
/hooks opens a read-only browser of everything currently configured.
Events
27 event names, grouped by what they're for:
| Group | Events |
|---|---|
| Tool calls | PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied |
| Session lifecycle | SessionStart, SessionEnd, Setup, Stop, StopFailure, InstructionsLoaded |
| Subagents & tasks | SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle |
| Context & input | UserPromptSubmit, PreCompact, PostCompact, Elicitation, ElicitationResult |
| Environment | ConfigChange, WorktreeCreate, WorktreeRemove, CwdChanged, FileChanged |
| Misc | Notification |
Matcher syntax
For tool events, matcher matches a tool name — "Write", "Write|Edit", "Bash", empty for every tool. Non-tool events use their own matcher values, e.g. PreCompact/PostCompact use "manual" or "auto". Each individual hook can also carry its own if field, using permission-rule syntax (like "Bash(git *)") to conditionally gate that one hook regardless of the matcher.
Action types
| Type | Fields | Notes |
|---|---|---|
command | command, if, shell, timeout, once, async, asyncRewake | Runs a shell command. asyncRewake runs it in the background and wakes the model if it exits with code 2. |
prompt | prompt (uses $ARGUMENTS), timeout, model | LLM-evaluated condition — works on tool events and lifecycle events like Stop. |
agent | prompt, timeout (default 60s), model (default Haiku) | Agentic verifier hook, runs with tools — works on tool events and lifecycle events like Stop. |
http | url, headers, allowedEnvVars | POSTs the hook input JSON to a URL. |
Input & output
Every hook gets a common base on stdin — session_id, transcript_path, cwd, hook_event_name, plus optional permission_mode/agent_id/agent_type — with extra fields layered on per event. Tool events (PreToolUse, PostToolUse, and friends) also carry tool_name, tool_input, and tool_response:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/path/to/project",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": { "file_path": "/path/to/file.txt", "content": "..." },
"tool_response": { "success": true }
}
To block or steer, a hook writes JSON to stdout:
{
"decision": "block",
"reason": "Explanation shown to the model",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "..."
}
}
Example
Run prettier after every write or edit:
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; prettier --write \"$f\"; } 2>/dev/null || true"
}]
}]
}
}
Hook chains
A separate, opt-in layer sits on top of ordinary hooks: instead of a matcher firing a command, a rules engine reacts to hook outcomes — a failed tool call, a completed task — with declarative remediation (spawn a fallback agent, notify the team, warm up remote capacity). Configured at .claudin/hook-chains.json, disabled by default. Most setups won't need this; reach for it only once plain hooks stop being enough.
Safety is conservative by design: a depth guard blocks dispatch once chainDepth >= maxChainDepth, each rule has a cooldown before it can re-fire, identical event/action pairs are deduped within a window, and every action fails safe (skipped with a structured reason) rather than throwing when its context isn't available.
Configuration schema
| Field | Type | Notes |
|---|---|---|
version | 1 | Defaults to 1. |
enabled | boolean | Global switch for this config file. |
maxChainDepth | integer | Global depth guard (default 2, max 10). |
defaultCooldownMs | integer | Default rule cooldown in ms (default 30000). |
defaultDedupWindowMs | integer | Default action dedup window in ms (default 30000). |
rules | HookChainRule[] | Defaults to []. An empty ruleset is valid — dispatch is a no-op. |
Each rule has an id, a trigger (event plus outcome/outcomes), an optional condition, and one or more actions run in order:
| Condition field | Matches against |
|---|---|
toolNames | tool_name / toolName in the event payload |
taskStatuses | task_status / taskStatus / status |
errorIncludes | Case-insensitive substring match against error / reason / message |
eventFieldEquals | Dot-path equality against the payload, e.g. "meta.source": "scheduler" |
Action types
| Type | Does | Key fields |
|---|---|---|
spawn_fallback_agent | Launches a recovery agent with a templated prompt | promptTemplate, agentType, model |
notify_team | Posts a templated message to team context | teamName, recipients, summary, messageTemplate |
warm_remote_capacity | Pre-warms a remote bridge environment | createDefaultEnvironmentIfMissing |
Template variables available in promptTemplate, summary, and messageTemplate: ${EVENT_NAME}, ${OUTCOME}, ${RULE_ID}, ${TASK_SUBJECT}, ${TASK_DESCRIPTION}, ${TASK_ID}, ${ERROR}, ${PAYLOAD_JSON}.
Example: fallback + notify + warm, combined
{
"version": 1,
"enabled": true,
"maxChainDepth": 2,
"defaultCooldownMs": 45000,
"defaultDedupWindowMs": 30000,
"rules": [
{
"id": "full-recovery-chain",
"trigger": {
"event": "TaskCompleted",
"outcomes": ["failed", "timeout"]
},
"condition": {
"errorIncludes": ["timeout", "capacity", "connection"]
},
"cooldownMs": 90000,
"actions": [
{
"type": "spawn_fallback_agent",
"id": "fallback-agent",
"description": "Recover failed task execution",
"promptTemplate": "Recover failed task and produce a concise fix summary.\nTask=${TASK_SUBJECT}\nError=${ERROR}\nPayload=${PAYLOAD_JSON}"
},
{
"type": "notify_team",
"id": "notify-team",
"recipients": ["*"],
"summary": "Recovery chain triggered",
"messageTemplate": "Recovery chain ${RULE_ID} fired.\nOutcome=${OUTCOME}\nTask=${TASK_SUBJECT}\nError=${ERROR}"
},
{
"type": "warm_remote_capacity",
"id": "warm-capacity",
"createDefaultEnvironmentIfMissing": false
}
]
}
]
}
Troubleshooting
| Symptom | Check |
|---|---|
| Rule never triggers | trigger.event/outcome(s) match the dispatched event exactly; condition filters (especially toolNames and eventFieldEquals dot-paths); the config file is valid JSON. |
| Actions show as skipped | Common reasons: action disabled, rule cooldown or dedup window still active, max chain depth reached, missing team context/team file, remote sessions blocked by policy, bridge inactive, or no fallback agent launcher registered. |
| Config changes not reflected | The loader memoizes by file mtime/size — confirm your editor writes the file fully and updates mtime, or force a reload with forceReloadConfig: true. |
| Existing workflows changed unexpectedly | Set "enabled": false at the top level, or globally disable with CLAUDIN_ENABLE_HOOK_CHAINS=0, then re-enable one rule at a time. |