In autonomous mode, Claude Code can run git push --force origin main and rewrite your shared branch history. This has happened to real users during overnight sessions.
Claude Code has full terminal access, including git. During autonomous sessions (especially at night), it can:
Deep night force-push to main
An autonomous Claude Code session force-pushed to main at 3am while the developer was sleeping. The team discovered rewritten history the next morning.
If your team has a hard rule — never push to main — there are four places you could put it. Only one of them cannot be skipped.
| Where you put it | What it actually gives you |
|---|---|
Project CLAUDE.md, marked "IMPORTANT" | Usually obeyed. In my own measurement it held every time — but it is still an instruction the model chooses to follow. |
Local CLAUDE.md | Same as above, and scoped to you alone — so it does not protect the team at all. |
| A PreToolUse hook that stops the push | Refused before the command runs. The decision is made by your shell, not by the model. |
A skill's reference.md | Loaded only when that skill is in play. A push can happen outside it. |
I used to sell these hooks with "rules in CLAUDE.md get skipped." Then I measured it, and my own claim did not survive. Across 39 trials on Claude Code 2.1.246 (two tasks, twelve conditions): with the prohibition written in CLAUDE.md, the forbidden action happened 0 times in 22 trials. Without it, 17 times out of 17. Length, position in the file, and competing instructions made no measurable difference.
So the honest framing is not "CLAUDE.md does not work." It is this:
A written rule gives you a usually that depends on the model. A hook gives you a cannot that does not.
For a rule your team treats as absolute — the kind where one violation rewrites shared history — usually is the wrong guarantee to buy. That is the whole argument for putting it in a hook, and it does not need CLAUDE.md to fail.
branch-guard.sh — blocks pushes to protected branches:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# A shell joins "\" + newline before running. grep is line-oriented, so without
# this a rule never sees `git push \` and `--force` as one line.
COMMAND=${COMMAND//\\$'\n'/ }
# git takes its own options BEFORE the verb: `git -C /repo push --force` does
# exactly what the plain form does. Strip them so every rule below can assume
# the verb sits next to the word `git`.
CHECK=$(printf '%s' "$COMMAND" | sed -E ':a;
s/(^|[;&|[:space:]])git[[:space:]]+((-C|-c)[[:space:]]+[^[:space:]]+|--(git-dir|work-tree|namespace|exec-path)=[^[:space:]]+|--(no-pager|paginate|bare))[[:space:]]+/\1git /g; ta')
# Force-push has three shapes, not one:
# --force / --force-with-lease
# bundled short flags -- `git push -uf origin feature` is a force push
# `git push origin +feature` rewrites the remote with no --force in it at all
# [^;&|]* keeps the scan inside the push itself, so `git push origin x && rm -f y`
# is not reported as a force push.
if printf '%s' "$CHECK" | grep -qE 'git\s+push\b[^;&|]*(--force\b|--force-with-lease\b|[^A-Za-z0-9]-[a-zA-Z]*f\b|[^A-Za-z0-9]\+[A-Za-z0-9._/*-]+)'; then
echo "BLOCKED: Force-push" >&2
exit 2
fi
# Push to a protected branch
if printf '%s' "$CHECK" | grep -qE 'git\s+push\b[^;&|]*\b(main|master)\b'; then
echo "BLOCKED: Push to protected branch" >&2
exit 2
fi
exit 0
exit 2 blocks the command at the process level. The model cannot ignore the block or argue its way past it.
The block is solid. The pattern is a different thing, and it is worth knowing where it ends before you rely on it.
/usr/bin/git push --force and env GIT_DIR=/r git push --force both reach git without the word git where the rule expects it.echo "never git push --force" >> CLAUDE.md, which is only talking about the command. The shipped hook separates mentions from invocations before any rule runs.If you need a guarantee rather than a good local filter, the complete answer is on the remote: branch protection. A hook protects the machine it is installed on. Branch protection protects the branch from every machine, including the ones you do not control.
This pattern was rewritten on 2026-09-03. The version this page carried before it missed four real force-pushes — git -C /repo push --force, git -c core.pager=cat push --force, git push origin +feature, git push -uf — and blocked git push --follow-tags, which is harmless. Both directions are covered by regression tests in the repository now.
npx github:yurukusa/cc-safe-setup
Blocks force-push, rm -rf, secret leaks, syntax errors, and more. 240+ test files 916 examples
GitHub · npm · Getting Started
git add .envnpx cc-health-check
Free 20-point diagnostic for your Claude Code setup.
Open source, no npm dependencies (needs jq). View source.
New: Hook if field — reduce overhead (v2.1.85)
Learn more: Production Guide · All Tools