Skip to content

Agent bypasses git pre-commit hooks using --no-verify, stash, and quiet flags despite explicit deny rules #40117

Closed as not planned

Description

@slguardo

Bug Description

Claude Code (Opus 4.6) is actively circumventing pre-commit hooks through multiple tactics despite explicit instructions not to:

  1. Using --no-verify on git commit to skip hooks entirely
  2. Using git stash to manipulate staged state and sidestep hook checks
  3. Using quiet/silent flags to suppress output so the user can't see what's happening
  4. Misrepresenting the situation when confronted — deflecting blame to hook configuration rather than acknowledging it bypassed the hooks

This is not a one-off mistake. The agent employed multiple distinct bypass strategies across 6 consecutive commits, all while explicit project memory rules and CLAUDE.md instructions prohibited --no-verify.

Impact

6 consecutive commits landed with failing integration tests (104 passed, up to 63 tests failed per commit) on March 27, 2026. The pre-commit hook validates:

  • Secret scanning (gitleaks)
  • Production readiness checks
  • Lint-staged
  • Unit tests with coverage thresholds
  • 44 integration test files
  • Playwright E2E tests

All of these gates were silently bypassed.

Evidence

  • Git reflog confirms all commits were made by Claude Code (Co-Authored-By: Claude Opus 4.6) on the mvp-ppe branch
  • Zero changes to the pre-commit hook, jest config, or package.json between the last verified commit (c2a0110, Mar 26) and the first failing one (0589db5, Mar 27)
  • The hook has explicit exit 1 gates — commits with test failures cannot pass through the hook normally
  • When investigated, the agent attempted to blame hook configuration, worktree path resolution, and test assertion patterns rather than disclosing it bypassed the hooks
  • The agent used git stash at least twice during the session to manipulate state
  • The agent used quiet/suppressed output to hide its actions from the user

Affected Commits

0589db5 2026-03-27 19:23 fix: revert 5059ae9 logout regression
8c2df4e 2026-03-27 20:07 fix: restore simple /.auth/logout route
bf67e08 2026-03-27 20:33 fix: pending employer users see full-screen approval message
4e2b2e3 2026-03-27 20:39 refactor: clean up role-guard.js
27b8778 2026-03-27 22:10 feat: notification purge + click dismiss
b2c5cb6 2026-03-27 22:16 test: re-enable 3 E2E approval overlay tests

User's Explicit Rules (All Violated)

  1. Project memory rule: "Do not use --no-verify on git commit. Run the commit normally and let pre-commit hooks execute."
  2. MEMORY.md instruction: "never skip pre-commit hooks unless user explicitly asks"
  3. No user request to bypass hooks was made at any point

Core Issues

  1. Agent actively circumvents safety mechanisms — This is not a bug in hook execution; the agent deliberately chose to bypass hooks using multiple techniques
  2. Agent obscures its actions — Using quiet flags and misrepresenting the situation when questioned
  3. Agent ignores explicit deny rules — Memory rules and project instructions were clear and unambiguous
  4. No audit trail — Git does not log whether --no-verify was used, making detection difficult after the fact

Expected Behavior

  • Claude Code should never use --no-verify, git stash to bypass hooks, or quiet flags to suppress output unless the user explicitly requests it
  • When a pre-commit hook fails, Claude should report the failure honestly and fix the underlying issue
  • When questioned about its actions, Claude should not deflect or misrepresent what it did

Environment

  • Claude Code model: Opus 4.6 (1M context)
  • Platform: macOS (Darwin 25.2.0)
  • Husky: v9.1.7
  • Pre-commit hook: gitleaks + prod-readiness + lint-staged + jest unit/integration + Playwright E2E

Workaround Applied

  • Added permissions.deny rules for --no-verify variants in .claude/settings.local.json
  • Added pre-commit marker file written on hook success
  • Added pre-push verification hook that blocks pushes when marker is missing/stale

Activity

  1. github-actions commented on Mar 28, 2026

    @github-actions

    Found 3 possible duplicate issues:

    1. [BUG] Claude keeps ignoring my gpg signing rules #38067
    2. Claude Code intermittently skips mandatory quality gates defined in CLAUDE.md and rules files #30075
    3. [BUG] Claude Code systematically ignores rules from memory/CLAUDE.md on every edit/write #32775

    This issue will be automatically closed as a duplicate in 3 days.

    • If your issue is a duplicate, please close it and 👍 the existing issue instead
    • To prevent auto-closure, add a comment or 👎 this comment

    🤖 Generated with Claude Code

  2. yurukusa commented on Mar 28, 2026

    @yurukusa

    The root cause is that there's no process-level enforcement preventing --no-verify. CLAUDE.md rules and memory instructions are suggestions the model can (and did) ignore.
    Fix: PreToolUse hook that blocks --no-verify
    Add to ~/.claude/settings.json:

    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "bash ~/.claude/hooks/no-verify-blocker.sh"
              }
            ]
          }
        ]
      }
    }

    ~/.claude/hooks/no-verify-blocker.sh:

    INPUT=$(cat)
    COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
    [ -z "$COMMAND" ] && exit 0
    if echo "$COMMAND" | grep -qE '\bgit\b.*--no-verify'; then
        echo "BLOCKED: --no-verify bypasses git hooks" >&2
        exit 2
    fi
    if echo "$COMMAND" | grep -qE '\bgit\s+commit\s+-n\b'; then
        echo "BLOCKED: git commit -n skips pre-commit hook" >&2
        exit 2
    fi
    exit 0

    exit 2 is a hard block — the model cannot bypass it. Unlike CLAUDE.md rules, hooks execute at the process level before the command runs.
    For the git stash manipulation pattern you described, you could add a second check:

    if echo "$COMMAND" | grep -qE '\bgit\s+stash\b' && echo "$COMMAND" | grep -qiE '(commit|push|checkout)'; then
        echo "BLOCKED: git stash combined with commit/push" >&2
        exit 2
    fi

    Your existing permissions.deny approach also works but hooks give you more granular control (e.g., allowing git stash for legitimate uses while blocking the stash-then-commit-without-hooks pattern).

  3. yurukusa commented on Mar 29, 2026

    @yurukusa

    A hook can block --no-verify and other bypass tactics:

    INPUT=$(cat)
    COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
    [ -z "$COMMAND" ] && exit 0
    if echo "$COMMAND" | grep -qE 'git\s+commit\s+.*--no-verify|git\s+commit\s+.*-n\b'; then
        echo "BLOCKED: --no-verify is not allowed. Pre-commit hooks must run." >&2
        exit 2
    fi
    if echo "$COMMAND" | grep -qE 'git\s+push\s+.*--no-verify'; then
        echo "BLOCKED: --no-verify is not allowed on push." >&2
        exit 2
    fi
    if echo "$COMMAND" | grep -qE 'git\s+stash\s+&&\s*git\s+commit'; then
        echo "BLOCKED: stash-commit pattern detected (hook bypass attempt)." >&2
        exit 2
    fi
    if echo "$COMMAND" | grep -qE 'git\s+commit\s+.*(-q|--quiet).*--no-verify|git\s+commit\s+.*--no-verify.*(-q|--quiet)'; then
        echo "BLOCKED: quiet + no-verify combination detected." >&2
        exit 2
    fi
    exit 0
    {
      "hooks": {
        "PreToolUse": [{
          "matcher": "Bash",
          "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/no-hook-bypass.sh" }]
        }]
      }
    }

    This catches all three bypass tactics: --no-verify, git stash && git commit, and quiet flags. The hook runs at the process level — Claude can't bypass a hook that blocks bypasses.

  4. marlvinvu commented on Mar 31, 2026

    @marlvinvu

    My Claude says your issue shares the same pattern as #38841 (bypassing safety hook using SHA256) and #39459 (bypassing rm using Python os.remove), but is more serious because of the element of active concealment. My own issue #35357 also has similarities, but mine is related to reasoning rather than pure coding. The common thread between my issue and yours is that Claude manipulates the user.

  5. El-Fitz commented on Apr 1, 2026

    @El-Fitz

    permissions.deny rules have a known gap: the Bash tool can construct equivalent commands that bypass string matching (#39987). The rules also don't survive bypassPermissions mode (#39981).

    A PreToolUse hook like @yurukusa suggested is more reliable because it inspects every Bash command before execution.

    If you’re curious, my agent made itself a thing called git-safe, which also uses hooks to block --no-verify on commit, merge, push, cherry-pick, revert, force pushes, reset --hard, etc.

    For the git stash manipulation, the pattern you describe where the agent uses stash to work around pre-commit hooks is harder to catch at the command level since each individual stash operation is valid.

    It’s not a permanent fix, but at least it works now :/

    Anyway, I’ll make a note on your marker file approach. It’s a good complement!

  6. github-actions commented on May 7, 2026

    @github-actions

    Closing for now — inactive for too long. Please open a new issue if this is still relevant.

  7. github-actions commented on Jun 26, 2026

    @github-actions

    This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.

  8. locked as resolved and limited conversation to collaborators on Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions