magi-code 0.63.2

Repository-aware CLI coding agent for terminal work
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
# Tool-call bash hooks

[Feature docs index](README.md) · [Repository README](../../README.md)

## Purpose

Run optional local before/after shell hooks around built-in tool calls for auditing, policy gates, notifications, or trusted validation.

## Details

Hooks are optional local shell commands that run immediately before or after built-in tool calls. Use them for local auditing, policy gates, notifications, or trusted validation that should stay outside provider prompts.

Hook facts:

- Disabled by default. No hook process starts unless `hooks.enabled` is `true` and at least one matching hook is configured.
- Successful hook visibility in Mission Control is separate and off by default. Set `hooks.show_in_tui: true` to show running/success/failure hook rows in the TUI activity tree; this does not enable hooks by itself.
- Provider context injection visibility in Mission Control uses separate `hooks.injected_content.*` settings. `hooks.show_in_tui` does not control injected-content transcript or activity-tree rows.
- Hooks wrap built-in tools only: `read`, `bash`/`shell`, `hash_edit`, `write`, `ffgrep`, `parallel_subagents`, `web_search`, and `code_search`.
- Each hook receives one JSON payload on stdin and runs in the active agent cwd. For subagent child tool calls, inherited hooks run in the child task cwd, not the parent cwd.
- Hook stdout/stderr is local diagnostic material by default. It is not sent to providers and cannot rewrite tool arguments or results. Exception: successful `after_tool`, `after_assistant`, and `after_reasoning` hooks can opt in to provider-visible context injection with `provider_context_injection: true` and a bounded `context_items` JSON stdout contract.
- `before_tool` hooks never inject provider-visible context, even if injection fields are present for config symmetry.
- `after_assistant` hooks fire after the assistant produces a complete message; `after_reasoning` hooks fire after a complete reasoning summary. These message-phase hooks do not wrap a tool call, so `include_tools`/`exclude_tools` do not apply.
- Hook execution is separate from the provider-callable `bash` tool, so hooks do not recurse through tool calls.
- Hook diagnostics and hook lifecycle records are recorded as local-only session events. They are excluded from provider replay. A blocked tool result is still returned to the assistant, but with a sanitized message only.
- Successful hook lifecycle records persist to active session JSONL as `hook_lifecycle` events even when `hooks.show_in_tui` is `false`; that setting controls live TUI rows only.
- `parallel_subagents` is wrapped as one top-level parent tool call. Parent hooks can match `parallel_subagents`. Child subagent tool calls automatically inherit the parent hook configuration when hooks are enabled and non-inert; child hook lifecycle/diagnostic records persist only in child session JSONL under `sessions/subagents/` and are excluded from parent provider replay and parent `parallel_subagents` results.

## Quick start

Add this to `~/.magi-code/settings.json`:

```json
{
  "hooks": {
    "enabled": true,
    "show_in_tui": true,
    "injected_content": {
      "show_in_transcript": false,
      "show_in_activity_tree": false,
      "style": "content"
    },
    "payload": "redacted",
    "failure_policy": "warn",
    "before_tool": [
      {
        "label": "audit-before",
        "command": "./scripts/magi-hooks/audit-before.sh"
      }
    ],
    "after_tool": [
      {
        "label": "audit-after",
        "command": "./scripts/magi-hooks/audit-after.sh",
        "failure_policy": "ignore"
      }
    ]
  }
}
```

Create scripts in the repository where you launch `magi-code`:

```sh
mkdir -p scripts/magi-hooks
cat > scripts/magi-hooks/audit-before.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail
mkdir -p .magi-hooks
cat >> .magi-hooks/tool-hooks.jsonl
printf '\n' >> .magi-hooks/tool-hooks.jsonl
SH
chmod +x scripts/magi-hooks/audit-before.sh

cat > scripts/magi-hooks/audit-after.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail
mkdir -p .magi-hooks
cat >> .magi-hooks/tool-hooks.jsonl
printf '\n' >> .magi-hooks/tool-hooks.jsonl
SH
chmod +x scripts/magi-hooks/audit-after.sh
```

Configured hook command strings must pass magi-code's conservative preflight. Prefer script paths like `./scripts/magi-hooks/audit-before.sh` over inline shell.

### Configuration schema

Global settings under `hooks`:

| Field | Type | Default | Bounds / values | Meaning |
| --- | --- | --- | --- | --- |
| `enabled` | boolean | `false` | `true` or `false` | Master switch. `false` makes hooks inert. |
| `show_in_tui` | boolean | `false` | `true` or `false` | Display-only Mission Control opt-in. Shows matching hook start/success/failure activity rows when hooks are enabled; does not enable hook execution. Does not control provider context injection visibility. |
| `injected_content.show_in_transcript` | boolean | `false` | `true` or `false` | Display provider context injection rows in Mission Control transcript. Visibility only; does not enable provider context injection. |
| `injected_content.show_in_activity_tree` | boolean | `false` | `true` or `false` | Display provider context injection rows in Mission Control activity tree under the related tool/message activity when available, or as root rows otherwise. Visibility only; does not enable provider context injection. |
| `injected_content.style` | string | `"content"` | `"content"`, `"metadata"` | Transcript display style for injected content. `content` shows redacted/truncated injected content when a surface gate is enabled; `metadata` hides injected content and shows only label, status, item count, and byte count. |
| `payload` | string | `"redacted"` | `"redacted"`, `"full"` | Default payload mode for hooks. |
| `timeout_seconds` | integer | `5` | `1` to `60` | Per-hook process timeout. Timeout is hook failure. |
| `stdout_max_bytes` | integer | `8192` | `1` to `65536` | Max captured stdout bytes. Exceeding cap is hook failure; output is suppressed in diagnostics. |
| `stderr_max_bytes` | integer | `8192` | `1` to `65536` | Max captured stderr bytes. Exceeding cap is hook failure; output is suppressed in diagnostics. |
| `provider_context_injection` | boolean | `false` | `true` or `false` | Default opt-in for successful `after_tool` hooks to parse stdout `context_items` as provider-visible user messages. |
| `provider_context_max_bytes` | integer | `4096` | `1` to `16384` | Max total UTF-8 bytes of injected context content per hook. |
| `failure_policy` | string | `"warn"` | `"ignore"`, `"warn"`, `"block"`, `"fail"` | Default handling for hook failures. `block` is valid only for `before_tool`. |
| `before_tool` | array | `[]` | hook definitions | Hooks run before matching tool dispatch. |
| `after_tool` | array | `[]` | hook definitions | Hooks run after matching tool dispatch. `failure_policy: "block"` is rejected here. |
| `after_assistant` | array | `[]` | hook definitions | Hooks run after the assistant produces a complete message. `failure_policy: "block"` is rejected. `include_tools`/`exclude_tools` do not apply. |
| `after_reasoning` | array | `[]` | hook definitions | Hooks run after a complete reasoning summary. `failure_policy: "block"` is rejected. `include_tools`/`exclude_tools` do not apply. |

Hook definition fields inside `before_tool` or `after_tool`:

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `label` | string | `"hook"` | Local diagnostic label. Empty labels become `"hook"`. |
| `command` | string | required | Shell command to run after safety preflight. |
| `payload` | string | global `hooks.payload` | Override payload mode for this hook. |
| `failure_policy` | string | global `hooks.failure_policy` | Override failure policy for this hook. `block` only works in `before_tool`. |
| `timeout_seconds` | integer | global `hooks.timeout_seconds` | Override timeout, `1` to `60`. |
| `stdout_max_bytes` | integer | global `hooks.stdout_max_bytes` | Override stdout cap, `1` to `65536`. |
| `stderr_max_bytes` | integer | global `hooks.stderr_max_bytes` | Override stderr cap, `1` to `65536`. |
| `provider_context_injection` | boolean | global `hooks.provider_context_injection` | Per-hook override for `after_tool` provider context injection. Has no runtime effect for `before_tool`. |
| `provider_context_max_bytes` | integer | global `hooks.provider_context_max_bytes` | Per-hook context byte limit, `1` to `16384`. |
| `include_tools` | string array | `[]` | If non-empty, hook runs only for exact tool names listed. Prefer canonical tool names: `read`, `bash`, `hash_edit`, `write`, `ffgrep`, `parallel_subagents`, `web_search`, `code_search`. Has no effect for `after_assistant` / `after_reasoning`. |
| `exclude_tools` | string array | `[]` | Hook does not run for exact tool names listed. Applied after `include_tools`. Has no effect for `after_assistant` / `after_reasoning`. |

Hard runtime limit: hook stdin JSON stays capped at 1 MiB. Redacted payloads are summarized before spawn. Trusted `payload: "full"` bodies that would exceed the cap are moved to an ephemeral local `payload_ref` file when possible; otherwise stdin remains valid JSON with `payload.status: "omitted_too_large"` and bounded legacy summaries.

Hooks that exit or stop reading stdin early can cause a `BrokenPipe` while payload writer cleans up. This is normal hook behavior and is ignored for classification; hook exit status, timeout, cleanup warnings, and output limits still determine result. Only non-`BrokenPipe` stdin write or envelope failures are `stdin` failures.

Complete example:

```json
{
  "hooks": {
    "enabled": true,
    "show_in_tui": true,
    "injected_content": {
      "show_in_transcript": false,
      "show_in_activity_tree": false,
      "style": "content"
    },
    "payload": "redacted",
    "timeout_seconds": 5,
    "stdout_max_bytes": 8192,
    "stderr_max_bytes": 8192,
    "failure_policy": "warn",
    "before_tool": [
      {
        "label": "audit-before",
        "command": "./scripts/magi-hooks/audit-jsonl.sh",
        "include_tools": ["bash", "write", "hash_edit"]
      },
      {
        "label": "block-dangerous-bash",
        "command": "./scripts/magi-hooks/block-dangerous-bash.sh",
        "payload": "full",
        "failure_policy": "block",
        "include_tools": ["bash"]
      }
    ],
    "after_tool": [
      {
        "label": "after-log",
        "command": "./scripts/magi-hooks/after-log.sh",
        "failure_policy": "ignore",
        "exclude_tools": ["read", "ffgrep"]
      }
    ]
  }
}
```

### Lifecycle

For each provider-requested built-in tool call:

1. magi-code records the tool call in the session.
2. Matching `before_tool` hooks run in array order. Each matching hook writes sanitized local-only `hook_lifecycle` `started` and terminal (`success` or `failed`) records when sessions are enabled.
3. If all before hooks continue, target tool runs.
4. magi-code records the tool result in the session.
5. Matching `after_tool` hooks run in array order with both request and result payload fields. Each matching hook writes sanitized `hook_lifecycle` records after the tool result. If provider context injection is enabled for a successful after hook, magi-code parses stdout JSON and appends validated user-role context items after the tool result for the next provider request.
6. Hook warnings are displayed locally and recorded as local-only `hook_diagnostic` session events.
7. Provider continuation receives the normal tool result, or a sanitized blocked-tool result if a before hook blocked the tool.

Session JSONL success order is `tool_call` → before-hook `hook_lifecycle` started/terminal → `tool_result` → after-hook `hook_lifecycle` started/terminal → optional local-only `hook_context_injection` audit → optional provider-visible `provider_context_item`. Lifecycle and audit persistence are best-effort local telemetry and never become provider-visible output. Inherited subagent hook records are written to the child session JSONL, not the parent session JSONL.

For message-phase hooks:

- `after_assistant` hooks fire after `SessionEventKind::AssistantOutput` is recorded. Payload `tool.name` is `"assistant"` and `request.text` holds the full assistant message text.
- `after_reasoning` hooks fire after `SessionEventKind::ReasoningSummary` is recorded. Payload `tool.name` is `"reasoning"` and `request.text` holds the full reasoning summary text.
- Both phases record `hook_lifecycle` and `hook_context_injection` audit events the same way as `after_tool`.

`hook_lifecycle` payloads use an allowlist: `phase`, `label`, `target_tool`, `status`, `policy`, `category`, `target_ran`, `tool_call_id`, `activity_id`, `hook_index`, `elapsed_ms`, and `message`. They never store hook command text, hook stdin JSON, stdout, stderr, tool arguments, tool results, environment values, credentials, auth codes, bearer headers, or account ids.

`hook_context_injection` audit payloads are also local-only and allowlisted: `schema_version`, `phase`, `label`, `target_tool`, `status`, `item_count`, `byte_count`, `max_bytes`, and `hook_index`. They never store raw stdout/stderr, hook stdin, tool args/results, or injected content text.

Hook failure categories are `exit`, `timeout`, `runner`, `stdin`, and `output-limit`.

### Mission Control visibility

`hooks.show_in_tui` is an opt-in display setting for `magi-code --tui`:

```json
{
  "hooks": {
    "enabled": true,
    "show_in_tui": true,
    "before_tool": [
      { "label": "audit", "command": "./scripts/magi-hooks/audit-jsonl.sh" }
    ]
  }
}
```

When enabled, matching hooks appear live as child rows under the related tool activity with phase, label, target tool, running/success/failure status, and failure category/policy when relevant. Successful live hook rows do not create `hook_diagnostic` session JSONL and are not provider-visible. Inherited subagent hook rows may appear under the child task/tool activity; this is local Mission Control activity only and is not parent provider replay or parent tool-result material. If `hooks.enabled` is `false`, `show_in_tui` alone does nothing.

Provider context injection visibility is controlled by `hooks.injected_content`, not `hooks.show_in_tui`:

```json
{
  "hooks": {
    "enabled": true,
    "provider_context_injection": true,
    "injected_content": {
      "show_in_transcript": true,
      "show_in_activity_tree": true,
      "style": "metadata"
    }
  }
}
```

`show_in_transcript` adds a distinct local/system transcript row such as `provider context injection · memory-after · success · 1 item · 42 bytes`. `show_in_activity_tree` adds a child row under the related tool/message activity when available, or a root row when no parent exists. Both gates default to `false` and are independent from each other. Default `style` is `content`, which shows redacted/truncated injected content when a surface gate is enabled; `style: "metadata"` hides injected content and shows only label, status, item count, and byte count. These settings do not enable provider context injection and do not change provider-visible injected content.

Persisted `hook_lifecycle` history is independent of `hooks.show_in_tui`: resumed Mission Control session history displays recorded hook rows from JSONL without rerunning hook commands. Provider context injection display also hydrates from session JSONL when `hooks.injected_content.show_in_transcript` or `hooks.injected_content.show_in_activity_tree` is enabled.

### Payloads

Every hook receives one JSON object on stdin using schema `magi-code.tool_hook` with `schema_version: 1`. Top-level legacy fields remain: `phase`, `hook`, `tool`, `cwd`, `payload_mode`, `request`, and after hooks also `result`.

Before payload shape:

```json
{
  "schema": "magi-code.tool_hook",
  "schema_version": 1,
  "phase": "before_tool",
  "hook": {
    "label": "audit-before",
    "index": 0,
    "failure_policy": "warn",
    "payload_mode": "redacted"
  },
  "tool": { "name": "bash", "call_id": "call_abc123" },
  "context": {
    "cwd": "/Users/example/project",
    "session_id": "session-id-if-enabled",
    "session_path": "/Users/example/.magi-code/sessions/session-id-if-enabled.jsonl",
    "provider_id": "openai-codex",
    "model_id": "gpt-5",
    "agent_id": "tars",
    "invocation_mode": "shell",
    "turn_id": "turn-0",
    "message_id": "tool-tool-call-0",
    "subagent": false,
    "timestamp": "2026-05-26T00:00:00Z"
  },
  "affected_paths": [],
  "payload": {
    "inline": true,
    "status": "inline",
    "data": {
      "request": {
        "command": { "redacted": true, "kind": "command", "bytes": 42 },
        "timeout": 30
      }
    }
  },
  "payload_ref": null,
  "cwd": "/Users/example/project",
  "payload_mode": "redacted",
  "request": {
    "command": { "redacted": true, "kind": "command", "bytes": 42 },
    "timeout": 30
  }
}
```

After payload shape:

```json
{
  "schema": "magi-code.tool_hook",
  "schema_version": 1,
  "phase": "after_tool",
  "hook": { "label": "after-log", "index": 0, "failure_policy": "ignore", "payload_mode": "redacted" },
  "tool": { "name": "write", "call_id": "call_def456" },
  "context": { "cwd": "/Users/example/project", "invocation_mode": "print", "subagent": false },
  "affected_paths": [
    { "path": "notes.txt", "kind": "write_target", "source": "request.path" }
  ],
  "payload": {
    "inline": true,
    "status": "inline",
    "data": {
      "request": {
        "path": "notes.txt",
        "content": { "redacted": true, "kind": "content", "bytes": 20 }
      },
      "result": {
        "tool_name": "write",
        "success": true,
        "content": "<redacted:tool-output>",
        "metadata": { "path": "notes.txt" }
      }
    }
  },
  "payload_ref": null,
  "cwd": "/Users/example/project",
  "payload_mode": "redacted",
  "request": {
    "path": "notes.txt",
    "content": { "redacted": true, "kind": "content", "bytes": 20 }
  },
  "result": {
    "tool_name": "write",
    "success": true,
    "content": "<redacted:tool-output>",
    "metadata": { "path": "notes.txt" }
  }
}
```

Context metadata availability:

| Field | Source / null behavior |
| --- | --- |
| `session_id`, `session_path` | Active session when persistence is enabled; otherwise `null`. Subagents use child session values. |
| `provider_id`, `model_id` | Runtime provider selection; no credentials or account ids. |
| `agent_id` | Selected primary-agent id or subagent task identity/label when available; otherwise `null`. |
| `invocation_mode` | One of `print`, `shell`, `mission_control`, `subagent`. Set by caller path, not inferred from prompt text. |
| `turn_id`, `message_id` | Local run-generated correlation ids. Before/after hooks for one tool call share `message_id`. |
| `subagent` | `true` only for child subagent runs. |
| `timestamp` | Hook payload construction time in UTC. |

`affected_paths` is a bounded allowlisted array. `read.path` and `ffgrep.path` use `kind: "read_target"`; `write.path` uses `kind: "write_target"`. `source` names the structured field, currently `request.path`. Unsafe traversal, cwd/root escapes, overlong paths, and symlink-resolved escapes are omitted. Bash command text, stdout/stderr, model prose, and arbitrary strings are never parsed for paths.

`payload: "redacted"` summarizes or removes sensitive material before the hook sees it:

- command bodies: `command`, `cmd`
- file contents: `content`; hash-edit patch text: `input`
- captured output: `stdout`, `stderr`, `output`
- credential-shaped keys: API keys, tokens, bearer/OAuth values, auth codes, account ids, and similar fields
- credential-shaped substrings inside strings

`payload: "full"` sends original tool request JSON and full tool result JSON to the hook process. Use it only for trusted local scripts because it can expose command bodies, file contents, tool output, and secrets already present in tool inputs/results.

Full before payload example for trusted validation:

```json
{
  "schema": "magi-code.tool_hook",
  "schema_version": 1,
  "phase": "before_tool",
  "hook": { "label": "trusted-write-validator", "index": 0, "failure_policy": "block", "payload_mode": "full" },
  "tool": { "name": "write", "call_id": "call_xyz789" },
  "payload_mode": "full",
  "request": {
    "path": "src/generated.rs",
    "content": "pub fn generated() -> &'static str { \"ok\" }"
  },
  "payload": {
    "inline": true,
    "status": "inline",
    "data": {
      "request": {
        "path": "src/generated.rs",
        "content": "pub fn generated() -> &'static str { \"ok\" }"
      }
    }
  },
  "payload_ref": null
}
```

Large full payload behavior:

- Stdin JSON remains valid and under the 1 MiB cap.
- If full request/result bodies make the envelope too large, magi-code writes those bodies to a runtime-owned local `payload_ref.path` and sends only context plus bounded summaries on stdin.
- `payload_ref` includes `path`, `bytes`, `payload_mode`, `media_type: "application/json"`, and `digest_sha256`.
- Payload-ref files are private local artifacts (`0700` directories and `0600` files on Unix where supported) and are deleted after the hook process exits by default.
- If spill capture cannot be created or a hard cap is exceeded, hooks receive `payload.status: "omitted_too_large"`; legacy top-level `request`/`result` contain bounded summaries with `original_bytes`, `omitted_fields`, and `reason` rather than truncated JSON or raw large bytes.
- `payload_ref` paths, spill bytes, hook stdin, stdout/stderr, and hook diagnostics are local-only. They are excluded from provider requests, provider continuations, context-cache material, and parent subagent summaries.

Payload-ref stdin example:

```json
{
  "schema": "magi-code.tool_hook",
  "schema_version": 1,
  "phase": "before_tool",
  "tool": { "name": "write", "call_id": "call_big" },
  "payload": { "inline": false, "status": "referenced" },
  "payload_ref": {
    "path": "/private/tmp/magi-code-hook-payloads/.../payload.json",
    "bytes": 2097152,
    "payload_mode": "full",
    "media_type": "application/json",
    "digest_sha256": "sha256:..."
  },
  "request": {
    "status": "moved_to_payload_ref",
    "original_bytes": 2097152,
    "omitted_fields": ["request", "result"]
  }
}
```

### Provider context injection

Provider context injection is disabled by default. Enable it globally or per `after_tool` / `after_assistant` / `after_reasoning` hook when a trusted local script should add bounded context to the next provider request:

```json
{
  "hooks": {
    "enabled": true,
    "provider_context_injection": false,
    "provider_context_max_bytes": 4096,
    "after_tool": [
      {
        "label": "memory-after",
        "command": "./scripts/magi-hooks/memory-after.sh",
        "provider_context_injection": true,
        "provider_context_max_bytes": 4096
      }
    ]
  }
}
```

Successful enabled `after_tool` hooks may print exactly this JSON shape on stdout:

```json
{
  "context_items": [
    { "role": "user", "content": "Relevant bounded memory for next provider call." }
  ]
}
```

Rules:

- Only `after_tool`, `after_assistant`, and `after_reasoning` can inject. `before_tool` stdout never injects.
- Hook process must succeed. Non-zero exit, timeout, runner error, stdin error, or output-limit failure injects nothing.
- Only `role: "user"` is supported in v1. Injected items become normal provider-visible user messages, ordered after the tool result (for `after_tool`) or after the assistant/reasoning message (for `after_assistant` / `after_reasoning`).
- Multiple matching after hooks append in config order; multiple `context_items` append in JSON order.
- `provider_context_max_bytes` counts total UTF-8 content bytes per hook. Default `4096`, max `16384`. Set `stdout_max_bytes` at least as high as expected JSON stdout too; default stdout cap is `8192`, so 16KiB context needs a larger stdout cap or it fails as `hook_failed`/output-limit.
- Validation is all-or-nothing per hook. Invalid JSON, missing/wrong `context_items`, unsupported role, empty/non-string content, or over-limit content injects nothing.
- `hook_context_injection` records sanitized local audit status such as `success`, `invalid_json`, `invalid_shape`, `unsupported_role`, `empty_content`, `over_limit`, or `hook_failed`.
- Child subagent injections stay in child conversation/session. Parent provider requests and parent `parallel_subagents` results do not receive child injected context unless child final answer intentionally includes it.

#### `after_assistant` and `after_reasoning` phases

These phases fire outside tool dispatch and give trusted local scripts two additional context injection points.

```json
{
  "hooks": {
    "enabled": true,
    "after_assistant": [
      {
        "label": "memory-inject-assistant",
        "command": "./scripts/magi-hooks/memory-after-assistant.sh",
        "provider_context_injection": true,
        "provider_context_max_bytes": 4096
      }
    ],
    "after_reasoning": [
      {
        "label": "memory-inject-reasoning",
        "command": "./scripts/magi-hooks/memory-after-reasoning.sh",
        "provider_context_injection": true,
        "provider_context_max_bytes": 4096
      }
    ]
  }
}
```

Payload shape for `after_assistant`:

```json
{
  "schema": "magi-code.tool_hook",
  "schema_version": 1,
  "phase": "after_assistant",
  "hook": { "label": "memory-inject-assistant", "index": 0, "failure_policy": "warn", "payload_mode": "redacted" },
  "tool": { "name": "assistant", "call_id": "assistant-turn-0" },
  "context": { "cwd": "/Users/example/project", "invocation_mode": "print", "subagent": false },
  "affected_paths": [],
  "payload": { "inline": true, "status": "inline", "data": { "request": { "text": "<full assistant message text>" } } },
  "payload_ref": null,
  "cwd": "/Users/example/project",
  "payload_mode": "redacted",
  "request": { "text": "<full assistant message text>" }
}
```

Payload shape for `after_reasoning`:

```json
{
  "schema": "magi-code.tool_hook",
  "schema_version": 1,
  "phase": "after_reasoning",
  "hook": { "label": "memory-inject-reasoning", "index": 0, "failure_policy": "warn", "payload_mode": "redacted" },
  "tool": { "name": "reasoning", "call_id": "reasoning-turn-0" },
  "context": { "cwd": "/Users/example/project", "invocation_mode": "print", "subagent": false },
  "affected_paths": [],
  "payload": { "inline": true, "status": "inline", "data": { "request": { "text": "<full reasoning summary text>" } } },
  "payload_ref": null,
  "cwd": "/Users/example/project",
  "payload_mode": "redacted",
  "request": { "text": "<full reasoning summary text>" }
}
```

The `request.text` field contains the full assistant message or reasoning summary. The `result` field is absent (`null`) for these phases since there is no tool result. The same `context_items` JSON stdout contract applies for provider context injection.

### Failure policies

| Policy | Before hook failure | After hook failure | `after_assistant` / `after_reasoning` failure |
| --- | --- | --- | --- |
| `ignore` | Continue silently. | Continue silently. | Continue silently. |
| `warn` | Continue and show sanitized local warning. Default. | Continue and show sanitized local warning. Default. | Continue and show sanitized local warning. Default. |
| `block` | Prevent target tool. Assistant receives sanitized blocked result. | Not allowed. `after_tool` cannot block because target already ran; config using it is rejected. | Not allowed. Same as `after_tool`; config using it is rejected. |
| `fail` | Stop the turn before target tool runs. | Preserve original tool result locally, then fail the turn before provider continuation. | Fail the turn before provider continuation. |

A hook failure means non-zero exit, timeout, runner/preflight error, stdin write error, or stdout/stderr exceeding configured capture limits.

### Command safety and cwd

Hook commands run through the platform shell with cwd set to the agent cwd:

- Unix-like hosts: `/bin/bash -lc <command>`
- Windows hosts: PowerShell (`pwsh` or `powershell.exe`) when available

Before spawning, magi-code rejects hook command strings containing shell expansion characters that make cwd-scoping ambiguous:

```text
$  ~  `  {  }
```

It also rejects `cd`, parent-directory path components such as `..`, and absolute paths when `tools.bash.absolute_paths` is `false`. With the default `tools.bash.absolute_paths: true`, absolute hook command paths may pass preflight, but script paths inside the repository are easier to review and move across machines.

Safe command examples:

```text
./scripts/magi-hooks/audit-jsonl.sh
./scripts/magi-hooks/block-dangerous-bash.sh
scripts/magi-hooks/after-log.sh
```

Rejected command examples:

```text
echo "$PWD"
cd scripts && ./hook.sh
../hooks/audit.sh
~/hooks/audit.sh
./scripts/{audit,notify}.sh
```

Put complex logic inside scripts. The script body may use normal shell features; the restriction applies to the configured hook command string.

Hooks are configured as shell command strings, not as a special script language. The command string runs through the host shell after preflight. Shell scripts are common but not required; Python works when invoked by a preflight-safe shell command.

Python command examples:

```text
python3 scripts/magi-hooks/audit.py
./scripts/magi-hooks/audit.py
```

For the executable form, put a shebang at the top of the script and mark it executable:

```python
#!/usr/bin/env python3
import json
import pathlib
import sys

payload = json.load(sys.stdin)
pathlib.Path(".magi-hooks").mkdir(exist_ok=True)
with open(".magi-hooks/python-hooks.jsonl", "a", encoding="utf-8") as handle:
    handle.write(json.dumps({"phase": payload["phase"], "tool": payload["tool"]["name"]}) + "\n")
```

Configured command preflight still applies to the command string (`python3 scripts/magi-hooks/audit.py` or `./scripts/magi-hooks/audit.py`). Put complex logic inside the Python or shell script, not in the configured command.

### Script examples

Audit every hook payload to local JSONL:

```sh
#!/usr/bin/env bash
set -euo pipefail
mkdir -p .magi-hooks
cat >> .magi-hooks/tool-hooks.jsonl
printf '\n' >> .magi-hooks/tool-hooks.jsonl
```

Before gate for dangerous bash commands. Configure this hook with `payload: "full"`, `failure_policy: "block"`, and `include_tools: ["bash"]`:

```sh
#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)
command=$(printf '%s' "$payload" | jq -r '.request.command // ""')
case "$command" in
  *"git push --force"*|*"rm -rf"*|*"curl "*"| sh"*)
    printf 'blocked dangerous command pattern\n' >&2
    exit 42
    ;;
esac
```

After log or notification. Configure as `after_tool` with `failure_policy: "ignore"` so notification issues never change tool behavior:

```sh
#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)
mkdir -p .magi-hooks
tool=$(printf '%s' "$payload" | jq -r '.tool.name')
success=$(printf '%s' "$payload" | jq -r '.result.success // "n/a"')
printf '%s tool=%s success=%s\n' "$(date -u +%FT%TZ)" "$tool" "$success" >> .magi-hooks/after.log
```

Trusted full-payload write validator. Configure with `payload: "full"`, `failure_policy: "block"`, and `include_tools: ["write"]`:

```sh
#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)
path=$(printf '%s' "$payload" | jq -r '.request.path // ""')
content=$(printf '%s' "$payload" | jq -r '.request.content // ""')
case "$path" in
  *.pem|*.key|.env|*/.env)
    printf 'refusing write to secret-like path\n' >&2
    exit 10
    ;;
esac
if printf '%s' "$content" | grep -Eq 'sk-[A-Za-z0-9_-]{16,}|Authorization: Bearer '; then
  printf 'refusing credential-like content\n' >&2
  exit 11
fi
```

Include/exclude tools example:

```json
{
  "hooks": {
    "enabled": true,
    "before_tool": [
      {
        "label": "mutating-tools-only",
        "command": "./scripts/magi-hooks/mutation-gate.sh",
        "include_tools": ["write", "hash_edit", "bash"]
      }
    ],
    "after_tool": [
      {
        "label": "everything-except-noisy-readers",
        "command": "./scripts/magi-hooks/after-summary.sh",
        "exclude_tools": ["read", "ffgrep"]
      }
    ]
  }
}
```

### Privacy and sessions

- Redacted mode is default because hook payloads can contain command bodies, file contents, and tool output.
- Full mode is local only, but local scripts can write, print, forward, or leak what they receive. Use full mode only with scripts you control.
- Hook diagnostics are sanitized before terminal display and before session persistence.
- Hook stdout/stderr contents are not shown in diagnostics; failures report status/category with stdout/stderr suppressed.
- Session JSONL may include local-only `hook_diagnostic`, `hook_lifecycle`, and `hook_context_injection` audit events. They are not replayed to providers during session continuation and are excluded from context/cache material.
- Opt-in injected context is persisted separately as provider-visible `provider_context_item` session events so resumed sessions replay what the provider saw.
- Replayed Mission Control history reads persisted hook lifecycle rows from JSONL and does not rerun hook commands.
- A `before_tool` block creates a provider-visible tool result saying the tool was blocked by local hook policy. Hook label and detailed script output are redacted from that result.
- Inherited subagent hook internals stay in child session JSONL. Parent `parallel_subagents` output and parent provider continuation receive only child task summaries and sanitized hook-policy failure text.
- Subagents may run concurrently, so inherited hook commands may also run concurrently in different child cwd values. Hook scripts that touch shared external files, sockets, or services must provide their own locking.

### Troubleshooting hooks

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Hook never runs | `hooks.enabled` is `false`, arrays are empty, or filters do not match tool name. | Enable hooks and check exact names in `include_tools` / `exclude_tools`. |
| Hooks run but no successful live rows appear in Mission Control | `hooks.show_in_tui` is `false`, Mission Control is not active, or no matching hook ran. Session `hook_lifecycle` records still persist when hooks run and sessions are enabled. | Set `"show_in_tui": true` under `hooks` for live rows, keep `hooks.enabled: true`, and run `magi-code --tui`. |
| Settings fail to load with `block` error | `after_tool`, `after_assistant`, or `after_reasoning` hook inherits or sets `failure_policy: "block"`. | Set that after hook to `ignore`, `warn`, or `fail`. |
| Command rejected before spawn | Command string contains `$`, `~`, backticks, `{}`, `cd`, `..`, or disallowed absolute path. | Move logic into a script and configure a simple script path. |
| Hook times out | Script exceeds `timeout_seconds` or waits for interactive input. | Read stdin once, avoid prompts, increase timeout up to `60`. |
| Hook fails with `stdin` | Child closed stdin early or a rare envelope construction path still exceeded the bounded stdin cap after summaries. Large trusted full payloads normally use `payload_ref` or `omitted_too_large` instead of failing before spawn. | Make script drain stdin; inspect `payload.status`, `payload_ref`, and hook diagnostics. |
| Hook fails with `output-limit` | Script printed more than stdout/stderr caps. | Write large logs to files; keep stdout/stderr short; raise cap up to `65536`. |
| Script cannot find files | Hooks run in the active agent cwd. Inherited subagent hooks run in the child task cwd. | Use paths relative to the task cwd, or reviewed absolute paths when policy permits. |
| Script sees redacted content | Default payload mode is `redacted`. | Use per-hook `payload: "full"` only for trusted validators. |
| Assistant did not see hook warning | Hook diagnostics are local-only. | Inspect terminal/TUI transcript or session JSONL; use block/fail policy when provider-visible behavior must change. |

---

[Back to feature docs](README.md) · [Back to repository README](../../README.md)