Hooks Reference
Every event, field, schema, exit code, and decision rule for the hooks system.
Hooks are configured under the hooks key in settings.json. Each event array has two levels of nesting: a HookDefinition picks which tools the hook applies to (via matcher), and a HookEntry is the handler that runs them.
One HookDefinition can own multiple HookEntry handlers. They all run for the same matcher, in the order listed.
Hook Definition fields (outer)
Chooses which tools this group of handlers applies to.
| Field | Required | Type | Description |
|---|---|---|---|
matcher | Optional | string | Omit to match every tool. Examples: "shell", "write|edit" |
hooks | Required | array | One or more handlers. Runs in the order listed |
HookEntry fields (inner)
Describes a single handler to execute.
| Field | Required | Type | Description |
|---|---|---|---|
type | Required | string | Handler kind. Supports command adapter only |
command | Required when type: "command" | string | Shell command to execute |
timeout | Optional | seconds | Defaults to 30, maximum 600 |
Example settings.json
In the example below, the PreToolUse hook scopes a 10-second guard to shell and write tool calls. The PostToolUse hook omits timeout (defaults to 30s) and audits every tool after it runs.
Before your hook runs, Command Code writes a single JSON object to its stdin. Read stdin to the end, parse it as JSON, then write your response to stdout.
Common fields
Present on all events.
| Field | Type | Description |
|---|---|---|
session_id | string | Session identifier, stable for the lifetime of one CLI session |
transcript_path | string | Absolute path to this session's transcript JSONL |
cwd | string | Absolute working directory at fire time |
hook_event_name | string | The event that fired the hook. |
permission_mode | "standard" | "auto-accept" | "plan" | Current permission mode |
Present on every event tied to a tool call.
| Field | Type | Description |
|---|---|---|
tool_use_id | string? | Stable tool invocation id. Present on every real tool call |
tool_name | string | Canonical tool id (shell_command, read_file, write_file, edit_file) |
tool_display_name | string | One of SHELL, READ, WRITE, EDIT. The value matcher is tested against |
tool_input | object | Tool arguments as emitted by the model. Shape depends on the tool, see below |
tool_input fields
The shape of tool_input depends on which tool fired. Hooks read these fields to inspect a call. Example, a shell guard checks tool_input.command, a write audit reads tool_input.file_path.
| Tool | fields |
|---|---|
shell_command | command: string, args?: string[], directory?: string, timeout?: number |
read_file | absolute_path: string, offset?: number, limit?: number |
write_file | file_path: string, content: string |
edit_file | file_path: string, old_value: string, new_value: string, replacement_count?: number, replace_all?: boolean |
Event-specific fields
An event may add its own fields on top of the common and tool-call sets. New events are introduced over time; each one appears as a subsection below.
PostToolUse
| Field | Type | Description |
|---|---|---|
tool_response | string | Output of the tool, the same text the model will see |
Stop
Stop fires when the assistant produces its final response with no remaining tool calls (end of turn). It carries no tool fields, only the common fields plus:
| Field | Type | Description |
|---|---|---|
stop_hook_active | boolean | true when this fire is itself the retry caused by a previous Stop hook returning decision: "block" or exit 2. Hook authors check this and exit 0 to bail out of retry loops |
Command Code injects four environment variables into every hook process.
| Variable | Value |
|---|---|
COMMANDCODE_PROJECT_DIR | Absolute path to the project (same as cwd) |
COMMANDCODE_SESSION_ID | Session ID, useful for correlating hooks to a run |
COMMANDCODE_HOOK_EVENT | PreToolUse, PostToolUse, or Stop |
COMMANDCODE_CWD | Alias of COMMANDCODE_PROJECT_DIR with the identical value |
Your environment variables are forwarded to hook processes with any sensitive variable being stripped out.
The hook's executable writes a single JSON object to stdout. All fields are optional. Empty stdout on exit 0 means "no opinion, allow".
Common fields
| Field | Type | Description |
|---|---|---|
continue | boolean | false halts the session after the current tool batch. Pair with stopReason |
stopReason | string | User-facing message shown in the TUI when continue: false. Not sent to the model |
suppressOutput | boolean | When true, omit the hook's parsed output from the audit log |
systemMessage | string | Free-text notice surfaced in the TUI feed. Not sent to the model |
PreToolUseOutput fields
Adds a hookSpecificOutput object on top of the common fields.
| Field | Type | Description |
|---|---|---|
hookSpecificOutput.hookEventName | "PreToolUse" | Optional. Helps user distinguish the PreToolUse shape; the engine already knows which event fired |
hookSpecificOutput.permissionDecision | "allow" | "deny" | "deny" blocks the tool. Omit or "allow" to permit |
hookSpecificOutput.permissionDecisionReason | string | Shown to the model when denying. Use this to teach the model not to retry |
hookSpecificOutput.additionalContext | string | Appended to the tool result before the model's next turn |
Full shape:
PostToolUseOutput fields
Adds top-level decision / reason and a smaller hookSpecificOutput.
| Field | Type | Description |
|---|---|---|
decision | "block" | Advisory retry signal to the model. The tool already ran, so nothing is un-done |
reason | string | Pairs with decision: "block" |
hookSpecificOutput.hookEventName | "PostToolUse" | Optional. Helps readers distinguish the PostToolUse shape; the engine already knows which event fired |
hookSpecificOutput.additionalContext | string | Appended to the tool result before the model's next turn |
Full shape:
StopOutput fields
Stop uses only top-level fields, with no hookSpecificOutput.
| Field | Type | Description |
|---|---|---|
decision | "block" | Prevents the assistant from finishing. The agent loop runs one more iteration. Capped at 3 retries per turn |
reason | string | Shown to the user, and fed to the model on retry, wrapped in framing so it reads as revision feedback. For raw diagnostics the model treats like tool output, use exit 2 + stderr |
Full shape:
Loop prevention
A naive Stop hook that always returns decision: "block" would loop forever. Two safety layers:
stop_hook_activeon input istrueon the retry fire. Hook authors shouldexit 0when they see it.- Hard cap of 3 retries per turn enforced by the engine. After the cap, the turn ends with a
Stop hook retry cap reached (3)line that names the offending script so you can fix or disable it.
Feeding text to the model on retry
Both retry triggers feed text to the model before the next turn. The conversation can't end on an assistant turn, so a retry always carries a user-role message:
decision: "block"+reason:reasonis wrapped in framing ("a Stop hook asked you to revise…") so the model treats it as revision feedback on its previous response, not a fresh request.exit 2+ stderr: the full stderr (capped, so multi-linetsc/lint diagnostics come through intact) is fed raw, so the model treats it like tool output and can act on it directly:
Use reason for natural-language revision guidance; use exit 2 + stderr for machine-style diagnostics the model should act on verbatim.
Who sees each field
Use this to pick the right field for the audience you want to reach.
| Field | User (TUI) | Model |
|---|---|---|
stopReason | ✓ | — |
systemMessage | ✓ | — |
permissionDecisionReason (Pre) | ✓ | ✓ (when denying) |
reason (Post / Stop) | ✓ | ✓ (Post & Stop when decision: "block"; an exit 2 retry feeds stderr instead) |
additionalContext | — | ✓ (appended before next turn) |
stderr (exit 2) | ✓ first line | ✓ (full text fed to model on retry, all events) |
Rule of thumb: for machine-style detail the model should act on verbatim, use exit 2 + stderr. For natural-language revision guidance, use Stop's reason. For a user-only notice, use systemMessage or stopReason.
The exit code is the fast path. Most hooks only ever use 0.
| Exit | Stdout handling | Effect on tool | Effect on session |
|---|---|---|---|
0 | Parsed as JSON | Determined by output (see decision matrix) | Continues (unless continue: false) |
2 | Ignored | PreToolUse: blocked. PostToolUse: advisory retry signal. Stop: n/a | PreToolUse / PostToolUse: continues. Stop: retries the turn |
| any other | Parsed if present | Tool proceeds, non-blocking error logged | Continues |
Exit code 2 block-reason resolution
The text sent to the model when a hook exits 2 is resolved in this order:
hookSpecificOutput.permissionDecisionReason(if stdout parses and sets one)- Top-level
reason(PostToolUse only) - Trimmed first line of stderr
- Generic fallback text naming the hook and its exit code
Exit code 0 stdout handling
- Empty or whitespace-only stdout means "no opinion" and the tool proceeds.
- Non-empty stdout that fails to parse as JSON, or parses but fails schema validation, is logged as a warning and the tool proceeds.
Each hook result produces two effects: whether the tool runs, and whether the session continues afterward.
| Signal | Value | Tool | Session |
|---|---|---|---|
exit code | 2 (PreToolUse) | skipped | continues |
exit code | 2 (PostToolUse) | (already ran) advisory retry | continues |
exit code | 2 (Stop) | n/a | retries the turn; stderr is fed to the model |
exit code | 0 | see output | see output |
exit code | other | runs | continues |
hookSpecificOutput.permissionDecision | "deny" (PreToolUse) | skipped | continues |
decision | "block" (PostToolUse) | (already ran) advisory retry | continues |
decision | "block" (Stop) | n/a | retries the turn (capped at 3); reason shown to the user and fed to the model |
continue | false | runs | halts after this batch |
When a PreToolUse hook denies the tool: the model receives the permissionDecisionReason (or stderr on exit 2) as the tool result. Remaining PreToolUse hooks for that call are skipped.
When any hook sets continue: false: every hook in the current batch still runs to completion. The session halts after.
How the engine runs hooks once a tool call fires.
Shell
Every hook command is spawned through a system shell. The JSON input is piped on stdin, the hook writes its response JSON to stdout. The first non-empty line of stderr is used as a fallback block reason when a hook exits 2.
Ordering
PreToolUsehooks run sequentially in the order they appear insettings.json. Execution stops as soon as one hook denies the tool (viapermissionDecision: "deny"or exit2); later hooks for the same event do not run.PostToolUsehooks run in parallel. One crashing hook cannot cancel another. Returned results preserve the order they appear insettings.json, not completion order.Stophooks run in parallel, likePostToolUse. Matchers on Stop hooks are silently ignored, since Stop has no tool to match against.
Timeouts
- Default timeout is 30 seconds. Override per hook with
timeout(seconds, capped at 600). - On timeout the engine sends
SIGTERM. Hooks that trapSIGTERMget a 5-second grace period beforeSIGKILL.
Isolation
Each hook fires with its own process and its own copy of stdin. Hooks cannot read each other's stdout or stderr, and cannot pass information between themselves. When multiple hooks match the same tool call, the outputs are combined by the rules in the decision matrix; no hook sees any other hook's result.
The permission_mode field on stdin is one of:
| Value | Description |
|---|---|
"standard" | Default. Model requests permission before each tool |
"auto-accept" | Permission prompts auto-accepted |
"plan" | Plan mode. Tool calls are restricted to read-only operations. Hooks are skipped entirely in plan mode |
Plan mode is read-only by design, so no PreToolUse guard is needed and no PostToolUse audit will fire. If your hook looks broken, check whether the session is in plan mode first.
.commandcode/hooks/guard-shell.sh
- Hooks overview: mental model and quickstart
- Examples: copy-paste hooks for blocking, context injection, and auditing
- Configuration: scopes, precedence, and file locations
- Best Practices: write safe hooks and debug with
cmd --debug - CLI Reference: flags and commands that pair with hooks
- Stuck on a schema edge case? Ask in Discord