Skip to content

feat(hooks): PreToolUse + PostToolUse subprocess hooks#206

Merged
Fullstop000 merged 2 commits into
masterfrom
feat/tool-hooks
Jun 19, 2026
Merged

feat(hooks): PreToolUse + PostToolUse subprocess hooks#206
Fullstop000 merged 2 commits into
masterfrom
feat/tool-hooks

Conversation

@Fullstop000

Copy link
Copy Markdown
Owner

Summary

Extends ignis's subprocess hook system with two tool-level events — its founding motivation: programmable per-tool transformation/vetoing.

  • PreToolUse runs before the permission gate. It can block the call (continue:false / exit 2 → the model gets Blocked 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.
  • 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 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 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 the Warning event the Ink frontend already renders.

Verification

  • Unit: matcher globbing, config parse (matcher + both events), protocol round-trip of tool fields, HookEvent::ALL=4.
  • Integration (real subprocess hooks): PreToolUse rewrite, exit-2 block, matcher-skip, PostToolUse rewrite.
  • Full gate: fmt, clippy (clean), 825 Rust tests.
  • Code-review pass: no significant findings (gate-after-rewrite, sub-agent isolation, parallel safety all confirmed).
  • Dogfooded through the real binary + a live model: a PreToolUse hook rewrote the model's echo hello-originalecho REWRITTEN_BY_HOOK, and the rewritten command is what executed (tool block shows bash(echo REWRITTEN_BY_HOOK)).

Note for sequencing

Touches hooks/* + the per-tool path in agent/mod.rs. The mechanical ..Default::default() / tool: None test-literal churn is from adding fields to HooksConfig/HookSpec/DispatchContext.

🤖 Generated with Claude Code

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.
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.

1 participant