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.

settings.json └── hooks └── <EventName> └── [ HookDefinition ] // matcher + list of handlers └── hooks: [ HookEntry ] // type + command + timeout

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.

FieldRequiredTypeDescription
matcherOptionalstringOmit to match every tool. Examples: "shell", "write|edit"
hooksRequiredarrayOne or more handlers. Runs in the order listed

HookEntry fields (inner)

Describes a single handler to execute.

FieldRequiredTypeDescription
typeRequiredstringHandler kind. Supports command adapter only
commandRequired when type: "command"stringShell command to execute
timeoutOptionalsecondsDefaults 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.

{ "hooks": { "PreToolUse": [ { "matcher": "shell|write", "hooks": [ { "type": "command", "command": "./.commandcode/hooks/guard-tools.sh", "timeout": 10 } ] } ], "PostToolUse": [ { "hooks": [ { "type": "command", "command": "./.commandcode/hooks/audit.sh" } ] } ] } }

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.

FieldTypeDescription
session_idstringSession identifier, stable for the lifetime of one CLI session
transcript_pathstringAbsolute path to this session's transcript JSONL
cwdstringAbsolute working directory at fire time
hook_event_namestringThe event that fired the hook.
permission_mode"standard" | "auto-accept" | "plan"Current permission mode

Present on every event tied to a tool call.

FieldTypeDescription
tool_use_idstring?Stable tool invocation id. Present on every real tool call
tool_namestringCanonical tool id (shell_command, read_file, write_file, edit_file)
tool_display_namestringOne of SHELL, READ, WRITE, EDIT. The value matcher is tested against
tool_inputobjectTool 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.

Toolfields
shell_commandcommand: string, args?: string[], directory?: string, timeout?: number
read_fileabsolute_path: string, offset?: number, limit?: number
write_filefile_path: string, content: string
edit_filefile_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

FieldTypeDescription
tool_responsestringOutput of the tool, the same text the model will see

Command Code injects four environment variables into every hook process.

VariableValue
COMMANDCODE_PROJECT_DIRAbsolute path to the project (same as cwd)
COMMANDCODE_SESSION_IDSession ID, useful for correlating hooks to a run
COMMANDCODE_HOOK_EVENTPreToolUse or PostToolUse
COMMANDCODE_CWDAlias 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

FieldTypeDescription
continuebooleanfalse halts the session after the current tool batch. Pair with stopReason
stopReasonstringUser-facing message shown in the TUI when continue: false. Not sent to the model
suppressOutputbooleanWhen true, omit the hook's parsed output from the audit log
systemMessagestringFree-text notice surfaced in the TUI feed. Not sent to the model

PreToolUseOutput fields

Adds a hookSpecificOutput object on top of the common fields.

FieldTypeDescription
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.permissionDecisionReasonstringShown to the model when denying. Use this to teach the model not to retry
hookSpecificOutput.additionalContextstringAppended to the tool result before the model's next turn

Full shape:

{ "continue": true, "suppressOutput": false, "stopReason": "", "systemMessage": "", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "", "additionalContext": "" } }

PostToolUseOutput fields

Adds top-level decision / reason and a smaller hookSpecificOutput.

FieldTypeDescription
decision"block"Advisory retry signal to the model. The tool already ran, so nothing is un-done
reasonstringPairs with decision: "block"
hookSpecificOutput.hookEventName"PostToolUse"Optional. Helps readers distinguish the PostToolUse shape; the engine already knows which event fired
hookSpecificOutput.additionalContextstringAppended to the tool result before the model's next turn

Full shape:

{ "continue": true, "suppressOutput": false, "stopReason": "", "systemMessage": "", "decision": "block", "reason": "", "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "" } }

Who sees each field

Use this to pick the right field for the audience you want to reach.

FieldUser (TUI)Model
stopReason
systemMessage
permissionDecisionReason (Pre)✓ (when denying)
reason (Post)✓ (when decision: "block")
additionalContext✓ (appended before next turn)

Rule of thumb: if you want to guide the model, use additionalContext or permissionDecisionReason. If you want to display/explain to the user, use systemMessage or stopReason.


The exit code is the fast path. Most hooks only ever use 0.

ExitStdout handlingEffect on toolEffect on session
0Parsed as JSONDetermined by output (see decision matrix)Continues (unless continue: false)
2IgnoredPreToolUse: blocked. PostToolUse: advisory retry signalContinues
any otherParsed if presentTool proceeds, non-blocking error loggedContinues

Exit code 2 block-reason resolution

The text sent to the model when a hook exits 2 is resolved in this order:

  1. hookSpecificOutput.permissionDecisionReason (if stdout parses and sets one)
  2. Top-level reason (PostToolUse only)
  3. Trimmed first line of stderr
  4. 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.

SignalValueToolSession
exit code2 (PreToolUse)skippedcontinues
exit code2 (PostToolUse)(already ran) advisory retrycontinues
exit code0see outputsee output
exit codeotherrunscontinues
hookSpecificOutput.permissionDecision"deny" (PreToolUse)skippedcontinues
decision"block" (PostToolUse)(already ran) advisory retrycontinues
continuefalserunshalts 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

  • PreToolUse hooks run sequentially in the order they appear in settings.json. Execution stops as soon as one hook denies the tool (via permissionDecision: "deny" or exit 2); later hooks for the same event do not run.
  • PostToolUse hooks run in parallel. One crashing hook cannot cancel another. Returned results preserve the order they appear in settings.json, not completion order.

Timeouts

  • Default timeout is 30 seconds. Override per hook with timeout (seconds, capped at 600).
  • On timeout the engine sends SIGTERM. Hooks that trap SIGTERM get a 5-second grace period before SIGKILL.

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:

ValueDescription
"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

#!/usr/bin/env bash set -euo pipefail # Read the entire stdin payload once. payload=$(cat) # Common fields on every event. session_id=$(printf '%s' "$payload" | jq -r '.session_id') cwd=$(printf '%s' "$payload" | jq -r '.cwd') event=$(printf '%s' "$payload" | jq -r '.hook_event_name') # Tool-call fields, present on every tool event. tool_name=$(printf '%s' "$payload" | jq -r '.tool_name') tool_display=$(printf '%s' "$payload" | jq -r '.tool_display_name') cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // ""') # Command Code also injects env vars for the same context. : "${COMMANDCODE_PROJECT_DIR:?}" : "${COMMANDCODE_SESSION_ID:?}" : "${COMMANDCODE_HOOK_EVENT:?}" # Deny a destructive command with a reason the model will see. if [[ "$cmd" == *"rm -rf /"* ]]; then jq -n --arg cmd "$cmd" '{ continue: true, systemMessage: "Blocked destructive command", hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: ("Policy forbids: " + $cmd) } }' exit 0 fi # Otherwise allow and attach extra context for the model's next turn. jq -n --arg event "$event" '{ continue: true, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", additionalContext: ("Verified by guard-shell.sh during " + $event) } }'