You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hooks let users attach behavior to pre-defined points in the agent loop. #669 established the shared domain.HookPoint catalog (8 symmetric pre/post points) and the first action kind — system reminders (inject text). This ticket adds the command action: run an arbitrary shell command/script at a hook point, primarily post_session ("when the agent finished generating"), for deterministic post-processing (format, lint, test, notify). Feature-flagged, off by default.
domain.HookPoint — the fixed catalog both reminders and command hooks attach to:
pre
post
pre_session
post_session
pre_stream
post_stream
pre_tool
post_tool
pre_queue_drain
post_queue_drain
post_session is the "agent finished generating" point this ticket targets first. Dispatch flows through the single dispatchHooks(point) seam #669 added in the event-driven agent (internal/agent/agent_utils.go) and the headless loop (cmd/agent.go); this ticket registers a command runner alongside the reminder injector at that seam.
A domain.HookCommandProvider interface (sibling of the domain.SystemReminderProvider[FEATURE] Flexible system reminders: multiple reminders across the agent loop #669 introduced) resolves which commands are due at a point (CommandsDue(hook) []HookCommand); the agent — not the provider — runs them, so config stays free of os/exec.
Allow-list gated, secure-by-default. Each command is gated on the existing per-mode bash allow-list (config.IsBashCommandAllowed — the same matcher a model-proposed bash command faces), so command hooks open no new bypass. An off-list command is skipped and reported (a hook_command_skipped stream event carrying the rejection hint), never run; the user authorizes a command by allow-listing it (tools.bash.mode.*.allow or INFER_TOOLS_BASH_ALLOW_APPEND). This keeps headless safe: an unattended infer agent runs only allow-listed hook commands.
A single shared runner (agent.RunCommandHooks) is the one chokepoint both the chat agent and the headless loop call, so the gate and observability cannot drift between them.
Command stdin receives a JSON context (hook point, turn, session id), Claude-Code style. v1 is fire-and-observe: stdout/stderr/exit-code are captured for the stream event and logged, but not fed back into the conversation or used to alter the loop (block completion / inject a follow-up is a later iteration).
Summary
Hooks let users attach behavior to pre-defined points in the agent loop. #669 established the shared
domain.HookPointcatalog (8 symmetric pre/post points) and the first action kind — system reminders (inject text). This ticket adds the command action: run an arbitrary shell command/script at a hook point, primarilypost_session("when the agent finished generating"), for deterministic post-processing (format, lint, test, notify). Feature-flagged, off by default.Shared hook-point catalog (from #669)
domain.HookPoint— the fixed catalog both reminders and command hooks attach to:pre_sessionpost_sessionpre_streampost_streampre_toolpost_toolpre_queue_drainpost_queue_drainpost_sessionis the "agent finished generating" point this ticket targets first. Dispatch flows through the singledispatchHooks(point)seam #669 added in the event-driven agent (internal/agent/agent_utils.go) and the headless loop (cmd/agent.go); this ticket registers a command runner alongside the reminder injector at that seam.Design
Config lives in a dedicated
hooks.yaml(mirroring [FEATURE] Flexible system reminders: multiple reminders across the agent loop #669'sreminders.yaml— "run code" and "inject text" stay separate concerns), feature-flagged, off by default:A
domain.HookCommandProviderinterface (sibling of thedomain.SystemReminderProvider[FEATURE] Flexible system reminders: multiple reminders across the agent loop #669 introduced) resolves which commands are due at a point (CommandsDue(hook) []HookCommand); the agent — not the provider — runs them, so config stays free ofos/exec.Allow-list gated, secure-by-default. Each command is gated on the existing per-mode bash allow-list (
config.IsBashCommandAllowed— the same matcher a model-proposed bash command faces), so command hooks open no new bypass. An off-list command is skipped and reported (ahook_command_skippedstream event carrying the rejection hint), never run; the user authorizes a command by allow-listing it (tools.bash.mode.*.alloworINFER_TOOLS_BASH_ALLOW_APPEND). This keeps headless safe: an unattendedinfer agentruns only allow-listed hook commands.A single shared runner (
agent.RunCommandHooks) is the one chokepoint both the chat agent and the headless loop call, so the gate and observability cannot drift between them.Command stdin receives a JSON context (hook point, turn, session id), Claude-Code style. v1 is fire-and-observe: stdout/stderr/exit-code are captured for the stream event and logged, but not fed back into the conversation or used to alter the loop (block completion / inject a follow-up is a later iteration).
Each run emits a
kind: hook_commandstream event (exit code, duration, truncated output) for observability, mirroring [FEATURE] Flexible system reminders: multiple reminders across the agent loop #669'ssystem_reminderevents.Acceptance Criteria
domain.HookPointcatalog (validated against the fixed set).post_sessionwired).dispatchHooksseam).hooks.enabled), turned off by default.Builds atop #669 (shared hook-point catalog + dispatch seam).