# claude-forge Hooks (v3.0) > Claude Code harness hooks — opt-in handlers for 21 lifecycle events. > Hooks fire inside the Claude Code runtime, read JSON from stdin, and gate behavior via exit codes. ## Event Catalog Claude Code exposes **27 hookable events** across 7 categories (official catalog as of 2026-04-23). claude-forge ships examples for the 21 most common; the remaining 6 are listed below without examples — implement per project need. Install only what you need. | Category | Event | Trigger | Common Use | Example | |----------|-------|---------|------------|---------| | Session | `SessionStart` | New session boots / `--continue` resume | Inject context, warm caches, print banners | `context-sync-suggest.sh` | | Session | `SessionEnd` | Clean session shutdown | Persist summary, sync TODAY.md | `work-tracker-stop.sh` | | Turn | `UserPromptSubmit` | User sends a prompt | Track activity, pre-flight checks | `work-tracker-prompt.sh` | | Turn | `Stop` | Assistant finishes a clean turn | Nudge session-wrap, commit suggest | `session-wrap-suggest.sh` | | Turn | `StopFailure` | Session ends abnormally (crash, rate-limit) | Dump crash report, set recovery flag | `examples/stop-failure.sh.example` | | Tool | `PreToolUse` | Before a tool runs | Guard destructive commands, rate-limit MCP | `remote-command-guard.sh` | | Tool | `PostToolUse` | After a tool runs (success OR failure) | Filter output secrets, log usage | `output-secret-filter.sh` | | Tool | `PostToolUseFailure` | Tool returns non-zero (failure only) | Escalate repeated failures, alert | `examples/post-tool-use-failure.sh.example` | | Subagent | `SubagentStart` | Task tool spawns a subagent | Record subagent name + start time | `examples/subagent-start.sh.example` | | Subagent | `SubagentStop` | Subagent finishes (success or failure) | Log duration, cost accounting | `examples/subagent-stop.sh.example` | | Context | `PreCompact` | Before context compaction runs | Snapshot TODAY.md / plan.md | `examples/pre-compact.sh.example` | | Context | `PostCompact` | After compaction completes | Persist summary for audit/relay | `examples/post-compact.sh.example` | | System | `ConfigChange` | `settings.json` mutated | Revalidate schema, reload hooks | (custom) | | System | `CwdChanged` | Working directory changes | Reset project context | (custom) | | System | `FileChanged` | Claude edits a watched file | Kick linters / formatters | (custom) | | System | `InstructionsLoaded` | CLAUDE.md / rules loaded | Audit rule sources | (custom) | | System | `Notification` | User-facing notification emitted | Mirror to Discord/Slack | (custom) | | System | `PermissionRequest` | A permission prompt is shown (auto/manual decision pending) | Audit or custom auto-approval | (custom) | | System | `TaskCreated` | Task tool creates a new task | Log assignment, notify team | `examples/task-created.sh.example` | | System | `TaskCompleted` | Task marked completed | Close loop, emit metrics | `task-completed.sh` | | Worktree | `WorktreeCreate` | New git worktree registered | Seed `.claude/` symlink, log | `examples/worktree-create.sh.example` | | Worktree | `WorktreeRemove` | Worktree torn down | Block if unmerged, cleanup | `examples/worktree-remove.sh.example` | ### Additional events (spec-only, no example shipped) The following 6 events are defined in the official Claude Code spec (see [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)) but claude-forge v3.0 does not ship example handlers. Consult the spec if you need to wire them: | Event | Trigger (official) | |-------|-------------------| | `UserPromptExpansion` | A `@file` / `@folder` reference is expanded inline before tool dispatch | | `PermissionDenied` | User explicitly rejected a permission request | | `TeammateIdle` | A spawned teammate agent has been idle past threshold | | `Elicitation` | Hook or tool requests structured input from the user | | `ElicitationResult` | User response to an elicitation is captured | | `TaskStatusChanged` | Any `TaskUpdate` mutates a task's status field (not just completion) | ## Hook Handler Types Each hook entry specifies a `type`. Claude Code supports four (Tier 0 source: [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks), verified 2026-04-23): | Type | Purpose | Notes | |------|---------|-------| | `command` | Run a shell script (most common) | Takes `command`, optional `timeout` in **seconds** (default 600). Inline env-var assignment supported: `FOO=bar ~/.claude/hooks/foo.sh`. Default shell = bash, `powershell` opt-in on Windows. | | `http` | POST the payload to an HTTP endpoint | Takes `url`, `headers` — good for Zapier/webhooks | | `prompt` | Invoke a nested LLM with a preset prompt | Takes `prompt`, `model`, optional `timeout` (seconds; default 30) — used for "auto-review" style checks | | `agent` | Invoke an MCP tool / subagent directly | Takes `server`, `tool`, `arguments`, optional `timeout` (seconds; default 60) — or agent reference for Task dispatch | > **v3.0.1 correction**: Earlier revisions of this guide labelled the last two types as `llm-prompt` and `mcp-tool`. Those names are not in the official spec — the correct identifiers are `prompt` and `agent`. If any of your hooks reference the old names, rename them before upgrading. ## Matcher Schema Hooks in the same event can be filtered by a `matcher` glob to narrow when they fire. ```json { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "~/.claude/hooks/remote-command-guard.sh", "timeout": 5 } ] }, { "matcher": "mcp__*", "hooks": [ { "type": "command", "command": "~/.claude/hooks/rate-limiter.sh" } ] } ], "SubagentStop": [ { "hooks": [ { "type": "command", "command": "~/.claude/hooks/subagent-stop.sh" } ] } ] } ``` Matcher semantics: - Omit `matcher` to catch all invocations of that event. - `matcher` accepts exact tool names (`Bash`, `Edit`) or globs (`mcp__*`, `Edit|Write`). - Multiple matcher blocks in the same event are OR-ed. ## Security Notes - **Exit codes** — `0` = success, `2` = blocking error (rejects the action), anything else = non-blocking warning logged to stderr. - **Timeout** — Default 600 s for `command` type (30 s for `prompt`, 60 s for `agent`). Set `timeout` (**seconds**, not ms) to enforce per-hook budget; blocking hooks that run long will stall the session. **SessionEnd has a special default 1.5 s** auto-raised to 60 s based on per-hook timeouts — override via `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS` env (in ms). - **Input** — Hooks receive the full event payload on stdin as JSON. Parse defensively; never assume fields exist. - **Secrets** — Never `echo $ANTHROPIC_API_KEY` or print env vars; PostToolUse secret filter may not cover stderr. - **Side-effects** — Avoid network calls without rate-limit guards. Long-running hooks block the whole session. - **Scripts** — Must be executable (`chmod +x`). Use absolute paths or `~/` expansion. ## Timing Wrapper (`_lib/timing.sh`, v3.0.1) Use `_lib/timing.sh` to wrap any hook and record start/end/duration into an append-only JSONL log. Especially useful for verifying whether `async: true` hooks actually run in parallel (distinct `start_ms` within the same event payload = parallel) versus serially. **Usage in `settings.json`:** ```json { "type": "command", "command": "HOOK_EVENT=SessionEnd ~/.claude/hooks/_lib/timing.sh ~/.claude/hooks/discord-notify.sh stop", "timeout": 8, "async": true } ``` **What gets logged** (`~/.claude/logs/hook-timing.jsonl`, one JSON per line): ```json {"hook":"discord-notify","cmd":"~/.claude/hooks/discord-notify.sh","pid":52341,"ppid":52340,"start_iso":"2026-04-23T01:18:18.828Z","start_ms":1776907098798,"end_ms":1776907099412,"duration_ms":614,"exit_code":0,"event":"SessionEnd","session_id":"..."} ``` **Analyzing parallelism** (after one session end): ```bash jq -s 'map(select(.event == "SessionEnd")) | sort_by(.start_ms)' ~/.claude/logs/hook-timing.jsonl ``` If all hooks in a batch have `start_ms` within ~50 ms of each other → **truly parallel**. If they stagger by `duration_ms` → effectively serial despite `async: true`. **Overhead:** ~60-140 ms per hook (python3 startup + JSON write). Acceptable for SessionEnd batching; avoid wrapping hot-path hooks like `PostToolUse` on Edit unless debugging. **Aggregation:** ```bash # Per-event summary jq -sr 'group_by(.event)[] | "\(.[0].event): count=\(length), max=\(map(.duration_ms)|max)ms, total_wall=\(map(.end_ms)|max - (map(.start_ms)|min))ms"' ~/.claude/logs/hook-timing.jsonl ``` `total_wall` = `max(end_ms) - min(start_ms)` across the batch — this is what the user actually waits for. Compare it to `sum(duration_ms)` to see if async parallelism is real. ## Migration from v2.1 v2.1 shipped **5 events** and shared a flat handler list. v3.0 expands to **21 events** with opt-in per-event installation. Existing v2.1 hooks continue to work unchanged. | v2.1 event | v3.0 replacement | Breaking? | |------------|------------------|-----------| | `SessionStart` | `SessionStart` | No | | `UserPromptSubmit` | `UserPromptSubmit` | No | | `PreToolUse` | `PreToolUse` | No | | `PostToolUse` | `PostToolUse` + new `PostToolUseFailure` | No (additive) | | `Stop` | `Stop` + new `StopFailure` | No (additive) | | — | 16 new events (Subagent / Context / System / Worktree) | Opt-in | Opt-in steps: 1. Copy the desired `examples/*.example` into `~/.claude/hooks/.sh`. 2. `chmod +x ~/.claude/hooks/.sh`. 3. Append a new entry to the corresponding event array in `~/.claude/settings.json`. 4. Restart your Claude Code session (or `claude --reload-settings` if available). To disable a hook without deleting it, comment the entry out of `settings.json` or remove the `+x` bit.