feat(hooks): PreToolUse + PostToolUse subprocess hooks#206
Merged
Conversation
Extend the subprocess hook system with two tool-level events, ignis's founding motivation — programmable per-tool transformation/vetoing. - PreToolUse runs BEFORE the permission gate. It can block the call (continue:false / exit 2 → "Blocked by hook") or rewrite the tool's args (updatedInput, parsed back to JSON). The gate + tool both see the rewrite. - PostToolUse runs after the tool (success OR error, via isError). It can rewrite the result the model sees (updatedOutput) or just observe; it cannot block (the tool already ran). - Optional tool-name `matcher` per hook spec (`bash`, `mcp__*`, `*`); absent = all tools. New `toolName`/`toolInput`/`toolResult`/`isError` envelope fields. - Reuses the entire existing subprocess machinery: chained dispatch, soft-fail →Warning, Landlock sandbox + env allowlist, `/hooks` listing + reload. Any hook failure degrades to the original value + a Warning (never blocks, except an explicit PreToolUse block). A malformed updatedInput soft-fails to the prior args. - Integrated in the per-tool execution path: PreToolUse (sequential) → gate → tool.call → PostToolUse (inside execute_single_tool, so it works under parallel fan-out). Main-agent only — sub-agent tool calls are unhooked in v1. Engine-only (no frontend changes); soft-failures surface via the existing Warning event the Ink frontend already renders. Tests: matcher globbing, config parse (matcher + both events), protocol round-trip of tool fields, HookEvent::ALL=4; integration tests with real subprocess hooks (PreToolUse rewrite, exit-2 block, matcher-skip, PostToolUse rewrite). Dogfooded through the real binary + a live model: a PreToolUse hook rewrote `echo hello-original` → `echo REWRITTEN_BY_HOOK` and the rewritten command is what executed.
Fullstop000
force-pushed
the
feat/tool-hooks
branch
from
June 19, 2026 05:02
e6e4d2b to
6bba8f4
Compare
Merged
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends ignis's subprocess hook system with two tool-level events — its founding motivation: programmable per-tool transformation/vetoing.
continue:false/ exit 2 → the model getsBlocked by hook: <reason>) or rewrite the tool's args (updatedInput, parsed back to JSON). The permission gate and the tool both see the rewritten args.isError). It can rewrite the result the model sees (updatedOutput) or just observe. It cannot block (the tool already ran).matcherper hook spec (bash,mcp__*,*); absent = all tools. NewtoolName/toolInput/toolResult/isErrorenvelope fields.Reuses the entire existing machinery: chained dispatch, soft-fail→Warning, Landlock sandbox + env allowlist,
/hookslisting + reload. Any hook failure degrades to the original value + a Warning (never blocks, except an explicit PreToolUse block); a malformedupdatedInputsoft-fails to the prior args.Integrated in the per-tool path: PreToolUse (sequential) → permission gate → tool.call → PostToolUse (inside
execute_single_tool, so it works under parallel fan-out). Main-agent only — sub-agent tool calls are unhooked in v1. Engine-only — soft-failures surface via theWarningevent the Ink frontend already renders.Verification
HookEvent::ALL=4.fmt,clippy(clean), 825 Rust tests.echo hello-original→echo REWRITTEN_BY_HOOK, and the rewritten command is what executed (tool block showsbash(echo REWRITTEN_BY_HOOK)).Note for sequencing
Touches
hooks/*+ the per-tool path inagent/mod.rs. The mechanical..Default::default()/tool: Nonetest-literal churn is from adding fields toHooksConfig/HookSpec/DispatchContext.🤖 Generated with Claude Code