Skip to content

feat(hooks): add 4 new hook events to match Claude Code / OpenCode / pi#2548

Merged
dgageot merged 3 commits intodocker:mainfrom
dgageot:board/comparing-hook-support-across-ai-coding-88de6052
Apr 28, 2026
Merged

feat(hooks): add 4 new hook events to match Claude Code / OpenCode / pi#2548
dgageot merged 3 commits intodocker:mainfrom
dgageot:board/comparing-hook-support-across-ai-coding-88de6052

Conversation

@dgageot
Copy link
Copy Markdown
Member

@dgageot dgageot commented Apr 27, 2026

What

Adds the four lifecycle hook events that Claude Code, OpenCode, and pi all expose but docker-agent did not, plus a small documentation fix.

Why

While auditing how docker-agent's hook system compares to peer AI coding agents, four events stood out as universally supported by competitors but missing from us. Each one unlocks a real use case that today either has to be hacked in via pre_tool_use/on_user_input or simply isn't possible.

New hook events

Event When it fires Can shape behaviour?
user_prompt_submit Once per real user message in RunStream, after session_start and before the first model call. Skipped for sub-sessions (whose kick-off message is synthesised by the runtime). Block submission, or contribute transient additional_context for that turn.
pre_compact Inside summarizeWithSource before context compaction. Trigger source (manual / auto / overflow / tool_overflow) is reported in Input.Source. Cancel compaction, or append guidance to the compaction prompt.
subagent_stop After runSubSessionForwarding and runSubSessionCollecting complete — success or failure (deferred dispatch). Observational.
permission_request Inside askUserForConfirmation just before the runtime would prompt the user. Auto-allow (skip the prompt) or auto-deny (permission_decision: allow / deny), mirroring pre_tool_use. Returning nothing falls through to the interactive confirmation.

Also fixed

  • post_tool_use doc / schema / example wording: it fires on both success and failure, with tool_response.is_error distinguishing them. The previous "after a tool completes successfully" claim was wrong.
  • EventPermissionRequest's doc comment now spells out the asymmetry with pre_tool_use (where allow is the implicit default vs. permission_request where it's an explicit auto-approve verdict). That's why Result.PermissionAllowed exists separately from Result.Allowed.

Files touched

  • pkg/config/latest/types.go — 4 new fields on HooksConfig + validation
  • pkg/hooks/{types,executor,config}.go — 4 new EventType constants, Result.PermissionAllowed, Input.{Prompt, AgentName, ParentSessionID}, executor wiring
  • pkg/runtime/{hooks,loop,runtime,agent_delegation,skill_runner,tool_dispatch}.go — dispatch helpers and call-site integration
  • agent-schema.json, examples/hooks.yaml, docs/configuration/hooks/index.md — schema, example yaml demonstrating all events, and user-facing docs

Testing

  • 7 contract tests in pkg/hooks/contract_widening_test.go pin the wire format for every new event (block-produces-deny, allow-produces-permission-allowed, fields-reach-the-hook, …).
  • 2 runtime tests in pkg/runtime/user_prompt_submit_test.go pin the gating contract: fires-once for top-level submissions, never for sub-sessions (SendUserMessage=false).
  • The example examples/hooks.yaml parses through config.Load and validates against agent-schema.json.
  • mise run lint → 0 issues. go test -count=1 ./... → 0 failures across ~150 packages.

Commits

  1. feat(hooks): add 4 new hook events to match Claude Code / OpenCode / pi — the feature
  2. refactor(hooks): simplify the call sites added for the new events — small in-place readability cleanup (merge duplicated guards, switch on PermissionDecision, stop double-decoding tool args, take *agent.Agent directly instead of name + lookup)
  3. fix(hooks): address review findings on the new hook eventssubagent_stop now fires on the error path of both sub-session helpers (defer); EventPermissionRequest doc clarification; examples/hooks.yaml jq-dependency note; user_prompt_submit gating regression test

Backward compatibility

  • The four new fields on HooksConfig are all omitempty; existing configs continue to parse unchanged.
  • Summarize's public signature is unchanged; internal call-sites use a private summarizeWithSource to attribute the trigger to pre_compact hooks.
  • runSubSessionForwarding's parameter list changed (string → *agent.Agent) but the function is package-private; both call-sites updated.
@dgageot dgageot requested a review from a team as a code owner April 27, 2026 16:27
@dgageot dgageot force-pushed the board/comparing-hook-support-across-ai-coding-88de6052 branch from a5a80d6 to 3b8ba08 Compare April 27, 2026 17:04
trungutt
trungutt previously approved these changes Apr 28, 2026
@dgageot dgageot force-pushed the board/comparing-hook-support-across-ai-coding-88de6052 branch from 3b8ba08 to 948a1f8 Compare April 28, 2026 07:29
rumpl
rumpl previously approved these changes Apr 28, 2026
@dgageot dgageot force-pushed the board/comparing-hook-support-across-ai-coding-88de6052 branch from 948a1f8 to 7d14f65 Compare April 28, 2026 07:57
dgageot added 3 commits April 28, 2026 09:59
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
Three issues surfaced by code review:

* subagent_stop now fires on the error path of both sub-session helpers.
  Previously the hook was placed *after* the for-range childEvents loop,
  which an ErrorEvent skipped via early return \u2014 so handlers configured
  to observe sub-agent completions silently missed every failed run.
  Move the dispatch into a defer at the top of each helper so it fires
  for both success and failure (handlers can detect failure via empty
  stop_response or by correlating with the parent's error event).

* Clarify the EventPermissionRequest doc comment. The old text said the
  hook "mirrors pre_tool_use" \u2014 misleading, because pre_tool_use treats
  allow as the implicit default while permission_request treats it as
  an explicit auto-approve verdict (and that asymmetry is the whole
  reason Result.PermissionAllowed exists separately from Result.Allowed).
  New comment spells out the contract.

* examples/hooks.yaml now documents that the command-style hooks need
  jq, with install hints and a note on the graceful-degradation behaviour
  when jq is missing.

Plus a regression test pinning the user_prompt_submit gating contract:
fires exactly once on a top-level submission, never on a sub-session
(SendUserMessage=false). The test caches a counter across the two
cases via a tiny shared scaffold.

Assisted-By: docker-agent
@dgageot dgageot force-pushed the board/comparing-hook-support-across-ai-coding-88de6052 branch from 7d14f65 to 1149655 Compare April 28, 2026 08:02
@dgageot dgageot merged commit 104e823 into docker:main Apr 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants