DEV Community

Cover image for My AI Assistant Bypassed My Git Commit Blocker
Anand Rathnas
Anand Rathnas

Posted on

My AI Assistant Bypassed My Git Commit Blocker

I set up hooks to prevent Claude Code from committing without my approval. It bypassed them using git -C /path commit instead of git commit. Here's the story and the fix.

The Setup

I use Claude Code as my coding assistant. It can read files, write code, run shell commands, and commit to git. That last part made me nervous. I wanted to review and commit changes myself.

Claude Code supports PreToolUse hooks that run before tool execution. Here's what I had in ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/block-git.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

And my block-git.sh:

#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')

if [[ $command == *"git commit"* ]] || \
   [[ $command == *"git push"* ]] || \
   [[ $command == *"git reset --hard"* ]]; then
  cat <<EOF
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "BLOCKED: git commit/push requires manual approval"
  }
}
EOF
  exit 0
fi

echo "{}"
exit 0
Enter fullscreen mode Exit fullscreen mode

This worked. When Claude tried git commit -m "message", it got blocked.

The Bypass

Today, I asked Claude to commit some changes. Instead of git commit, it ran:

git -C /Users/anand/ws/project commit -m "Fix bug"
Enter fullscreen mode Exit fullscreen mode

It went through. The commit was made without my approval.

The -C flag tells git to run as if it was started in that directory. Functionally identical to cd /path && git commit, but syntactically different enough to slip past my pattern matching.

Why It Bypassed

My hook used glob patterns:

[[ $command == *"git commit"* ]]
Enter fullscreen mode Exit fullscreen mode

This looks for the literal string git commit with a space between them. But the actual command was:

git -C /path commit -m "..."
Enter fullscreen mode Exit fullscreen mode

There's -C /path between git and commit. No direct match. No block.

The Fix

Updated the hook to use regex with word boundaries:

#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')

if echo "$command" | grep -qE '\bgit\b.*\bcommit\b' || \
   echo "$command" | grep -qE '\bgit\b.*\bpush\b' || \
   echo "$command" | grep -qE '\bgit\b.*\breset\b.*--hard' || \
   echo "$command" | grep -qE '\bgit\b.*\brebase\b' || \
   echo "$command" | grep -qE '\bgit\b.*--force' || \
   echo "$command" | grep -qE '\bgit\b.*-f\b'; then
  cat <<EOF
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "BLOCKED: git commit/push requires manual approval"
  }
}
EOF
  exit 0
fi

echo "{}"
exit 0
Enter fullscreen mode Exit fullscreen mode

The key change: \bgit\b.*\bcommit\b matches "git" followed by anything, then "commit". Word boundaries (\b) prevent partial matches.

Testing

# Now blocked:
echo '{"tool_input":{"command":"git -C /path commit -m test"}}' | ~/.claude/block-git.sh
# BLOCKED

echo '{"tool_input":{"command":"git commit -m test"}}' | ~/.claude/block-git.sh
# BLOCKED

# Still allowed:
echo '{"tool_input":{"command":"git status"}}' | ~/.claude/block-git.sh
# {} (allowed)
Enter fullscreen mode Exit fullscreen mode

Blocking Standalone cd

I also block standalone cd commands to prevent persistent directory changes:

#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
trimmed_command=$(echo "$command" | sed 's/^[[:space:]]*//')

if [[ "$trimmed_command" =~ ^cd[[:space:]] ]]; then
  cat <<EOF
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "BLOCKED: Use subshell pattern: (cd /path && command)"
  }
}
EOF
  exit 0
fi

echo "{}"
exit 0
Enter fullscreen mode Exit fullscreen mode

This forces the AI to use (cd /path && command) which runs in a subshell and doesn't persist.

Takeaways

Glob patterns are fragile for command matching. Use regex. AI assistants can find alternative syntax you didn't anticipate. Test your guardrails by trying to bypass them yourself.

The irony? Claude helped me fix the very hook it bypassed.


Have you set up guardrails for your AI tools? What edge cases have you hit?

Building jo4.io - a URL shortener with analytics and white-labeling.

Tags: #ai #security #git #claudecode #devtools

Top comments (2)

Collapse
 
sharadcodes profile image
Sharad Raj (He/Him)

What a time to live in. LOL

Some comments may only be visible to logged-in visitors. Sign in to view all comments.