Hooks

May 7, 2026 · View on GitHub

last-updated: 2026-05-07

Hooks are Node.js scripts that fire automatically at lifecycle events in Claude Code. You never invoke them manually. They provide automated quality enforcement and telemetry.

Active Hooks (29 of 29 Claude Code events)

HookEventPurpose
protect-files.jsPreToolUseBlock edits to protected files and out-of-scope paths
external-action-gate.jsPreToolUse (Bash)Gate external actions (git push, API calls)
governance.jsPreToolUse (Edit/Write/Bash/Agent)Audit every significant tool call
post-edit.jsPostToolUsePer-file typecheck + structural/performance/visual lenses
organize-enforce.jsPostToolUse (Edit/Write)Enforce file placement conventions
circuit-breaker.jsPostToolUse (Bash) + PostToolUseFailureDetect failure loops
cost-tracker.jsPostToolUseReal-time session cost monitoring
complexity-check.jsPostToolUse (Edit/Write)Advisory complexity score for JS/TS files
post-tool-batch.jsPostToolBatchWave-level quality checkpoint (async, asyncRewake)
quality-gate.jsStopCold-path anti-pattern scan before session ends
stop-failure.jsStopFailureLog hook failures
user-prompt-submit.jsUserPromptSubmitLog turn boundaries; extension point for prompt gating
user-prompt-expansion.jsUserPromptExpansionLog skill invocations to skill-usage.jsonl
init-project.jsSessionStart + SetupScaffold .planning/ state; also runs in --init-only mode
restore-compact.jsSessionStart (compact)Restore context after compression
intake-scanner.jsSessionStartReport pending work items
session-end.jsSessionEndFlush session telemetry
subagent-start.jsSubagentStartBind fleet agent identity at spawn time
subagent-stop.jsSubagentStopLog agent completion + flag abnormal exits
teammate-idle.jsTeammateIdleLog teammate idle events (multi-instance fleet)
permission-request.jsPermissionRequest + PermissionDeniedAuto-approve safe Citadel ops, log all decisions
instructions-loaded.jsInstructionsLoadedDetect CLAUDE.md reloads, queue doc-sync
file-changed.jsFileChangedReact to file-on-disk changes; queue doc-sync and skill-lint
cwd-changed.jsCwdChangedLog directory changes; flag when moving outside project root
config-change.jsConfigChangeDetect harness.json / settings.json changes mid-session
elicitation.jsElicitation + ElicitationResultLog MCP elicitation requests; never auto-responds
notification.jsNotificationElevated audit for auth events; log idle alerts
task-events.jsTaskCreated + TaskCompletedTask lifecycle telemetry
worktree-setup.jsWorktreeCreateInitialize agent worktrees
worktree-remove.jsWorktreeRemoveClean up worktree state
pre-compact.jsPreCompactSave context before compression
post-compact.jsPostCompactRestore compact state

Lifecycle Events (all 29)

EventWhenCan Block?Citadel Hook
Setup--init-only or --maintenance modeNoinit-project.js
UserPromptSubmitBefore Claude processes each user promptYesuser-prompt-submit.js
UserPromptExpansionSlash command expandsYesuser-prompt-expansion.js
SessionStartNew conversation beginsNoinit-project.js, restore-compact.js, intake-scanner.js
PreToolUseBefore a tool executesYes (exit 2)protect-files.js, external-action-gate.js, governance.js
PostToolUseAfter a tool completesNopost-edit.js, organize-enforce.js, circuit-breaker.js, cost-tracker.js, complexity-check.js
PostToolBatchAfter ALL parallel tools in a wave settleNopost-tool-batch.js
PostToolUseFailureAfter a tool failsNocircuit-breaker.js
StopSession turn endingNoquality-gate.js
StopFailureHook error on StopNostop-failure.js
SessionEndSession terminatedNosession-end.js
SubagentStartSubagent spawns (Agent tool)Nosubagent-start.js
SubagentStopSubagent session endsNosubagent-stop.js
TeammateIdleA Claude Code teammate goes idleNoteammate-idle.js
PermissionRequestPermission dialog appearsYes (via JSON output)permission-request.js
PermissionDeniedAuto-mode denies a toolNopermission-request.js
InstructionsLoadedCLAUDE.md or rules/*.md loadedNoinstructions-loaded.js
FileChangedWatched file changes on diskNofile-changed.js
CwdChangedWorking directory changesNocwd-changed.js
ConfigChangeSettings file changes mid-sessionNoconfig-change.js
ElicitationMCP server requests user inputNoelicitation.js
ElicitationResultUser responds to MCP elicitationNoelicitation.js
NotificationPermission prompts, idle alerts, auth eventsNonotification.js
TaskCreatedTask createdNotask-events.js
TaskCompletedTask completedNotask-events.js
PreCompactBefore message compressionNopre-compact.js
PostCompactAfter compressionNopost-compact.js
WorktreeCreateAgent creates a worktreeNoworktree-setup.js
WorktreeRemoveWorktree deletedNoworktree-remove.js

Hook Protocol

Hooks receive a JSON payload on stdin and communicate results via:

MechanismHowWhen
Exit 0Success — no blockAlways for observer hooks
Exit 2Block — abort the toolPreToolUse and UserPromptSubmit only
additionalContextJSON {"additionalContext": "text"} on stdoutInject text into Claude's context window
hookSpecificOutputJSON on stdoutPermissionRequest auto-approve decisions
asyncRewake: trueDeclared in hook registrationRun async, wake Claude only on exit 2

Key protocol fields from the event payload that hooks consume:

FieldAvailable OnUsed By
agent_idAll events inside subagentsgovernance.js, subagent-start.js, post-edit.js
agent_typeAll events inside subagentsgovernance.js, subagent-start.js, post-edit.js
duration_msPostToolUsepost-edit.js (wall-clock timing, excluding permission prompts)
file_pathPostToolUse (Write/Edit/Read)post-edit.js, organize-enforce.js

Configuration

Hook definitions live in hooks/hooks-template.json. Installed per-project via scripts/install-hooks.js:

# From your project directory:
node /path/to/Citadel/scripts/install-hooks.js

To force the full hook surface after upgrading Claude Code:

node /path/to/Citadel/scripts/install-hooks.js --hook-profile latest

PostToolBatch — Wave-Level Quality Checkpoint

post-tool-batch.js fires once after all parallel tool calls in a wave settle, rather than once per tool. This is the wave-level checkpoint — more efficient than per-tool checks for multi-file edit waves.

Registered with async: true, asyncRewake: true — runs in the background without blocking the edit path. If it exits 2, Claude Code wakes Claude with the stderr as feedback. Currently exit 0 only (observer mode).

Permission Auto-Approval

permission-request.js auto-approves known-safe Citadel operations without showing the permission dialog. Safe patterns:

  • node .citadel/scripts/*.js (telemetry delegates)
  • Write/Edit to .planning/** (campaign and fleet state)
  • Write/Edit to .citadel/** (harness scaffolding)

All permission requests (approved and deferred) are logged to audit.jsonl.

additionalContext Output

quality-gate.js (Stop) and post-tool-batch.js (PostToolBatch) inject quality signals directly into Claude's context window via the additionalContext protocol field, rather than printing to stderr. This means Claude sees the violation summary in its context without relying on stderr display.

CITADEL_UI mode (when CITADEL_UI=true) uses the Citadel-formatted JSON instead.

Language-Adaptive Typecheck

The post-edit.js hook detects your project's language from .claude/harness.json and runs the appropriate checker:

LanguageCheckerPer-File?
TypeScripttsc --noEmitYes
Pythonmypy or pyrightYes
Gogo vetPackage-level
Rustcargo checkProject-level

Configure in harness.json:

{
  "typecheck": {
    "command": "npx tsc --noEmit",
    "perFile": true
  }
}

Dependency-Aware Pattern Detection

The post-edit.js hook warns agents when they use raw APIs that an installed library already handles. Configure in harness.json:

{
  "dependencyPatterns": [
    {
      "dependency": "@tanstack/react-query",
      "banned": ["fetch(", "axios("],
      "message": "Use tanstack query instead of raw fetch"
    }
  ]
}

Quality Gate Rules

RuleWhat It Catches
no-confirm-alertconfirm(), alert(), prompt() in JS/TS
no-transition-alltransition-all in CSS/JSX
no-magic-intervalsHardcoded setInterval numbers

Add custom rules in harness.json:

{
  "qualityRules": {
    "builtIn": ["no-confirm-alert", "no-transition-all"],
    "custom": [
      {
        "name": "no-console-log",
        "pattern": "console\\.log\\(",
        "filePattern": "\\.(ts|tsx)$",
        "message": "Remove console.log before committing"
      }
    ]
  }
}

Circuit Breaker

Tracks tool failures. After 3 failures: suggests alternatives. After 5: escalates to "stop and rethink". State stored in .claude/circuit-breaker-state.json (gitignored).

Rules

  1. Hooks are fail-safe. Observer hooks always exit 0. Only PreToolUse and UserPromptSubmit can block (exit 2).
  2. Hot-path hooks must be fast. PostToolUse fires on every edit — keep it under 5 seconds.
  3. Use additionalContext for feedback. Inject quality signals into Claude's context window rather than printing to stderr.
  4. Heavy checks use asyncRewake. Slow quality checks (typecheck, test runs) run async on PostToolBatch — zero blocking penalty on the edit path.
  5. Fleet agents are attributed. agent_id and agent_type are captured on every audit log entry when inside a subagent.