Skip to content

feat(cli): support user-owned PreToolUse hooks that can deny a tool call (#217)#218

Merged
qwen-code-dev-bot merged 1 commit into
mainfrom
issue/217-pretooluse-deny-hook
Jul 22, 2026
Merged

feat(cli): support user-owned PreToolUse hooks that can deny a tool call (#217)#218
qwen-code-dev-bot merged 1 commit into
mainfrom
issue/217-pretooluse-deny-hook

Conversation

@qwen-code-dev-bot

Copy link
Copy Markdown
Owner

Summary

Adds a hooks.PreToolUse section 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 in executeToolCall; a denying hook blocks the call before any side effect.
  • src/effective-settings.ts — registered hooks as a settings key, added to OBJECT_SECTIONS and PROTECTED_PROJECT_SECTIONS (project scope cannot inject a hook).
  • src/index.ts--list-hooks CLI 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

Criterion Evidence
hooks.PreToolUse validates strictly unit: unknown keys, credential fields, invalid matchers all rejected
Deny hook blocks tool call with redacted reason integration: exit 2 and JSON deny both block; file not created
Hook can only deny, never approve integration: allow/exit-0 hook does not block; unit: allow treated as silence
Project scope not honored integration: project-scope hook listing returns empty; runtime deny ignored
Malformed/oversized/timeout fail closed integration: malformed exits 2 before provider call; unit: timeout/spawn deny
Command, args, reason redacted unit + integration: secrets and home path redacted in deny reason
Redacted listing, empty inventory on absent integration: --list-hooks exits 0 with empty hooks array

Verification

  • 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

…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.
@qwen-code-dev-bot

Copy link
Copy Markdown
Owner Author

E2E verification

  • Repository: qwen-code-dev-bot/oh-my-cli
  • Pull request: #218
  • Commit: 7f73801ea26f822996a2da55eed9bf7c3e280485
  • Evidence mode: terminal
  • Result: PASS
Scenario Command Exit Duration
integration npm run test:integration 0 28.058s
smoke npm run smoke 0 3.017s

E2E evidence

Full immutable report

@qwen-code-dev-bot
qwen-code-dev-bot merged commit 3029868 into main Jul 22, 2026
2 checks passed
@qwen-code-dev-bot
qwen-code-dev-bot deleted the issue/217-pretooluse-deny-hook branch July 22, 2026 14:46
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.

Hooks: support a user-owned PreToolUse hook that can deny a tool call (redacted, fail-closed)

1 participant