auto-mode permission-deny envelope returned without intercepting Bash dispatch (silent-bypass on git push origin main, CLI 2.1.114, Opus 4.7)

Resolved 💬 3 comments Opened Apr 19, 2026 by jrjenkinsiv Closed Jun 17, 2026

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.deny and zero permissions.ask entries in ~/.claude/settings.json, ~/.claude/settings.local.json, or any project-scoped .claude/settings.json.
  • The user's PreToolUse guard.sh script had no rule for plain git push origin main at the time (rule 4b was added later as PR mitigation).
  • /tmp/claude-hook-events-${SESSION_ID}.jsonl has no hook entry for this tool_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:

  1. Real deny — the envelope is returned to the model AND the shell dispatch is intercepted. (Expected behavior.)
  2. 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_decision field 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) to block (exit 2 + stderr) for git 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-dev ticket (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!

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗