Summary
packages/agent-bundle/src/events/projection.ts (around line 759) projects a tool/before result for Claude/Codex as:
permissionDecision: parsedValue?.outcome === 'deny' ? 'deny' : 'allow',
So a route that returns { outcome: 'continue' } — the normal "I have nothing to say about this command" answer — is delivered to the host as an explicit permissionDecision: "allow". Per the Claude Code hooks contract, an explicit allow bypasses the permission prompt for that tool call. Returning undefined from the route does not help: parsedValue is undefined and the ternary still yields 'allow' whenever the wrapper writes output.
Impact
Any plugin with a tool/before route matching Bash (cargo-hauler's does, to rewrite cargo … invocations) silently turns every other Bash call in the session — rm -rf, git push --force, anything — into an auto-approved call on Claude Code. Codex semantics unverified but the same field is emitted. This is a security regression introduced by the projection, not by plugin code, and a plugin cannot opt out today.
Expected
outcome: 'continue' with no updatedInput and no additionalContext → project to no permissionDecision (or emit no hookSpecificOutput at all), leaving the host's normal permission flow intact.
updatedInput present → permissionDecision: 'allow' is legitimate only if the contract requires it to apply the rewrite; if so, document that a rewrite implies approval, and consider a distinct route-level opt-in.
deny unchanged.
Repro
Any src/events/tool/before.tsx returning <Agent.Result value={{ outcome: 'continue' }} />; run the emitted Claude hook wrapper with a PreToolUse payload for Bash ls and observe hookSpecificOutput.permissionDecision === "allow" in the output.
Version
agent-bundle preview 886b192 (main); the code is unchanged at 284141958.
Summary
packages/agent-bundle/src/events/projection.ts(around line 759) projects atool/beforeresult for Claude/Codex as:So a route that returns
{ outcome: 'continue' }— the normal "I have nothing to say about this command" answer — is delivered to the host as an explicitpermissionDecision: "allow". Per the Claude Code hooks contract, an explicitallowbypasses the permission prompt for that tool call. Returningundefinedfrom the route does not help:parsedValueis undefined and the ternary still yields'allow'whenever the wrapper writes output.Impact
Any plugin with a
tool/beforeroute matchingBash(cargo-hauler's does, to rewritecargo …invocations) silently turns every other Bash call in the session —rm -rf,git push --force, anything — into an auto-approved call on Claude Code. Codex semantics unverified but the same field is emitted. This is a security regression introduced by the projection, not by plugin code, and a plugin cannot opt out today.Expected
outcome: 'continue'with noupdatedInputand noadditionalContext→ project to nopermissionDecision(or emit nohookSpecificOutputat all), leaving the host's normal permission flow intact.updatedInputpresent →permissionDecision: 'allow'is legitimate only if the contract requires it to apply the rewrite; if so, document that a rewrite implies approval, and consider a distinct route-level opt-in.denyunchanged.Repro
Any
src/events/tool/before.tsxreturning<Agent.Result value={{ outcome: 'continue' }} />; run the emitted Claude hook wrapper with a PreToolUse payload forBashlsand observehookSpecificOutput.permissionDecision === "allow"in the output.Version
agent-bundle preview
886b192(main); the code is unchanged at284141958.