Blog / git
Stop your coding agent committing straight to main
A git hook and a Claude Code hook that stop agents committing or pushing on main, and make them branch instead. Tested with Claude Code.
On this page
- Layer 1: a git hook that refuses commits on main
- Stop pushes to main as well
- Share the hooks with your team
- The gap: --no-verify
- Layer 2: a Claude Code hook
- What the agent does when it's blocked
- Other agents
- Where Joxo fits
- Frequently asked questions
- How do I stop Claude Code committing to main?
- Can an agent skip a git pre-commit hook?
- Are git hooks shared when someone clones the repo?
- Does the hook slow Claude Code down?
- Try it with your team
You tell the agent to "commit when it's done", and it does, on whatever branch it's sitting on. Usually that's main. If it can push, the half-tested change is now on everyone's main too, and the next person to pull gets it.
Asking nicely in CLAUDE.md or AGENTS.md helps most of the time, but an instruction can be forgotten in a long session. A check that runs every time is better. This post sets up two: a git hook that works for every agent and every person, and a Claude Code hook that stops the agent before it even tries and tells it what to do instead.
We tested both in a scratch repo with git 2.43 and Claude Code 2.1.318.
Layer 1: a git hook that refuses commits on main#
Git runs .git/hooks/pre-commit before every commit. If it exits with an error, there is no commit. Save this as .git/hooks/pre-commit:
#!/bin/sh
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
case "$branch" in
main|master)
echo "Commits on $branch are blocked. Make a branch first: git switch -c <name>" >&2
exit 1 ;;
esacThen make it runnable and try it:
chmod +x .git/hooks/pre-commit
git commit -am "on main"Commits on main are blocked. Make a branch first: git switch -c <name>On any other branch the commit goes through as normal. The message matters: agents read it, so tell them the next step, not just "no".
Stop pushes to main as well#
A commit on a branch can still be pushed to main with git push origin mybranch:main. A pre-push hook sees where each ref is going. Save it as .git/hooks/pre-push:
#!/bin/sh
while read local_ref local_sha remote_ref remote_sha; do
case "$remote_ref" in
refs/heads/main|refs/heads/master)
echo "Pushing to ${remote_ref#refs/heads/} is blocked. Push a branch and open a pull request." >&2
exit 1 ;;
esac
donechmod +x .git/hooks/pre-push
git push origin fix/x # goes through
git push origin fix/x:main # blockedPushing to main is blocked. Push a branch and open a pull request.
error: failed to push some refs to 'origin'If your host supports branch protection, turn that on too. It's the only check a local setting can't skip.
Share the hooks with your team#
Files in .git/ aren't committed, so each clone starts without them. Put them in a folder in the repo and point git at it:
mkdir -p .githooks
mv .git/hooks/pre-commit .git/hooks/pre-push .githooks/
git config core.hooksPath .githooksEach person (and each agent's own clone) runs the git config line once. Add it to your setup steps.
Worktrees are already covered. If you run each agent in its own git worktree, they all use the hooks of the repo they were made from. We made a worktree, committed in it, and the main repo's pre-commit hook blocked it there too. So one setup protects every agent working from that clone.
The gap: --no-verify#
Every git hook has the same way round it:
git commit -am "just this once" --no-verifyThat commit went through in our test. Agents rarely reach for it, but some do when a hook keeps failing and they want to finish the task. That's what the second layer is for.
Layer 2: a Claude Code hook#
Claude Code can run your own script before each tool call (a PreToolUse hook). If the script exits with code 2, the call is blocked and Claude gets your message as the reason. Save this as .claude/:
#!/bin/sh
# PreToolUse hook for Bash: block commits and pushes on main, and --no-verify anywhere.
input=$(cat)
case "$input" in
*--no-verify*|*"git commit -n"*)
echo "Don't skip the git hooks. Fix what they report instead." >&2
exit 2 ;;
esac
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
case "$branch" in
main|master)
case "$input" in
*"git commit"*|*"git push"*)
echo "You're on $branch. Make a branch first (git switch -c <name>), then commit there." >&2
exit 2 ;;
esac ;;
esac
exit 0Make it runnable with chmod +x .claude/, then add it to .claude/:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-main.sh" }]
}
]
}
}The hook gets the tool call as JSON on its input, so it matches on the command text. It's a plain text match, not a full parser: it can block a command that only mentions git commit in a message, which is the safe way round.
What the agent does when it's blocked#
We left a change on main, removed the git hook so only the Claude Code hook was in place, and asked:
claude -p "Commit the change to a.txt on main with the message 'update a again'. Use git directly. If something blocks you, quote the exact message you got in one line and stop." --allowedTools "Bash"A hook blocked the commit: "You're on main. Make a branch first (git switch -c <name>), then commit there."
I've stopped, so nothing is committed. `a.txt` is still modified and unstaged.In an earlier run, with both hooks in place and no "stop" in the prompt, we asked it just to commit the change. It took the hint:
Committed `update a` on a new branch, `update-a`, which I created because a hook blocked committing directly on `main`.That's the behaviour you want: the agent works on, on a branch, and main only changes through a review.
Other agents#
The git hooks work for every agent, because they all commit with git. Codex, Gemini CLI and Cursor also have their own hook or rule settings, with different names and formats. If yours has a pre-command hook, the same script works: block on main, block --no-verify, and print the next step. Either way, add one line to your instruction file so the agent knows the rule before it runs into it:
Never commit or push on main. Make a branch with git switch -c <name> first.Where Joxo fits#
On a team, each person's agent can follow different rules without anyone noticing. Joxo gives everyone's agents one shared set of team rules for the project, so "never commit on main" applies to all of them. Setup is one line pasted into your agent.
Frequently asked questions#
How do I stop Claude Code committing to main?#
Add a PreToolUse hook for Bash that exits with code 2 when the branch is main and the command contains git commit or git push. Claude reads the message and usually makes a branch instead.
Can an agent skip a git pre-commit hook?#
Yes, with git commit --no-verify. Block that flag in your agent's own hook, and use branch protection on your git host for pushes.
Are git hooks shared when someone clones the repo?#
No. Files in .git/ stay on your machine. Commit them in a folder like .githooks and have everyone run git config core.hooksPath .githooks once.
Does the hook slow Claude Code down?#
Not noticeably. It's a short shell script that runs before each Bash command and exits at once for anything that isn't a commit or a push.