Never push to main: where should that rule live so Claude cannot skip it?

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.

The Problem

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.

#36640

Where should the rule live?

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 itWhat 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.mdSame as above, and scoped to you alone — so it does not protect the team at all.
A PreToolUse hook that stops the pushRefused before the command runs. The decision is made by your shell, not by the model.
A skill's reference.mdLoaded only when that skill is in play. A push can happen outside it.

Honest version: CLAUDE.md is better than I used to say

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.

The Fix: branch-guard Hook

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.

Where this rule stops

The block is solid. The pattern is a different thing, and it is worth knowing where it ends before you rely on it.

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.

Install Branch Protection + 7 More Safety Hooks

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

Also Prevents

Check Your Safety Score

npx 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