← Articles

// FIELD NOTE

How to Actually Stop Claude Code from Committing to Main

Cory LaNou

Cory LaNou

How to Actually Stop Claude Code from Committing to Main

How to Actually Stop Claude Code from Committing to Main

Overview

CLAUDE.md says "NEVER commit to main" but Claude ignores it anyway. Here's how to use hooks for system-level enforcement that Claude CAN'T bypass.

The Problem

We've all been there. You're working with Claude Code, you've got NEVER commit directly to main/master branch right there in your CLAUDE.md, and Claude commits to main anyway. It drives me crazy.

Why does this happen? Because CLAUDE.md is just guidance. It's soft enforcement that Claude can choose to ignore under certain conditions. When you're moving fast, when context gets long, when Claude decides it knows better - those instructions get skipped.

I work on a lot of open source projects, and there's nothing more frustrating than when Claude commits to main on someone else's repo. Then I have to unwind that mess, force push to undo, and hope nobody noticed.

The Solution: Claude Code Hooks

The fix is Claude Code hooks. Unlike CLAUDE.md instructions, Claude cannot ignore hooks. They intercept tool calls at the system level before they execute.

Here's how it works: 1. You create a bash script in ~/.claude/hooks/ 2. The script intercepts every git commit command 3. It checks if you're on a protected branch 4. If you are, it returns a deny decision 5. Claude must then create a feature branch instead

The Guard Branch Script

Create this file at ~/.claude/hooks/guard-branch.sh:

The full script is available in GitHub issue #56, but here's the key logic:

  1. Read the JSON input from stdin and extract the command
  2. Check if it's a git commit command
  3. Allow if creating a new branch first (git checkout -b)
  4. Check the current branch against protected branches list
  5. If on a protected branch, output a deny response

The deny response outputs JSON to stdout with a hookSpecificOutput object containing permissionDecision set to deny and a helpful permissionDecisionReason message.

Make it executable:

chmod +x ~/.claude/hooks/guard-branch.sh

Settings Configuration

Add the hook to your ~/.claude/settings.json. You need to configure a PreToolUse hook that matches the Bash tool and runs your guard-branch.sh script with a timeout of 5 seconds.

CRITICAL GOTCHA: Use the absolute path!

You cannot use ~/.claude/hooks/guard-branch.sh - the tilde doesn't expand in settings.json. You must use the full path like /Users/YOUR_USERNAME/.claude/hooks/guard-branch.sh.

This tripped me up for longer than I'd like to admit. See GitHub issue #56 for the complete settings.json example.

Setup Steps

  1. Create the hooks directory:

    mkdir -p ~/.claude/hooks
    
    1. Create the guard-branch.sh script (copy the code above)
    2. Make it executable: bash chmod +x ~/.claude/hooks/guard-branch.sh
  2. Update ~/.claude/settings.json with the absolute path

  3. Restart Claude Code - hooks are cached at session start

Key Gotchas We Discovered

During testing, we ran into several edge cases. Here's what we learned:

Problem Solution
Tilde (~) doesn't expand in settings.json Use absolute path: /Users/username/.claude/hooks/…
Old decision block format deprecated Use the new hookSpecificOutput format with permissionDecision deny
cd /repo && git commit on main not detected Extract target directory from command to check correct branch
git checkout -b feature && commit was blocked Detect checkout-before-commit pattern and allow it

Test Results

Here's what the hook catches (and what it allows):

Scenario Result
Direct commit to main BLOCKED
cd /repo && git commit (on main) BLOCKED
git checkout -b feature/x && git commit Allowed
git checkout feature/x && git commit Allowed
git checkout main && git commit BLOCKED
Non-git Bash commands Allowed (no interference)

Customizing Protected Branches

Want to protect more branches? Edit the PROTECTED_BRANCHES regex:

# Protect main, master, dev, staging, and any release branches
PROTECTED_BRANCHES="^(main|master|dev|staging|release.*)$"

What Happens When It Blocks

When Claude tries to commit to a protected branch, the hook fires and Claude sees:

BLOCKED: Cannot commit directly to ‘main'. Create a feature branch first: git checkout -b feature/your-feature-name

At this point, Claude typically asks if you want to create a feature branch, which is exactly what we want. Sometimes it might ask if you want to disable the hook - definitely don't do that.

Conclusion

Hooks beat instructions every time. When you need Claude to always follow a rule, don't rely on CLAUDE.md - use hooks for hard enforcement.

This simple script has saved me countless headaches on open source projects. No more accidental commits to main, no more embarrassing force pushes to undo mistakes.

The full code is available in the description. Set it up once, and never worry about Claude committing to main again.

Want more AI development insights?

Subscribe to the newsletter for weekly tips on using AI in professional development.

Subscribe to Newsletter

// KEEP READING

More articles