Skip to content

Commit 1060601

Browse files
committed
feat(hooks): add 4 new hook events to match Claude Code / OpenCode / pi
Adds the four hook events that all three competitor coding agents expose but docker-agent did not: - user_prompt_submit: fires once per user message, after submission and before the first model call. Can block the prompt or contribute transient additional_context. Skipped for sub-sessions whose kick-off message is synthesised by the runtime. - pre_compact: fires before context-window compaction (manual / auto / overflow / tool_overflow trigger). Can cancel compaction or append guidance to the compaction prompt. - subagent_stop: fires when a sub-agent (transfer_task, background agent, skill sub-session) finishes. Runs against the parent's hooks executor so handlers placed on the orchestrator see every child. - permission_request: fires just before the runtime would prompt the user to approve a tool call. Hooks can short-circuit the prompt by returning permission_decision=allow|deny, mirroring pre_tool_use. Also fixes the post_tool_use documentation everywhere (Go doc, schema, docs/, example) to state that it fires on both success and failure; tool_response.is_error distinguishes the two. Adds five contract-widening tests pinning the new events' wire-format contract. Assisted-By: docker-agent
1 parent 87233ff commit 1060601

14 files changed

Lines changed: 707 additions & 42 deletions

File tree

‎agent-schema.json‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,14 @@
523523
},
524524
"post_tool_use": {
525525
"type": "array",
526-
"description": "Hooks that run after a tool completes. Can provide validation or additional context.",
526+
"description": "Hooks that run after a tool completes (both success and failure). The result is delivered in tool_response (failed calls carry an is_error flag and any error text). Returning decision=block or exit code 2 stops the run loop after the current tool batch — useful for circuit-breaker patterns.",
527+
"items": {
528+
"$ref": "#/definitions/HookMatcherConfig"
529+
}
530+
},
531+
"permission_request": {
532+
"type": "array",
533+
"description": "Hooks that run just before the runtime prompts the user to approve a tool call (i.e. when --yolo and permission rules did not short-circuit the decision). Hooks may auto-allow or auto-deny via hook_specific_output.permission_decision; otherwise control falls through to the interactive confirmation. Tool-matched, like pre_tool_use.",
527534
"items": {
528535
"$ref": "#/definitions/HookMatcherConfig"
529536
}
@@ -535,6 +542,13 @@
535542
"$ref": "#/definitions/HookDefinition"
536543
}
537544
},
545+
"user_prompt_submit": {
546+
"type": "array",
547+
"description": "Hooks that run once per user message, after the user has submitted their prompt and before the first model call of the turn. The submitted text is passed in the prompt field. Hooks can block submission (decision=block / continue=false / exit code 2) or contribute additional_context that is spliced into the conversation as a transient system message for that turn only. Sub-sessions (transferred tasks, background agents) do NOT fire this event because their kick-off message is synthesised by the runtime.",
548+
"items": {
549+
"$ref": "#/definitions/HookDefinition"
550+
}
551+
},
538552
"turn_start": {
539553
"type": "array",
540554
"description": "Hooks that run at the start of every agent turn (each model call). Their AdditionalContext is appended as transient system messages for that turn only — it is NOT persisted to the session, so per-turn signals (date, prompt files) are recomputed every turn instead of bloating message history on every resume.",
@@ -563,6 +577,20 @@
563577
"$ref": "#/definitions/HookDefinition"
564578
}
565579
},
580+
"pre_compact": {
581+
"type": "array",
582+
"description": "Hooks that run just before the runtime compacts the session transcript into a summary. The trigger is reported in source: 'manual' (user-initiated /compact), 'auto' (proactive threshold), 'overflow' (context-overflow recovery), or 'tool_overflow' (proactive after tool results pushed past the threshold). Hooks may block compaction (decision=block / continue=false / exit code 2) or contribute additional_context that is appended to the compaction prompt — useful for steering the summary without modifying the agent's instruction.",
583+
"items": {
584+
"$ref": "#/definitions/HookDefinition"
585+
}
586+
},
587+
"subagent_stop": {
588+
"type": "array",
589+
"description": "Hooks that run when a sub-agent (transferred task, background agent, skill sub-session) finishes. Fires against the parent agent's executor so handlers configured on the orchestrator see every child completion. The sub-agent's name is in agent_name and its final assistant message in stop_response.",
590+
"items": {
591+
"$ref": "#/definitions/HookDefinition"
592+
}
593+
},
566594
"on_user_input": {
567595
"type": "array",
568596
"description": "Hooks that run when the agent needs user input. Can send notifications or log events.",

‎docs/configuration/hooks/index.md‎

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Hooks allow you to execute shell commands or scripts at key points in an agent's
1919
- Validate or transform tool inputs before execution
2020
- Log all tool calls to an audit file
2121
- Block dangerous operations based on custom rules
22+
- Validate, redact, or enrich user prompts before they reach the model
23+
- Programmatically approve or deny tool calls without prompting the user
24+
- Steer or veto context-window compaction
25+
- Audit sub-agent handoffs in multi-agent setups
2226
- Set up the environment when a session starts
2327
- Clean up resources when a session ends
2428
- Log or validate model responses before returning to the user
@@ -30,15 +34,19 @@ Hooks allow you to execute shell commands or scripts at key points in an agent's
3034

3135
There are seven hook event types:
3236

33-
| Event | When it fires | Can block? |
34-
| ---------------- | ------------------------------------------------------ | ---------- |
35-
| `pre_tool_use` | Before a tool call executes | Yes |
36-
| `post_tool_use` | After a tool completes successfully | No |
37-
| `session_start` | When a session begins or resumes | No |
38-
| `session_end` | When a session terminates | No |
39-
| `on_user_input` | When the agent is waiting for user input | No |
40-
| `stop` | When the model finishes responding | No |
41-
| `notification` | When the agent emits a notification (error or warning) | No |
37+
| Event | When it fires | Can block? |
38+
| ------------------- | ------------------------------------------------------------------- | ---------- |
39+
| `pre_tool_use` | Before a tool call executes | Yes |
40+
| `post_tool_use` | After a tool completes — fires for both success and failure | Yes |
41+
| `permission_request`| Just before the runtime would prompt the user to approve a tool | Yes |
42+
| `session_start` | When a session begins or resumes | No |
43+
| `user_prompt_submit`| Once per user message, after submission and before the model runs | Yes |
44+
| `session_end` | When a session terminates | No |
45+
| `pre_compact` | Just before the runtime compacts the session transcript | Yes |
46+
| `subagent_stop` | When a sub-agent (transferred task / background) finishes | No |
47+
| `on_user_input` | When the agent is waiting for user input | No |
48+
| `stop` | When the model finishes responding | No |
49+
| `notification` | When the agent emits a notification (error or warning) | No |
4250

4351
## Configuration
4452

@@ -178,26 +186,34 @@ Hooks receive JSON input via stdin with context about the event:
178186

179187
### Input Fields by Event Type
180188

181-
| Field | pre_tool_use | post_tool_use | session_start | session_end | on_user_input | stop | notification |
182-
| ---------------------- | ------------ | ------------- | ------------- | ----------- | ------------- | ---- | ------------ |
183-
| `session_id` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
184-
| `cwd` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
185-
| `hook_event_name` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
186-
| `tool_name` | ✓ | ✓ | | | | | |
187-
| `tool_use_id` | ✓ | ✓ | | | | | |
188-
| `tool_input` | ✓ | ✓ | | | | | |
189-
| `tool_response` | | ✓ | | | | | |
190-
| `source` | | | ✓ | | | | |
191-
| `reason` | | | | ✓ | | | |
192-
| `stop_response` | | | | | | ✓ | |
193-
| `notification_level` | | | | | | | ✓ |
194-
| `notification_message` | | | | | | | ✓ |
189+
| Field | pre_tool_use | post_tool_use | permission_request | session_start | user_prompt_submit | session_end | pre_compact | subagent_stop | on_user_input | stop | notification |
190+
| ---------------------- | ------------ | ------------- | ------------------ | ------------- | ------------------ | ----------- | ----------- | ------------- | ------------- | ---- | ------------ |
191+
| `session_id` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
192+
| `cwd` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
193+
| `hook_event_name` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
194+
| `tool_name` | ✓ | ✓ | ✓ | | | | | | | | |
195+
| `tool_use_id` | ✓ | ✓ | ✓ | | | | | | | | |
196+
| `tool_input` | ✓ | ✓ | ✓ | | | | | | | | |
197+
| `tool_response` | | ✓ | | | | | | | | | |
198+
| `source` | | | | ✓ | | | ✓ | | | | |
199+
| `reason` | | | | | | ✓ | | | | | |
200+
| `prompt` | | | | | ✓ | | | | | | |
201+
| `agent_name` | | | | | | | | ✓ | | | |
202+
| `parent_session_id` | | | | | | | | ✓ | | | |
203+
| `stop_response` | | | | | | | | ✓ | | ✓ | |
204+
| `notification_level` | | | | | | | | | | | ✓ |
205+
| `notification_message` | | | | | | | | | | | ✓ |
195206

196207
The `source` field for `session_start` can be: `startup`, `resume`, `clear`, or `compact`.
208+
The `source` field for `pre_compact` can be: `manual` (user-initiated `/compact`), `auto` (proactive threshold), `overflow` (context-overflow recovery), or `tool_overflow` (proactive recovery after tool results pushed past the threshold).
197209

198210
The `reason` field for `session_end` can be: `clear`, `logout`, `prompt_input_exit`, or `other`.
199211

200-
The `stop_response` field contains the model's final text response.
212+
The `prompt` field for `user_prompt_submit` is the text the user just submitted. Sub-sessions (transferred tasks, background agents, skills) do **not** fire this event because their kick-off message is synthesised by the runtime, not authored by the user.
213+
214+
The `agent_name` field for `subagent_stop` is the name of the sub-agent that just finished; `parent_session_id` is the session that spawned it.
215+
216+
The `stop_response` field contains the model's final text response (for both `stop` and `subagent_stop`).
201217

202218
The `notification_level` field can be: `error` or `warning`.
203219

@@ -245,7 +261,7 @@ The `hook_specific_output` for `pre_tool_use` supports:
245261

246262
### Plain Text Output
247263

248-
For `session_start`, `post_tool_use`, and `stop` hooks, plain text written to stdout (i.e., output that is not valid JSON) is captured as additional context for the agent.
264+
For `session_start`, `user_prompt_submit`, `post_tool_use`, `pre_compact`, and `stop` hooks, plain text written to stdout (i.e., output that is not valid JSON) is captured as additional context for the agent. For `pre_compact` it is appended to the compaction prompt; for the others it is spliced into the conversation as a (transient or persisted) system message depending on the event.
249265

250266
## Exit Codes
251267

@@ -410,6 +426,51 @@ The `notification` hook fires when:
410426
- A degenerate tool call loop is detected
411427
- The maximum iteration limit is reached
412428

429+
### Pre-Compact: steer the summary
430+
431+
`pre_compact` fires just before the runtime compacts the session transcript. Its `source` field tells you why compaction was triggered:
432+
433+
- `manual` — the user invoked `/compact`
434+
- `auto` — proactive compaction at the configured threshold
435+
- `overflow` — emergency compaction after a context-overflow error
436+
- `tool_overflow` — proactive compaction triggered by tool results pushing the estimated context past the threshold
437+
438+
Return `additional_context` (or plain stdout) to append guidance to the compaction prompt without modifying the agent's instruction. Block the event (`decision: block` / exit code 2) to cancel compaction — useful when you want to handle truncation yourself.
439+
440+
### User-Prompt-Submit: gate or enrich every user message
441+
442+
`user_prompt_submit` fires once per user message, after the prompt is recorded in the session and before the first model call. The submitted text is in `prompt`. Use it to:
443+
444+
- block prompts that violate policy (`decision: block` / exit code 2),
445+
- inject per-prompt context (`additional_context` is spliced as a transient system message for that turn),
446+
- audit user prompts to a log.
447+
448+
It does **not** fire for sub-sessions (transferred tasks, background agents, skill sub-sessions) because their kick-off message is synthesised by the runtime.
449+
450+
### Subagent-Stop: observe handoff completions
451+
452+
`subagent_stop` fires whenever a sub-agent finishes — `transfer_task` returns, a background agent completes, or a skill sub-session ends. It runs against the *parent* agent's hooks executor, so handlers configured on the orchestrator see every child completion in one place. The sub-agent's name is in `agent_name`, the parent's session ID in `parent_session_id`, and the child's final assistant message in `stop_response`.
453+
454+
### Permission-Request: programmatic tool approval
455+
456+
`permission_request` fires just before the runtime would prompt the user to approve a tool call (i.e. when neither `--yolo` nor a permissions rule short-circuited the decision and the tool is not read-only). Use the same `hook_specific_output.permission_decision` shape as `pre_tool_use` to auto-approve or auto-deny the call:
457+
458+
```yaml
459+
hooks:
460+
permission_request:
461+
- matcher: "shell"
462+
hooks:
463+
- type: command
464+
command: |
465+
INPUT=$(cat)
466+
CMD=$(echo "$INPUT" | jq -r '.tool_input.cmd // ""')
467+
if echo "$CMD" | grep -qE '^(ls|pwd|cat) '; then
468+
echo '{"hook_specific_output":{"permission_decision":"allow","permission_decision_reason":"safe read-only command"}}'
469+
fi
470+
```
471+
472+
Return nothing to fall through to the usual interactive confirmation.
473+
413474
</div>
414475

415476
## CLI Flags

0 commit comments

Comments
 (0)