// FIELD NOTE
How to Actually Stop Claude Code from Committing to Main

Cory LaNou

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:
- Read the JSON input from stdin and extract the command
- Check if it's a git commit command
- Allow if creating a new branch first (git checkout -b)
- Check the current branch against protected branches list
- 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.
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
Create the hooks directory:
mkdir -p ~/.claude/hooks- Create the guard-branch.sh script (copy the code above)
- Make it executable:
bash chmod +x ~/.claude/hooks/guard-branch.sh
Update
~/.claude/settings.jsonwith the absolute pathRestart 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.


