feat(cli): support user-owned PreToolUse hooks that can deny a tool call (#217)#218
Merged
Merged
Conversation
…all (#217) Add a hooks.PreToolUse section to user settings so operators can declare a command that runs before a tool executes and can block it (exit code 2 or a JSON permissionDecision of deny). A hook can only make a decision more restrictive — silence leaves the normal approval flow unchanged, so a hook can never relax an approval. Hook configuration is read only from the user scope; a trusted project scope cannot inject a hook. Malformed configuration, an oversized configuration, and a hook timeout fail closed before any tool side effect. The hook command, arguments, and deny reason are redacted. Includes a redacted read-only listing (--list-hooks) that reports an empty inventory when no hooks section exists.
Owner
Author
E2E verification
|
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
Adds a
hooks.PreToolUsesection to user settings so operators can declare a command that runs before a tool executes and can block it. A hook can only deny — silence leaves the normal approval flow unchanged, so a hook can never relax an approval.What changed
src/hook-contract.ts— new module: versioned contract (v1), strict Zod validation (unknown keys rejected, credential fields rejected, invalid matchers rejected),spawnHookRunner(direct spawn, no shell, bounded timeout and output),evaluatePreToolUseHooks(deny-only gate, fail closed on timeout/spawn error),collectHookList/formatHookList(redacted read-only listing).src/agent.ts— PreToolUse hook gate runs before approval inexecuteToolCall; a denying hook blocks the call before any side effect.src/effective-settings.ts— registeredhooksas a settings key, added toOBJECT_SECTIONSandPROTECTED_PROJECT_SECTIONS(project scope cannot inject a hook).src/index.ts—--list-hooksCLI flag (redacted inventory, exit 0 on empty, exit 2 on malformed); hook resolution at session start for both headless (-p) and interactive paths.tests/unit/hook-contract.test.ts— 37 unit tests: version negotiation, strict validation, user-scope-only resolution, matcher logic, deny/allow/silence decisions, timeout/spawn-error fail-closed, redaction, listing.tests/integration/hook-contract.test.ts— 15 integration tests: listing (JSON/text, empty, malformed), deny gate end-to-end via-p(exit 2, JSON deny, non-matching, yolo mode, project-scope ignored, redaction).Acceptance criteria coverage
hooks.PreToolUsevalidates strictly--list-hooksexits 0 with empty hooks arrayVerification
npm run typecheck✓npm run build✓npm test— 1585 unit tests pass ✓npm run test:integration— 428 integration tests pass ✓npm run smoke— 51 smoke tests pass ✓Closes #217