command-code 0.52.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +2541 -0
  2. package/dist/bundled/command-code-knowledge/SKILL.md +52 -0
  3. package/dist/bundled/command-code-knowledge/reference/checkpoints.md +366 -0
  4. package/dist/bundled/command-code-knowledge/reference/custom-agents.md +91 -0
  5. package/dist/bundled/command-code-knowledge/reference/custom-slash-commands.md +345 -0
  6. package/dist/bundled/command-code-knowledge/reference/headless.md +234 -0
  7. package/dist/bundled/command-code-knowledge/reference/hooks.md +1097 -0
  8. package/dist/bundled/command-code-knowledge/reference/mcp.md +626 -0
  9. package/dist/bundled/command-code-knowledge/reference/memory.md +98 -0
  10. package/dist/bundled/command-code-knowledge/reference/models.md +81 -0
  11. package/dist/bundled/command-code-knowledge/reference/permissions.md +412 -0
  12. package/dist/bundled/command-code-knowledge/reference/plan-mode.md +101 -0
  13. package/dist/bundled/command-code-knowledge/reference/product-help.md +421 -0
  14. package/dist/bundled/command-code-knowledge/reference/skills.md +993 -0
  15. package/dist/bundled/mod-builder/SKILL.md +128 -0
  16. package/dist/bundled/mod-builder/examples/block-dangerous-commands.ts +41 -0
  17. package/dist/bundled/mod-builder/examples/custom-entry-renderer.ts +32 -0
  18. package/dist/bundled/mod-builder/examples/custom-tool.ts +53 -0
  19. package/dist/bundled/mod-builder/examples/flags-and-options.ts +29 -0
  20. package/dist/bundled/mod-builder/examples/input-shortcuts.ts +41 -0
  21. package/dist/bundled/mod-builder/examples/kitchen-sink.ts +110 -0
  22. package/dist/bundled/mod-builder/examples/lifecycle-hooks.ts +64 -0
  23. package/dist/bundled/mod-builder/examples/observe-events.ts +30 -0
  24. package/dist/bundled/mod-builder/examples/slash-command.ts +33 -0
  25. package/dist/bundled/mod-builder/reference/api.md +81 -0
  26. package/dist/bundled/mod-builder/reference/hooks-and-events.md +308 -0
  27. package/dist/bundled/mod-builder/reference/overview.md +161 -0
  28. package/dist/bundled/mod-builder/reference/packaging.md +63 -0
  29. package/dist/bundled/mod-builder/reference/ui.md +63 -0
  30. package/dist/bundled/mod-builder/reference/verify.md +58 -0
  31. package/dist/bundled/skill-builder/SKILL.md +104 -0
  32. package/dist/cli.mjs +5 -5
  33. package/package.json +35 -26
  34. package/vsix/commandcode-vscode.vsix +0 -0
  35. /package/{skills → dist/bundled}/agent-browser/SKILL.md +0 -0
  36. /package/{skills → dist/bundled}/design/SKILL.md +0 -0
  37. /package/{skills → dist/bundled}/design/references/border.md +0 -0
  38. /package/{skills → dist/bundled}/design/references/button.md +0 -0
  39. /package/{skills → dist/bundled}/design/references/checkup.md +0 -0
  40. /package/{skills → dist/bundled}/design/references/color.md +0 -0
  41. /package/{skills → dist/bundled}/design/references/create.md +0 -0
  42. /package/{skills → dist/bundled}/design/references/design-html.md +0 -0
  43. /package/{skills → dist/bundled}/design/references/deslop.md +0 -0
  44. /package/{skills → dist/bundled}/design/references/finish.md +0 -0
  45. /package/{skills → dist/bundled}/design/references/interaction.md +0 -0
  46. /package/{skills → dist/bundled}/design/references/layout.md +0 -0
  47. /package/{skills → dist/bundled}/design/references/motion.md +0 -0
  48. /package/{skills → dist/bundled}/design/references/redesign.md +0 -0
  49. /package/{skills → dist/bundled}/design/references/refine.md +0 -0
  50. /package/{skills → dist/bundled}/design/references/relayout.md +0 -0
  51. /package/{skills → dist/bundled}/design/references/report-html.md +0 -0
  52. /package/{skills → dist/bundled}/design/references/responsive.md +0 -0
  53. /package/{skills → dist/bundled}/design/references/review.md +0 -0
  54. /package/{skills → dist/bundled}/design/references/setup.md +0 -0
  55. /package/{skills → dist/bundled}/design/references/shadow.md +0 -0
  56. /package/{skills → dist/bundled}/design/references/smell.md +0 -0
  57. /package/{skills → dist/bundled}/design/references/surface.md +0 -0
  58. /package/{skills → dist/bundled}/design/references/tokenize.md +0 -0
  59. /package/{skills → dist/bundled}/design/references/typeset.md +0 -0
  60. /package/{skills → dist/bundled}/design/references/voice.md +0 -0
  61. /package/{skills → dist/bundled}/design/references/writing.md +0 -0
@@ -0,0 +1,1097 @@
1
+ <!-- GENERATED FILE — do not edit. Source: packages/docs/src/app/hooks/page.mdx. Regenerate: pnpm generate:knowledge -->
2
+
3
+ # Hooks
4
+
5
+ Hooks are shell scripts that Command Code runs automatically before and after each tool call. They give you deterministic control over Command Code's behavior. Use hooks to enforce project rules, audit tool use, and integrate Command Code with workflows you already have.
6
+
7
+ ---
8
+
9
+ ## What are hooks?
10
+
11
+ Hooks fire before a shell command runs, before a file read, and after a write. Each hook receives the tool's inputs as JSON on stdin and returns one of four actions: allow, deny, halt the session, or inject extra context for the next turn.
12
+
13
+ ---
14
+
15
+ ## What you can do with hooks?
16
+
17
+ With hooks configured, you can:
18
+
19
+ - **Block dangerous bash commands** before they execute, so `rm -rf /` never makes it past a policy guard.
20
+ - **Inject project-specific context** when the agent reads sensitive files, nudging the model to redact secrets.
21
+ - **Audit every write to the disk** by forwarding file paths to a local log.
22
+ - **Halt the session** if a tool's output leaks sensitive information.
23
+
24
+ ---
25
+
26
+ ## Supported events
27
+
28
+ | **Event** | **Fires** | **Can block?** |
29
+ | --------- | --------- | -------------- |
30
+ | `PreToolUse` | Before a tool runs | Yes (denies the tool) |
31
+ | `PostToolUse` | After the tool returns | No (advisory) |
32
+ | `Stop` | When the assistant finishes a turn | Yes (forces a revision, capped at 3 retries) |
33
+ | `SessionStart` | When a session begins (startup, resume, clear) | No (injects context only) |
34
+
35
+ Need to react to more of the session lifecycle, like sub-agent activity, custom tools, or other moments beyond these four events? That's what [mods](https://commandcode.ai/docs/mods) are for.
36
+
37
+ ---
38
+
39
+ ## Supported tools
40
+
41
+ | **Tool** | **Matcher literal** | **What it covers** |
42
+ | -------- | ------------------- | ------------------ |
43
+ | Shell | `shell` | Shell command execution |
44
+ | Read | `read` | File reads |
45
+ | Write | `write` | File creation or full overwrite |
46
+ | Edit | `edit` | In-place edits to an existing file |
47
+
48
+ For `matcher` syntax (regex, case-insensitivity, matching multiple tools), see [Settings schema](#settings-schema).
49
+
50
+ ---
51
+
52
+ ## How hooks work
53
+
54
+ A hook is any executable that reads a JSON payload on stdin and writes a JSON object on stdout. Write one in bash, Node.js, Python, or anything that can parse JSON.
55
+
56
+ 1. **Event fires**: Command Code is about to run (or has just run) a tool.
57
+ 2. **Hook triggered**: Command Code spawns your executable and writes the tool's details as JSON on its stdin.
58
+ 3. **Hook responds**: Your executable writes a JSON response to stdout to allow, deny, inject context, or halt the session.
59
+
60
+ For the stdin and stdout schema, exit codes, environment variables, and execution semantics, see the [reference sections](#settings-schema) below.
61
+
62
+ ---
63
+
64
+ ## Quickstart
65
+
66
+ This quickstart wires up your first hook in Command Code.
67
+
68
+ ### 1. Create the settings file
69
+
70
+ Create `.commandcode/settings.json` at the root of your project and paste in the snippet below. The `PreToolUse` hook triggers before every shell command and prints a message.
71
+
72
+ **`.commandcode/settings.json`**
73
+
74
+ ```json
75
+ {
76
+ "hooks": {
77
+ "PreToolUse": [
78
+ {
79
+ "matcher": "shell",
80
+ "hooks": [
81
+ { "type": "command", "command": "echo '{\"systemMessage\":\"hook fired\"}'" }
82
+ ]
83
+ }
84
+ ]
85
+ }
86
+ }
87
+ ```
88
+
89
+ The `command` field is what Command Code runs when the hook fires. Here it prints a JSON object with a `systemMessage` field, and Command Code surfaces that message when running shell commands.
90
+
91
+ ### 2. Try it out
92
+
93
+ Hooks initialize on startup. Restart Command Code with `cmd`, then run a prompt such as `Use the shell tool to list the files in the current directory`. The message `PreToolUse: hook fired` should appear before the shell command executes.
94
+
95
+ The quickstart hook emits a static message. Real hooks parse the JSON payload on stdin and return results, halt sessions, or inject context for the next turn — read on for configuration, the full schema, examples, and best practices.
96
+
97
+ ---
98
+
99
+ ## Configuration
100
+
101
+ Hooks are configured under the `hooks` key in a `settings.json` file. Command Code looks for settings.json in the following paths:
102
+
103
+ | **Scope** | **Config file** | **Applies to** | **Committed to git?** |
104
+ | --------- | --------------- | ----------------- | --------------------- |
105
+ | User | `~/.commandcode/settings.json` | Across all projects | No |
106
+ | Project | `.commandcode/settings.json` | Anyone using the project | Yes |
107
+
108
+ **Precedence:** project > user.
109
+ When the exact same command string appears in multiple scopes, the higher-priority source wins.
110
+
111
+ ### Ordering
112
+
113
+ Within the same event, hooks fire in the order they appear in `settings.json` (project first, then user).
114
+
115
+ For example, `PreToolUse` hooks run sequentially. As soon as one blocks the tool, the remaining `PreToolUse` hooks are skipped. `PostToolUse` hooks run in parallel because the tool has already finished.
116
+
117
+ You can wire multiple hooks under a single matcher. They run in listed order:
118
+
119
+ ```json
120
+ {
121
+ "hooks": {
122
+ "PreToolUse": [
123
+ {
124
+ "matcher": "shell",
125
+ "hooks": [
126
+ { "type": "command", "command": "./.commandcode/hooks/guard-bash.sh", "timeout": 5 },
127
+ { "type": "command", "command": "./.commandcode/hooks/log-shell.sh" }
128
+ ]
129
+ }
130
+ ]
131
+ }
132
+ }
133
+ ```
134
+
135
+ Here `guard-bash.sh` runs first. If it denies, `log-shell.sh` is skipped.
136
+
137
+ For a complete `settings.json` wiring multiple matchers across both events, see [Example settings.json](#example-settingsjson).
138
+
139
+ ---
140
+
141
+ ## Settings schema
142
+
143
+ 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.
144
+
145
+ ```text
146
+ settings.json
147
+ └── hooks
148
+ └── <EventName>
149
+ └── [ HookDefinition ] // matcher + list of handlers
150
+ └── hooks: [ HookEntry ] // type + command + timeout
151
+ ```
152
+
153
+ One `HookDefinition` can own multiple `HookEntry` handlers. They all run for the same matcher, in the order listed.
154
+
155
+ ### Hook Definition fields (outer)
156
+
157
+ Chooses which tools this group of handlers applies to.
158
+
159
+ | **Field** | **Required** | **Type** | **Description** |
160
+ | --------- | ------------ | -------- | --------------- |
161
+ | `matcher` | Optional | `string` | Omit to match every tool. Examples: `"shell"`, `"write\|edit"`. Only meaningful for the tool events (`PreToolUse`, `PostToolUse`). `Stop` and `SessionStart` carry no tool, so a `matcher` there never matches and the hook **will not fire** — omit it for those events. |
162
+ | `hooks` | Required | `array` | One or more handlers. Runs in the order listed |
163
+
164
+ ### HookEntry fields (inner)
165
+
166
+ Describes a single handler to execute.
167
+
168
+ | **Field** | **Required** | **Type** | **Description** |
169
+ | --------- | ------------ | -------- | --------------- |
170
+ | `type` | Required | `string` | Handler kind. Supports `command` adapter only |
171
+ | `command` | Required when `type: "command"` | `string` | Shell command to execute |
172
+ | `timeout` | Optional | seconds | Defaults to `30`, maximum `600` |
173
+
174
+ ### Example settings.json
175
+
176
+ 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.
177
+
178
+ ```json
179
+ {
180
+ "hooks": {
181
+ "PreToolUse": [
182
+ {
183
+ "matcher": "shell|write",
184
+ "hooks": [
185
+ {
186
+ "type": "command",
187
+ "command": "./.commandcode/hooks/guard-tools.sh",
188
+ "timeout": 10
189
+ }
190
+ ]
191
+ }
192
+ ],
193
+ "PostToolUse": [
194
+ {
195
+ "hooks": [
196
+ {
197
+ "type": "command",
198
+ "command": "./.commandcode/hooks/audit.sh"
199
+ }
200
+ ]
201
+ }
202
+ ]
203
+ }
204
+ }
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Hook input (stdin)
210
+
211
+ 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.
212
+
213
+ ### Common fields
214
+
215
+ Present on all events.
216
+
217
+ | **Field** | **Type** | **Description** |
218
+ | --------- | -------- | --------------- |
219
+ | `session_id` | `string` | Session identifier, stable for the lifetime of one CLI session |
220
+ | `transcript_path` | `string` | Absolute path to this session's transcript JSONL |
221
+ | `cwd` | `string` | Absolute working directory at fire time |
222
+ | `hook_event_name` | `string` | The event that fired the hook. |
223
+ | `permission_mode` | `"default" \| "auto-accept" \| "plan" \| "bypass" \| "dont-ask" \| ""` | Current [permission mode](#permission-modes). Empty when no mode applies. |
224
+
225
+ ### Tool-call fields
226
+
227
+ Present on every event tied to a tool call.
228
+
229
+ | **Field** | **Type** | **Description** |
230
+ | --------- | -------- | --------------- |
231
+ | `tool_use_id` | `string?` | Stable tool invocation id. Present on every real tool call |
232
+ | `tool_name` | `string` | Canonical tool id (`shell_command`, `read_file`, `write_file`, `edit_file`) |
233
+ | `tool_display_name` | `string` | One of `SHELL`, `READ`, `WRITE`, `EDIT`. The value `matcher` is tested against |
234
+ | `tool_input` | `object` | Tool arguments as emitted by the model. Shape depends on the tool, see below |
235
+
236
+ #### `tool_input` fields
237
+
238
+ 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`.
239
+
240
+ | **Tool** | **fields** |
241
+ | -------- | ----------------------- |
242
+ | `shell_command` | `command: string`, `args?: string[]`, `directory?: string`, `timeout?: number` |
243
+ | `read_file` | `absolute_path: string`, `offset?: number`, `limit?: number` |
244
+ | `write_file` | `file_path: string`, `content: string` |
245
+ | `edit_file` | `file_path: string`, `old_value: string`, `new_value: string`, `replacement_count?: number`, `replace_all?: boolean` |
246
+
247
+ ### Event-specific fields
248
+
249
+ 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.
250
+
251
+ #### PostToolUse
252
+
253
+ | **Field** | **Type** | **Description** |
254
+ | --------- | -------- | --------------- |
255
+ | `tool_response` | `string` | Output of the tool, the same text the model will see |
256
+
257
+ #### Stop
258
+
259
+ `Stop` fires when the assistant produces its final response with no remaining tool calls (end of turn). It carries no tool fields — omit `matcher` on `Stop` hooks (a `matcher` here prevents the hook from firing). It provides only the common fields plus:
260
+
261
+ | **Field** | **Type** | **Description** |
262
+ | --------- | -------- | --------------- |
263
+ | `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 |
264
+
265
+ #### SessionStart
266
+
267
+ `SessionStart` fires once when a session begins. It carries no tool fields — omit `matcher` on `SessionStart` hooks (a `matcher` here prevents the hook from firing). It provides only the common fields plus:
268
+
269
+ | **Field** | **Type** | **Description** |
270
+ | --------- | -------- | --------------- |
271
+ | `source` | `"startup" \| "resume" \| "clear"` | Why the session started: a fresh launch, an existing session reopened, or the conversation cleared |
272
+
273
+ ---
274
+
275
+ ## Environment variables
276
+
277
+ Command Code injects four environment variables into every hook process.
278
+
279
+ | **Variable** | **Value** |
280
+ | ------------ | --------- |
281
+ | `COMMANDCODE_PROJECT_DIR` | Absolute path to the project (same as `cwd`) |
282
+ | `COMMANDCODE_SESSION_ID` | Session ID, useful for correlating hooks to a run |
283
+ | `COMMANDCODE_HOOK_EVENT` | `PreToolUse`, `PostToolUse`, `Stop`, or `SessionStart` |
284
+ | `COMMANDCODE_CWD` | Alias of `COMMANDCODE_PROJECT_DIR` with the identical value |
285
+
286
+ Your environment variables are forwarded to hook processes with any sensitive variable being stripped out.
287
+
288
+ ---
289
+
290
+ ## Hook output (stdout)
291
+
292
+ The hook's executable writes a single JSON object to stdout. All fields are optional. Empty stdout on exit `0` means "no opinion, allow".
293
+
294
+ ### Common output fields
295
+
296
+ | **Field** | **Type** | **Description** |
297
+ | --------- | -------- | --------------- |
298
+ | `continue` | `boolean` | `false` halts the session after the current tool batch. Pair with `stopReason` |
299
+ | `stopReason` | `string` | User-facing message shown in the TUI when `continue: false`. Not sent to the model |
300
+ | `suppressOutput` | `boolean` | When `true`, omit the hook's parsed output from the audit log |
301
+ | `systemMessage` | `string` | Free-text notice surfaced in the TUI feed. Not sent to the model |
302
+
303
+ ### PreToolUseOutput fields
304
+
305
+ Adds a `hookSpecificOutput` object on top of the common fields.
306
+
307
+ | **Field** | **Type** | **Description** |
308
+ | --------- | -------- | --------------- |
309
+ | `hookSpecificOutput.hookEventName` | `"PreToolUse"` | Optional. Helps user distinguish the PreToolUse shape; the engine already knows which event fired |
310
+ | `hookSpecificOutput.permissionDecision` | `"allow" \| "deny"` | `"deny"` blocks the tool. Omit or `"allow"` to permit |
311
+ | `hookSpecificOutput.permissionDecisionReason` | `string` | Shown to the model when denying. Use this to teach the model not to retry |
312
+ | `hookSpecificOutput.additionalContext` | `string` | Appended to the tool result before the model's next turn |
313
+
314
+ Full shape:
315
+
316
+ ```json
317
+ {
318
+ "continue": true,
319
+ "suppressOutput": false,
320
+ "stopReason": "",
321
+ "systemMessage": "",
322
+ "hookSpecificOutput": {
323
+ "hookEventName": "PreToolUse",
324
+ "permissionDecision": "allow",
325
+ "permissionDecisionReason": "",
326
+ "additionalContext": ""
327
+ }
328
+ }
329
+ ```
330
+
331
+ ### PostToolUseOutput fields
332
+
333
+ Adds top-level `decision` / `reason` and a smaller `hookSpecificOutput`.
334
+
335
+ | **Field** | **Type** | **Description** |
336
+ | --------- | -------- | --------------- |
337
+ | `decision` | `"block"` | Advisory retry signal to the model. The tool already ran, so nothing is un-done |
338
+ | `reason` | `string` | Pairs with `decision: "block"` |
339
+ | `hookSpecificOutput.hookEventName` | `"PostToolUse"` | Optional. Helps readers distinguish the PostToolUse shape; the engine already knows which event fired |
340
+ | `hookSpecificOutput.additionalContext` | `string` | Appended to the tool result before the model's next turn |
341
+
342
+ Full shape:
343
+
344
+ ```json
345
+ {
346
+ "continue": true,
347
+ "suppressOutput": false,
348
+ "stopReason": "",
349
+ "systemMessage": "",
350
+ "decision": "block",
351
+ "reason": "",
352
+ "hookSpecificOutput": {
353
+ "hookEventName": "PostToolUse",
354
+ "additionalContext": ""
355
+ }
356
+ }
357
+ ```
358
+
359
+ ### StopOutput fields
360
+
361
+ `Stop` uses only top-level fields, with no `hookSpecificOutput`.
362
+
363
+ | **Field** | **Type** | **Description** |
364
+ | --------- | -------- | --------------- |
365
+ | `decision` | `"block"` | Prevents the assistant from finishing. The agent loop runs one more iteration. Capped at 3 retries per turn |
366
+ | `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 |
367
+
368
+ Full shape:
369
+
370
+ ```json
371
+ {
372
+ "continue": true,
373
+ "suppressOutput": false,
374
+ "stopReason": "",
375
+ "systemMessage": "",
376
+ "decision": "block",
377
+ "reason": ""
378
+ }
379
+ ```
380
+
381
+ ### SessionStartOutput fields
382
+
383
+ `SessionStart` is non-blocking: it has no top-level `decision` / `reason` and never halts, retries, or denies. It carries the universal fields plus a `hookSpecificOutput` block for injecting context.
384
+
385
+ | **Field** | **Type** | **Description** |
386
+ | --------- | -------- | --------------- |
387
+ | `hookSpecificOutput.hookEventName` | `"SessionStart"` | Optional. Helps readers distinguish the SessionStart shape; the engine already knows which event fired |
388
+ | `hookSpecificOutput.additionalContext` | `string` | Injected into the session's first user turn, so the model sees it before it starts work |
389
+
390
+ When several SessionStart hooks return `additionalContext`, the values are joined and injected together.
391
+
392
+ Full shape:
393
+
394
+ ```json
395
+ {
396
+ "suppressOutput": false,
397
+ "systemMessage": "",
398
+ "hookSpecificOutput": {
399
+ "hookEventName": "SessionStart",
400
+ "additionalContext": ""
401
+ }
402
+ }
403
+ ```
404
+
405
+ #### Loop prevention
406
+
407
+ A naive Stop hook that always returns `decision: "block"` would loop forever. Two safety layers:
408
+
409
+ 1. **`stop_hook_active`** on input is `true` on the retry fire. Hook authors should `exit 0` when they see it.
410
+ 2. **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.
411
+
412
+ ```bash
413
+ # canonical Stop hook skeleton
414
+ INPUT=$(cat)
415
+ if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
416
+ exit 0 # Already retrying, let the turn end
417
+ fi
418
+ # ... your check here ...
419
+ ```
420
+
421
+ #### Feeding text to the model on retry
422
+
423
+ 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:
424
+
425
+ - **`decision: "block"` + `reason`**: `reason` is 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.
426
+ - **`exit 2` + stderr**: the full stderr (capped, so multi-line `tsc`/lint diagnostics come through intact) is fed raw, so the model treats it like tool output and can act on it directly:
427
+
428
+ ```bash
429
+ echo "tsc: 3 errors found" >&2
430
+ exit 2
431
+ ```
432
+
433
+ Use `reason` for natural-language revision guidance; use `exit 2` + stderr for machine-style diagnostics the model should act on verbatim.
434
+
435
+ ### Who sees each field
436
+
437
+ Use this to pick the right field for the audience you want to reach.
438
+
439
+ | **Field** | **User (TUI)** | **Model** |
440
+ | --------- | :------------: | :-------: |
441
+ | `stopReason` | ✓ | — |
442
+ | `systemMessage` | ✓ | — |
443
+ | `permissionDecisionReason` (Pre) | ✓ | ✓ (when denying) |
444
+ | `reason` (Post / Stop) | ✓ | ✓ (Post & Stop when `decision: "block"`; an `exit 2` retry feeds stderr instead) |
445
+ | `additionalContext` | — | ✓ (appended before next turn) |
446
+ | `stderr` (exit 2) | ✓ first line | ✓ (full text fed to model on retry, all events) |
447
+
448
+ 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`.
449
+
450
+ ---
451
+
452
+ ## Exit codes
453
+
454
+ The exit code is the fast path. Most hooks only ever use `0`.
455
+
456
+ | **Exit** | **Stdout handling** | **Effect on tool** | **Effect on session** |
457
+ | -------- | ------------------- | ------------------ | --------------------- |
458
+ | `0` | Parsed as JSON | Determined by output (see [decision matrix](#block-and-halt-decision-matrix)) | Continues (unless `continue: false`) |
459
+ | `2` | Ignored | `PreToolUse`: blocked. `PostToolUse`: advisory retry signal. `Stop` / `SessionStart`: n/a | `PreToolUse` / `PostToolUse`: continues. `Stop`: retries the turn. `SessionStart`: continues (non-blocking) |
460
+ | any other | Parsed if present | Tool proceeds, non-blocking error logged | Continues |
461
+
462
+ ### Exit code `2` block-reason resolution
463
+
464
+ The text sent to the model when a hook exits `2` is resolved in this order:
465
+
466
+ 1. `hookSpecificOutput.permissionDecisionReason` (if stdout parses and sets one)
467
+ 2. Top-level `reason` (PostToolUse only)
468
+ 3. Trimmed first line of stderr
469
+ 4. Generic fallback text naming the hook and its exit code
470
+
471
+ ### Exit code `0` stdout handling
472
+
473
+ - Empty or whitespace-only stdout means "no opinion" and the tool proceeds.
474
+ - Non-empty stdout that fails to parse as JSON, or parses but fails schema validation, is logged as a warning and the tool proceeds.
475
+
476
+ ---
477
+
478
+ ## Block and halt decision matrix
479
+
480
+ Each hook result produces two effects: whether the tool runs, and whether the session continues afterward.
481
+
482
+ | **Signal** | **Value** | **Tool** | **Session** |
483
+ | ---------- | --------- | :------: | :---------: |
484
+ | `exit code` | `2` (PreToolUse) | skipped | continues |
485
+ | `exit code` | `2` (PostToolUse) | (already ran) advisory retry | continues |
486
+ | `exit code` | `2` (Stop) | n/a | retries the turn; stderr is fed to the model |
487
+ | `exit code` | `0` | see output | see output |
488
+ | `exit code` | other | runs | continues |
489
+ | `hookSpecificOutput.permissionDecision` | `"deny"` (PreToolUse) | skipped | continues |
490
+ | `decision` | `"block"` (PostToolUse) | (already ran) advisory retry | continues |
491
+ | `decision` | `"block"` (Stop) | n/a | retries the turn (capped at 3); `reason` shown to the user and fed to the model |
492
+ | `continue` | `false` | runs | halts after this batch |
493
+
494
+ **SessionStart is non-blocking**: it never blocks, retries, or halts. Exit `2`, `decision`, and `continue: false` are all ignored as control signals so a hook can never stop a session from opening. Only `additionalContext` (injected into the first turn) and `systemMessage` (shown to the user) are consumed.
495
+
496
+ **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.
497
+
498
+ **When any hook sets `continue: false`**: every hook in the current batch still runs to completion. The session halts after. This does not apply to `SessionStart`.
499
+
500
+ ---
501
+
502
+ ## Execution semantics
503
+
504
+ How the engine runs hooks once a tool call fires.
505
+
506
+ ### Shell
507
+
508
+ 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`.
509
+
510
+ ### Execution order
511
+
512
+ - **`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.
513
+ - **`PostToolUse`** hooks run **in parallel**. One crashing hook cannot cancel another. Returned results preserve the order they appear in `settings.json`, not completion order.
514
+ - **`Stop`** hooks run **in parallel**, like `PostToolUse`. Matchers on Stop hooks are silently ignored, since Stop has no tool to match against.
515
+ - **`SessionStart`** hooks run **in parallel** when a session begins, before the first turn. Matchers are silently ignored, since SessionStart has no tool to match against.
516
+
517
+ ### Timeouts
518
+
519
+ - Default timeout is 30 seconds. Override per hook with `timeout` (seconds, capped at 600).
520
+ - On timeout the engine sends `SIGTERM`. Hooks that trap `SIGTERM` get a 5-second grace period before `SIGKILL`.
521
+
522
+ ### Isolation
523
+
524
+ 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](#block-and-halt-decision-matrix); no hook sees any other hook's result.
525
+
526
+ ---
527
+
528
+ ## Permission modes
529
+
530
+ The `permission_mode` field on stdin carries the session's current mode:
531
+
532
+ | **Value** | **Description** |
533
+ | --------- | --------------- |
534
+ | `"default"` | Model requests permission before each tool |
535
+ | `"auto-accept"` | Permission prompts auto-accepted |
536
+ | `"plan"` | Plan mode. Tool calls are restricted to read-only operations. **Hooks are skipped entirely in plan mode** |
537
+ | `"bypass"` | Prompts are skipped; ordinary safety checks still apply |
538
+ | `"dont-ask"` | Never prompts; anything that would need a prompt is denied instead |
539
+ | `""` (empty) | No mode applies to this fire |
540
+
541
+ `standard` is accepted as a legacy alias when *setting* the mode (in settings.json or `--permission-mode`), but the value reported on the wire is always `default`.
542
+
543
+ 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.
544
+
545
+ ---
546
+
547
+ ## Example: full hook script
548
+
549
+ **`.commandcode/hooks/guard-shell.sh`**
550
+
551
+ ```bash
552
+ #!/usr/bin/env bash
553
+ set -euo pipefail
554
+
555
+ # Read the entire stdin payload once.
556
+ payload=$(cat)
557
+
558
+ # Common fields on every event.
559
+ session_id=$(printf '%s' "$payload" | jq -r '.session_id')
560
+ cwd=$(printf '%s' "$payload" | jq -r '.cwd')
561
+ event=$(printf '%s' "$payload" | jq -r '.hook_event_name')
562
+
563
+ # Tool-call fields, present on every tool event.
564
+ tool_name=$(printf '%s' "$payload" | jq -r '.tool_name')
565
+ tool_display=$(printf '%s' "$payload" | jq -r '.tool_display_name')
566
+ cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // ""')
567
+
568
+ # Command Code also injects env vars for the same context.
569
+ : "${COMMANDCODE_PROJECT_DIR:?}"
570
+ : "${COMMANDCODE_SESSION_ID:?}"
571
+ : "${COMMANDCODE_HOOK_EVENT:?}"
572
+
573
+ # Deny a destructive command with a reason the model will see.
574
+ if [[ "$cmd" == *"rm -rf /"* ]]; then
575
+ jq -n --arg cmd "$cmd" '{
576
+ continue: true,
577
+ systemMessage: "Blocked destructive command",
578
+ hookSpecificOutput: {
579
+ hookEventName: "PreToolUse",
580
+ permissionDecision: "deny",
581
+ permissionDecisionReason: ("Policy forbids: " + $cmd)
582
+ }
583
+ }'
584
+ exit 0
585
+ fi
586
+
587
+ # Otherwise allow and attach extra context for the model's next turn.
588
+ jq -n --arg event "$event" '{
589
+ continue: true,
590
+ hookSpecificOutput: {
591
+ hookEventName: "PreToolUse",
592
+ permissionDecision: "allow",
593
+ additionalContext: ("Verified by guard-shell.sh during " + $event)
594
+ }
595
+ }'
596
+ ```
597
+
598
+ ---
599
+
600
+ ## Examples
601
+
602
+ A set of working hooks you can copy into your project. Each shows a different use case, so you can modify them as you see fit. All are bash scripts here, but a hook can be any executable that parses JSON on stdin.
603
+
604
+ | **Example** | **Event** | **Kind** | **What you get** |
605
+ | ----------- | --------- | --------- | ---------------- |
606
+ | [Block dangerous bash](#block-dangerous-bash-commands) | `PreToolUse` | Blocking | Model can't run `rm -rf /`, `curl \| sh` |
607
+ | [Warn on sensitive reads](#warn-the-agent-about-sensitive-files-on-read) | `PreToolUse` | Context injection | Model is told to redact secrets before quoting |
608
+ | [Audit tool calls](#audit-tool-calls-to-a-log-file) | `PreToolUse` or `PostToolUse` | Audit only | Matching tool calls appended to a local log file |
609
+ | [Quality gate](#quality-gate-stop-hook) | `Stop` | Blocking (retry) | Assistant can't finish with `DO NOT SHIP` markers left in the code |
610
+ | [Inject git status](#inject-git-status-on-session-start) | `SessionStart` | Context injection | Model knows the branch and uncommitted files before it starts |
611
+
612
+ Every example has two files:
613
+ 1. A `settings.json` that wires the hook.
614
+ 2. A shell script at `.commandcode/hooks/<name>.sh`.
615
+
616
+ Ensure each script is executable with `chmod +x` before Command Code fires the hook.
617
+
618
+ Each example ends with a **Test** block containing a prompt to paste into Command Code and the result to expect so you can confirm the hook fires.
619
+
620
+ ### Block dangerous bash commands
621
+
622
+ Match the model's shell command against a short list of dangerous patterns. On a hit, deny the tool and tell the model why, so it doesn't retry the same pattern.
623
+
624
+ **`.commandcode/settings.json`**
625
+
626
+ ```json
627
+ {
628
+ "hooks": {
629
+ "PreToolUse": [
630
+ {
631
+ "matcher": "shell",
632
+ "hooks": [
633
+ {
634
+ "type": "command",
635
+ "command": "./.commandcode/hooks/deny-dangerous.sh", "timeout": 5
636
+ }
637
+ ]
638
+ }
639
+ ]
640
+ }
641
+ }
642
+ ```
643
+
644
+ **`.commandcode/hooks/deny-dangerous.sh`**
645
+
646
+ ```bash
647
+ #!/usr/bin/env bash
648
+ set -euo pipefail
649
+ cmd=$(jq -r '.tool_input.command // ""')
650
+
651
+ # Dangerous patterns: rm -rf /, curl | sh, fork bomb, sudo rm
652
+ pattern='rm[[:space:]]+-[rR]f?[[:space:]]+/'
653
+ pattern+='|curl.*\|.*(sh|bash)'
654
+ pattern+='|:\(\)[[:space:]]*\{'
655
+ pattern+='|sudo[[:space:]]+rm'
656
+
657
+ if printf '%s' "$cmd" | grep -qE "$pattern"; then
658
+ jq -n --arg cmd "$cmd" '{
659
+ systemMessage: "blocked dangerous command",
660
+ hookSpecificOutput: {
661
+ hookEventName: "PreToolUse",
662
+ permissionDecision: "deny",
663
+ permissionDecisionReason: ("Command matched a dangerous pattern. Policy forbids: " + ($cmd | .[0:120]))
664
+ }
665
+ }'
666
+ else
667
+ exit 0
668
+ fi
669
+ ```
670
+
671
+ **Test: Block dangerous bash commands**
672
+
673
+ Prompt in Command Code:
674
+
675
+ ```text copy filename="Prompt"
676
+ Try to run this shell command: rm -rf /tmp/cmd-hook-demo
677
+ ```
678
+
679
+ Expected result:
680
+
681
+ - ❌ The command is blocked before it runs (it matches the `rm -rf /` pattern, even though the target is harmless).
682
+ - The agent sees *"Command matched a dangerous pattern. Policy forbids…"* and moves on without retrying.
683
+
684
+ **How it works**
685
+
686
+ - Reads `tool_input.command` from stdin with `jq`.
687
+ - Matches against four patterns: `rm -rf /`, `curl | sh`, a `:(){}` fork bomb, `sudo rm`.
688
+ - On a hit, emits `permissionDecision: "deny"`. The tool is skipped and the model receives `permissionDecisionReason` as the tool result.
689
+ - On a miss, exits `0` with no stdout. The tool runs normally.
690
+
691
+ ### Warn the agent about sensitive files on read
692
+
693
+ Allow every read, but quietly inject a note when the path looks sensitive. The model sees the note as extra context and adjusts its response.
694
+
695
+ **`.commandcode/settings.json`**
696
+
697
+ ```json
698
+ {
699
+ "hooks": {
700
+ "PreToolUse": [
701
+ {
702
+ "matcher": "read",
703
+ "hooks": [
704
+ {
705
+ "type": "command",
706
+ "command": "./.commandcode/hooks/warn-sensitive-reads.sh"
707
+ }
708
+ ]
709
+ }
710
+ ]
711
+ }
712
+ }
713
+ ```
714
+
715
+ **`.commandcode/hooks/warn-sensitive-reads.sh`**
716
+
717
+ ```bash
718
+ #!/usr/bin/env bash
719
+ set -euo pipefail
720
+ path=$(jq -r '.tool_input.absolute_path // ""')
721
+
722
+ # Default note. Upgraded to a strong warning when the path looks sensitive.
723
+ ctx="File is being read under an audit hook. Do not paste full contents unless the user asks."
724
+ if printf '%s' "$path" | grep -qE '(\.ssh/|/\.env$|\.pem$|id_rsa)'; then
725
+ ctx="SENSITIVE READ: $path. Redact any keys or tokens before quoting from this file."
726
+ fi
727
+
728
+ jq -n --arg ctx "$ctx" '{
729
+ hookSpecificOutput: {
730
+ hookEventName: "PreToolUse",
731
+ permissionDecision: "allow",
732
+ additionalContext: $ctx
733
+ }
734
+ }'
735
+ ```
736
+
737
+ **Test: Warn on sensitive reads**
738
+
739
+ Prompt in Command Code:
740
+
741
+ ```text copy filename="Prompt"
742
+ Read the .env file in my home directory
743
+ ```
744
+
745
+ Expected result:
746
+
747
+ - ✅ The read is allowed. The hook never blocks it.
748
+ - The agent receives the injected note *"SENSITIVE READ: …/.env. Redact any keys or tokens before quoting from this file."* and refuses to paste the full contents.
749
+
750
+ **How it works**
751
+
752
+ - Always returns `permissionDecision: "allow"`. The Read is never blocked.
753
+ - Sends `additionalContext` to the model, appended to the tool result before the next turn.
754
+ - Upgrades the note to an explicit redaction warning when the path matches `.ssh/`, `.env`, `.pem`, or `id_rsa`.
755
+
756
+ ### Audit tool calls to a log file
757
+
758
+ Log every matching tool call to an append-only file. The hook writes nothing to stdout, so it's observe-only: the tool runs unchanged. The pattern (read `tool_input` with `jq`, append a tab-separated line) is the same for any tool. Swap the event, matcher, and extracted field to fit what you want to observe.
759
+
760
+ Use `PostToolUse` to log what completed. Use `PreToolUse` to log what was attempted; this also catches commands that a later hook denies, as long as the audit hook runs first.
761
+
762
+ #### Writes and edits (`PostToolUse`)
763
+
764
+ Fires after the file mutation completes. Uses `COMMANDCODE_PROJECT_DIR` and `COMMANDCODE_SESSION_ID` directly, so there's no need to parse `cwd` or `session_id` from stdin.
765
+
766
+ **`.commandcode/settings.json`**
767
+
768
+ ```json
769
+ {
770
+ "hooks": {
771
+ "PostToolUse": [
772
+ {
773
+ "matcher": "write|edit",
774
+ "hooks": [
775
+ {
776
+ "type": "command",
777
+ "command": "./.commandcode/hooks/audit-writes.sh"
778
+ }
779
+ ]
780
+ }
781
+ ]
782
+ }
783
+ }
784
+ ```
785
+
786
+ **`.commandcode/hooks/audit-writes.sh`**
787
+
788
+ ```bash
789
+ #!/usr/bin/env bash
790
+ set -euo pipefail
791
+
792
+ LOG_FILE="$COMMANDCODE_PROJECT_DIR/.commandcode/write-audit.log"
793
+
794
+ # Extract the target file path from the JSON payload on stdin.
795
+ path=$(jq -r '.tool_input.file_path // "?"')
796
+ timestamp=$(date -u +%FT%TZ)
797
+
798
+ # Append one tab-separated line per write: timestamp, session ID, file path.
799
+ printf '%s\t%s\t%s\n' "$timestamp" "$COMMANDCODE_SESSION_ID" "$path" >> "$LOG_FILE"
800
+ ```
801
+
802
+ **Test: Audit writes**
803
+
804
+ Prompt in Command Code:
805
+
806
+ ```text copy filename="Prompt"
807
+ Create a new file called config.txt with some test data
808
+ ```
809
+
810
+ Expected result:
811
+
812
+ - ✅ The file is created.
813
+ - The hook appends one line to `.commandcode/write-audit.log`.
814
+ - Verify with `cat .commandcode/write-audit.log`. You should see a tab-separated line (timestamp, session ID, file path).
815
+
816
+ #### Shell commands (`PreToolUse`)
817
+
818
+ Fires before execution, so this logs every shell command the agent issues.
819
+
820
+ **`.commandcode/settings.json`**
821
+
822
+ ```json
823
+ {
824
+ "hooks": {
825
+ "PreToolUse": [
826
+ {
827
+ "matcher": "shell",
828
+ "hooks": [
829
+ {
830
+ "type": "command",
831
+ "command": "./.commandcode/hooks/log-shell.sh"
832
+ }
833
+ ]
834
+ }
835
+ ]
836
+ }
837
+ }
838
+ ```
839
+
840
+ **`.commandcode/hooks/log-shell.sh`**
841
+
842
+ ```bash
843
+ #!/usr/bin/env bash
844
+ set -euo pipefail
845
+
846
+ LOG_FILE="/tmp/cmd-shell.log"
847
+
848
+ # Extract the shell command from the JSON payload on stdin.
849
+ command=$(jq -r '.tool_input.command // ""')
850
+ timestamp=$(date -u +%FT%TZ)
851
+
852
+ # Append one tab-separated line to the audit log.
853
+ printf '%s\t%s\n' "$timestamp" "$command" >> "$LOG_FILE"
854
+ ```
855
+
856
+ **Test: Audit shell commands**
857
+
858
+ Prompt in Command Code:
859
+
860
+ ```text copy filename="Prompt"
861
+ Run this command: ls -la /home
862
+ Then tell me you're done.
863
+ ```
864
+
865
+ Expected result:
866
+
867
+ - ✅ The command runs as normal.
868
+ - The hook appends one line to `/tmp/cmd-shell.log`.
869
+ - Verify with `cat /tmp/cmd-shell.log`. Each entry in `/tmp/cmd-shell.log` has the shape (fields are tab-separated):
870
+
871
+ ```
872
+ 2026-04-21T17:38:51Z ls -la /home
873
+ ```
874
+
875
+ ### Quality gate (Stop hook)
876
+
877
+ Block the agent from finishing the turn while a `DO NOT SHIP` marker is still in the code. The `Stop` event fires at end of turn; this hook greps for the marker and exits `2` when it finds one, sending the assistant back for another pass with the offending lines as feedback. The `stop_hook_active` check is the canonical loop-prevention pattern. Without it, the hook would fire forever.
878
+
879
+ **`.commandcode/settings.json`**
880
+
881
+ ```json
882
+ {
883
+ "hooks": {
884
+ "Stop": [
885
+ {
886
+ "hooks": [
887
+ {
888
+ "type": "command",
889
+ "command": "./.commandcode/hooks/no-ship-gate.sh"
890
+ }
891
+ ]
892
+ }
893
+ ]
894
+ }
895
+ }
896
+ ```
897
+
898
+ **`.commandcode/hooks/no-ship-gate.sh`**
899
+
900
+ ```bash
901
+ #!/usr/bin/env bash
902
+ set -euo pipefail
903
+
904
+ INPUT=$(cat)
905
+
906
+ # Already retrying, so let the turn end and don't loop.
907
+ if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
908
+ exit 0
909
+ fi
910
+
911
+ # Quality gate: don't finish while "DO NOT SHIP" markers remain.
912
+ # On a hit, echo the lines to stderr and exit 2. The engine feeds
913
+ # that text to the model on the retry so it knows what to remove.
914
+ # `.commandcode` is excluded so the hook can't match its own source.
915
+ HITS=$(grep -rn "DO NOT SHIP" . \
916
+ --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.commandcode \
917
+ 2>/dev/null || true)
918
+ if [ -n "$HITS" ]; then
919
+ echo "Remove these 'DO NOT SHIP' markers before finishing:" >&2
920
+ echo "$HITS" | head -20 >&2
921
+ exit 2
922
+ fi
923
+ ```
924
+
925
+ **Test: Quality gate**
926
+
927
+ Prompt in Command Code:
928
+
929
+ ```text copy filename="Prompt"
930
+ Write a JavaScript function that processes user data.
931
+ Include this comment: // DO NOT SHIP - add error handling
932
+ Then tell me you're done.
933
+ ```
934
+
935
+ Expected result: the turn is blocked, the agent retries on its own, and it finishes once the marker is gone. Step by step:
936
+
937
+ - ❌ **First turn:** the assistant finishes → the hook greps → finds the `DO NOT SHIP` marker → exits `2` with the matching lines on stderr. A gray Stop frame appears in the feed:
938
+
939
+ ```text
940
+ Ran 1 stop hook
941
+ └ Stop hook [./.commandcode/hooks/no-ship-gate.sh] exited 2: Remove these 'DO NOT SHIP' markers before finishing: [retry 1/3]
942
+ ```
943
+
944
+ - The engine feeds that stderr text back to the model as a user-role message and re-runs the turn.
945
+ - ✅ **Second turn:** the assistant removes the marker and adds error handling. The hook fires again with `stop_hook_active: true` → exits `0`. The turn ends.
946
+
947
+ If the hook keeps blocking (e.g. it's broken), the engine caps retries at 3 and ends the turn with `Stop hook retry cap reached (3). Ending turn. Same outcome ×4. Fix or disable: ./.commandcode/hooks/no-ship-gate.sh`.
948
+
949
+ ### Inject git status on session start
950
+
951
+ Give the model a head start by injecting the current git branch and any uncommitted changes explicitly into the first turn. The `SessionStart` event fires once when a session begins (startup, resume, or clear). This hook prints the repo state as `additionalContext`, so the model sees it before you even type a prompt. SessionStart is non-blocking, so a slow or failing hook never holds up the session.
952
+
953
+ **`.commandcode/settings.json`**
954
+
955
+ ```json
956
+ {
957
+ "hooks": {
958
+ "SessionStart": [
959
+ {
960
+ "hooks": [
961
+ {
962
+ "type": "command",
963
+ "command": "./.commandcode/hooks/git-status.sh"
964
+ }
965
+ ]
966
+ }
967
+ ]
968
+ }
969
+ }
970
+ ```
971
+
972
+ **`.commandcode/hooks/git-status.sh`**
973
+
974
+ ```bash
975
+ #!/usr/bin/env bash
976
+ set -euo pipefail
977
+
978
+ # Build a short summary of the repo state.
979
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
980
+ CHANGES=$(git status --porcelain 2>/dev/null | head -20)
981
+
982
+ SUMMARY="Current git branch: ${BRANCH}."
983
+ if [ -n "$CHANGES" ]; then
984
+ SUMMARY="${SUMMARY}"$'\n'"Uncommitted changes:"$'\n'"${CHANGES}"
985
+ else
986
+ SUMMARY="${SUMMARY} Working tree is clean."
987
+ fi
988
+
989
+ # Emit additionalContext for the model's first turn.
990
+ jq -n --arg ctx "$SUMMARY" '{
991
+ hookSpecificOutput: {
992
+ hookEventName: "SessionStart",
993
+ additionalContext: $ctx
994
+ }
995
+ }'
996
+ ```
997
+
998
+ **Test: Inject git status**
999
+
1000
+ Make a change so the working tree has uncommitted changes, then start a fresh Command Code session with the prompt:
1001
+
1002
+ ```text copy filename="Prompt"
1003
+ What branch am I on and what have I changed so far?
1004
+ ```
1005
+
1006
+ Expected result: the model answers with the branch name and the list of uncommitted files, even though you never told it. The context rode the first turn from the hook. A gray SessionStart frame also appears in the feed if the hook emits a `systemMessage`.
1007
+
1008
+ ---
1009
+
1010
+ ## Best practices
1011
+
1012
+ How to pick hooks over other mechanisms, write ones you can trust, test them without running Command Code, and debug the ones that misfire.
1013
+
1014
+ ### When to use hooks
1015
+
1016
+ Hooks are the right tool when you need **deterministic, out-of-model enforcement** in your workflows. When to use hooks vs. other Command Code features:
1017
+
1018
+ | **Mechanism** | **When to use** |
1019
+ | ------------- | --------------- |
1020
+ | **Hooks** | Block destructive actions, audit every tool call, inject context the model must see, halt the session on a signal |
1021
+ | **Mods** | Extend Command Code itself with new tools, commands, flags, or broader lifecycle behavior; see [Mods](https://commandcode.ai/docs/mods) |
1022
+ | **Skills** | Give the model a workflow it can choose to invoke |
1023
+ | **Slash commands** | Let the user trigger a fixed prompt or action |
1024
+ | **`AGENTS.md`** | Describe project norms the model follows by default but can deviate from |
1025
+
1026
+ ### Writing safe hooks
1027
+
1028
+ - **Parse stdin with `jq -r`, never `eval`:** Everything in `tool_input` came from the model and should be treated as untrusted.
1029
+ - **Quote every variable** before passing it to shell. The [`deny-dangerous.sh`](#block-dangerous-bash-commands) example uses `grep -qE` on a quoted `printf`, never `eval $cmd`.
1030
+ - **Keep `timeout` tight** (10 seconds or less for sync hooks). A slow hook blocks the tool call and makes Command Code feel laggy.
1031
+ - **Prefer `additionalContext` over `systemMessage`** when guiding the model, and `systemMessage` when explaining a policy violation to the user.
1032
+
1033
+ ### Testing a hook locally
1034
+
1035
+ You don't need to run Command Code to iterate on a hook. Pipe a fake payload in:
1036
+
1037
+ ```bash
1038
+ cat <<'EOF' | COMMANDCODE_PROJECT_DIR="$PWD" COMMANDCODE_SESSION_ID=test COMMANDCODE_HOOK_EVENT=PreToolUse ./.commandcode/hooks/deny-dangerous.sh
1039
+ {
1040
+ "session_id": "test",
1041
+ "transcript_path": "/tmp/t.jsonl",
1042
+ "cwd": ".",
1043
+ "hook_event_name": "PreToolUse",
1044
+ "permission_mode": "default",
1045
+ "tool_name": "shell_command",
1046
+ "tool_display_name": "SHELL",
1047
+ "tool_input": { "command": "rm -rf /" }
1048
+ }
1049
+ EOF
1050
+ ```
1051
+
1052
+ Inspect stdout (JSON, or empty for "no opinion") and the exit code (`echo $?`). Ensure the script has execute permissions (`chmod +x`) to run as a hook in Command Code.
1053
+
1054
+ ### Common failure modes
1055
+
1056
+ What you'll see when a hook doesn't behave the way you expect:
1057
+
1058
+ | **Symptom** | **Likely cause** | **Fix** |
1059
+ | ----------- | ---------------- | ------- |
1060
+ | Hook never runs | You're in plan mode (hooks are skipped). The matcher regex doesn't match any of `SHELL`, `READ`, `WRITE`, `EDIT`. The script isn't executable | Exit plan mode. Check `matcher` against the display names `SHELL`, `READ`, `WRITE`, `EDIT`. Run `chmod +x` for your executables. |
1061
+ | Tool runs despite `"deny"` | `permissionDecision` is misspelled, or `hookSpecificOutput` is missing | Validate output against the [Hook output](#hook-output-stdout) schema |
1062
+ | Timeout errors in the log | Hook takes longer than its `timeout` | Raise `timeout`, or move slow work to a background process |
1063
+ | Hook crashes silently | Stdout is invalid JSON on exit `0` | Return empty stdout for "no opinion", or emit valid JSON |
1064
+ | `jq: command not found` | `jq` isn't installed on the machine running Command Code | `brew install jq`, or rewrite the hook in Python or Node |
1065
+
1066
+ For all of these, `cmd --debug` logs each hook evaluation (see below).
1067
+
1068
+ ### Debugging hooks
1069
+
1070
+ When a hook fires but doesn't behave the way you expect, run Command Code with `--debug` and tail the log:
1071
+
1072
+ ```bash
1073
+ cmd --debug
1074
+ # in another terminal
1075
+ tail -f ~/.commandcode/logs/command.log
1076
+ ```
1077
+
1078
+ The log records every hook evaluation: trust checks, config loads, matcher decisions, stdin/stdout payloads, and non-zero exit codes. You can see exactly why a hook was (or wasn't) invoked and what it returned.
1079
+
1080
+ The log file only exists while `--debug` is active and appends across sessions, so clear it between runs if noisy.
1081
+
1082
+ ### Performance
1083
+
1084
+ Hooks fire on **every** matching tool call. A slow hook means added latency on every matching tool call.
1085
+
1086
+ - **Keep hooks fast:** Well under a second. Users notice lag immediately.
1087
+ - **Push slow work to `PostToolUse`:** It runs after the tool completes and in parallel, so slowness there doesn't block the agent.
1088
+ - **Move truly slow work out of process:** If you need to talk to a SIEM or policy server, send a fire-and-forget HTTP request or append to a local log. Don't block on the round trip.
1089
+
1090
+ ---
1091
+
1092
+ ## Next steps
1093
+
1094
+ - [Mods](https://commandcode.ai/docs/mods): extend Command Code itself with new tools, commands, and broader lifecycle behavior
1095
+ - [CLI Reference](https://commandcode.ai/docs/reference/cli): flags and commands that pair with hooks
1096
+ - [Skills](./skills.md): give the model workflows it can choose to invoke
1097
+ - Stuck on a schema edge case? Ask in our [Discord community](https://commandcode.ai/discord)