auto-mode permission-deny envelope returned without intercepting Bash dispatch (silent-bypass on git push origin main, CLI 2.1.114, Opus 4.7)
Summary
In Claude Code CLI 2.1.114 with Opus 4.7, a Bash tool call to git push origin main returned a permission-denied tool_result (is_error: true) with the standard envelope, but the underlying shell command actually executed. The agent saw a denial and changed strategy; the push had already landed on the remote.
This is a silent-bypass class bug: any tool call that mutates remote state (push, API call, network write) and gets a denied-envelope-but-actually-ran response will leave the agent with an incorrect mental model of what's deployed.
Evidence (homelab dev environment, captured for repro)
- Session id:
f86d1392-e3ff-4842-bf3d-ebd9267cc4b5 - tool_use_id:
toolu_0121ToWtQueMK8ZgL6CETHTC - Timestamp:
2026-04-18T22:15:07Z - CLI version:
2.1.114 - Model:
claude-opus-4-7(1M context) - cwd:
/Users/jrjenkinsiv/Development/homelab-agents - Permission mode:
auto(default),skipAutoPermissionPrompt: true,Bash(*)in allow
The denial envelope text we received (verbatim):
Permission for this action has been denied. Reason: Pushing directly to the default branch (main) of the homelab-agents repo bypasses PR review; user did not authorize pushing to main. ... To allow this type of action in the future, the user can add a Bash permission rule to their settings.
That trailer ("To allow this type of action in the future, the user can add a Bash permission rule to their settings") is the recognizable Claude Code envelope. We did a thorough internal audit and confirmed the denial string is not produced by any user hook or settings file:
- Zero
permissions.denyand zeropermissions.askentries in~/.claude/settings.json,~/.claude/settings.local.json, or any project-scoped.claude/settings.json. - The user's PreToolUse
guard.shscript had no rule for plaingit push origin mainat the time (rule 4b was added later as PR mitigation). /tmp/claude-hook-events-${SESSION_ID}.jsonlhas no hook entry for thistool_use— the harness intercepted before dispatching to user hooks.
So the envelope was synthesized by the CLI / model policy layer itself.
We verified the command actually ran via git ls-remote origin main immediately after — commit 1adf4ea (the local HEAD at push time) was on origin/main. Worse: the agent had already created a feature branch from the same HEAD and pushed that too, assuming the original push had failed.
Hypothesis
The auto-mode "deny with reason" path appears to have two sub-paths:
- Real deny — the envelope is returned to the model AND the shell dispatch is intercepted. (Expected behavior.)
- Advisory-style — the envelope is returned to the model BUT the shell dispatch proceeds. (Observed in this incident.)
The model has no way to distinguish (1) from (2) without re-checking remote state, and most agents will treat the envelope as authoritative.
What we'd like
- Confirmation of whether the auto-mode policy layer can return the denial envelope without intercepting (a known knob, a regression, or an unintended path).
- If unintended: a fix so the envelope only fires when dispatch is genuinely blocked.
- If intended (advisory mode): a way for the model to detect it (e.g. distinct envelope text, a
permission_decisionfield in the result), so agents can verify side-effects rather than trust the denial.
Workarounds we shipped on our side
- We replaced the relevant hook from
ask(which uses the same envelope path) toblock(exit 2 + stderr) forgit push origin main. Exit-2 hooks are honored, no bypass observed. - We added a "harness fingerprint" detection rule to our guard-recovery skill: if a denial message ends with the trailer above and the command would have mutated remote state, verify execution state before trusting the denial.
- We filed an internal
agent-devticket (jrjenkinsiv/homelab-agents#44) for ongoing tracking.
We're happy to share more telemetry from the incident session if useful — JSONL transcript, hook event log, settings.json, full hook script contents.
Thanks!
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗