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

SourcePathGit
Project.claudin/settings.jsonCommit
Project-local.claudin/settings.local.jsonGitignore
User~/.claudin/settings.json—
ManagedOS-specific absolute path, e.g. /etc/claude-code/managed-settings.json on LinuxEnterprise

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:

GroupEvents
Tool callsPreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied
Session lifecycleSessionStart, SessionEnd, Setup, Stop, StopFailure, InstructionsLoaded
Subagents & tasksSubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle
Context & inputUserPromptSubmit, PreCompact, PostCompact, Elicitation, ElicitationResult
EnvironmentConfigChange, WorktreeCreate, WorktreeRemove, CwdChanged, FileChanged
MiscNotification

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

TypeFieldsNotes
commandcommand, if, shell, timeout, once, async, asyncRewakeRuns a shell command. asyncRewake runs it in the background and wakes the model if it exits with code 2.
promptprompt (uses $ARGUMENTS), timeout, modelLLM-evaluated condition — works on tool events and lifecycle events like Stop.
agentprompt, timeout (default 60s), model (default Haiku)Agentic verifier hook, runs with tools — works on tool events and lifecycle events like Stop.
httpurl, headers, allowedEnvVarsPOSTs 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

FieldTypeNotes
version1Defaults to 1.
enabledbooleanGlobal switch for this config file.
maxChainDepthintegerGlobal depth guard (default 2, max 10).
defaultCooldownMsintegerDefault rule cooldown in ms (default 30000).
defaultDedupWindowMsintegerDefault action dedup window in ms (default 30000).
rulesHookChainRule[]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 fieldMatches against
toolNamestool_name / toolName in the event payload
taskStatusestask_status / taskStatus / status
errorIncludesCase-insensitive substring match against error / reason / message
eventFieldEqualsDot-path equality against the payload, e.g. "meta.source": "scheduler"

Action types

TypeDoesKey fields
spawn_fallback_agentLaunches a recovery agent with a templated promptpromptTemplate, agentType, model
notify_teamPosts a templated message to team contextteamName, recipients, summary, messageTemplate
warm_remote_capacityPre-warms a remote bridge environmentcreateDefaultEnvironmentIfMissing

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

SymptomCheck
Rule never triggerstrigger.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 skippedCommon 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 reflectedThe 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 unexpectedlySet "enabled": false at the top level, or globally disable with CLAUDIN_ENABLE_HOOK_CHAINS=0, then re-enable one rule at a time.