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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants