Purpose: Local git hooks that refuse to commit or push directly to
main. · Lang: POSIX sh · Deps: git (Husky optional) Copy to:.husky/(Husky) or.githooks/+core.hooksPath. · Use when: you (or an AI session using your credentials) can bypass branch protection on the remote, so the only guard that actually fires is local. · Related: secret-guard
Two hooks — a pre-commit and a pre-push — that abort when you're on
main (commit) or pushing to refs/heads/main (push). Pure POSIX sh + git,
zero runtime dependency; Husky is just a convenient installer.
GitHub branch-protection rules / rulesets run server-side, but a ruleset that lets the repo owner bypass it can't stop a push made with the owner's credentials — which is exactly the case when an AI coding session (Claude Code, etc.) pushes on your behalf. The server waves it through because it's "you."
The local pre-push hook is the last checkpoint that sees the push before it
leaves the machine, so it's the layer that can actually catch an accidental
git push while main is checked out, or a stray git push origin HEAD:main.
That's the protection this snippet provides.
pre-commit— reads the current branch (git rev-parse --abbrev-ref HEAD) and exits non-zero if it'smain. Stops a commit from landing onmainin the first place.pre-push— reads git's stdin (<local ref> <local sha> <remote ref> <remote sha>per line) and exits non-zero if anyremote_refisrefs/heads/main. Catchesgit push(branch tracking main),git push origin main, andgit push origin HEAD:mainalike.
Both print a ✋ message and the intentional-override command, then exit 1.
npm install --save-dev husky
npx husky init # creates .husky/ and wires the prepare script
cp scripts/branch-guard/pre-commit scripts/branch-guard/pre-push .husky/
chmod +x .husky/pre-commit .husky/pre-pushHusky v9+ runs these bare hook files as-is (no husky.sh sourcing line needed).
The prepare script husky init adds means the hooks activate on npm install
for every clone — this is the team-shareable path.
mkdir -p .githooks
cp scripts/branch-guard/pre-commit scripts/branch-guard/pre-push .githooks/
chmod +x .githooks/pre-commit .githooks/pre-push
git config core.hooksPath .githooks # per-clone; each dev runs this onceCommit .githooks/. core.hooksPath is local git config, so wire the one-line
git config into your bootstrap/make setup.
A repo has one file per hook, so chain them. With Husky, add the guard line
to the existing .husky/pre-commit; with plain hooks, call one from the other:
# in your existing pre-commit, before your own logic:
"$(dirname "$0")/branch-guard-pre-commit" || exit 1This pairs naturally with secret-guard (also a pre-commit):
one keeps secrets out of a commit, the other keeps commits off main.
git commit --no-verify # or: git commit -n
git push --no-verifyPair with a deny rule to make it a hard lock for an AI session. On its own this hook is a speed-bump:
--no-verifywalks right past it. To close that for an assistant working with your credentials, deny the bypass commands in your Claude Code settings so the assistant can't reach for them:Now the hook stops the accidental push (plain
git pushon main) and the deny rule stops the deliberate override — while you, working in your own terminal outside Claude Code's permission layer, keep--no-verifyfor the legitimate owner bypass. Note the deny patterns are prefix matches: a flag in a different position (git push origin main --no-verify) won't match, so keep the branch as the guard's job and treat the deny list as belt-to-its-suspenders, not the sole lock.
- Different default branch. Change
maintomaster/developin both files (the[ "$branch" = "main" ]test and therefs/heads/maincase). - Guard several branches. Widen the
caseinpre-push, e.g.refs/heads/main|refs/heads/release/*). - Message/tone. The
echolines are yours to reword; keep them on stderr (>&2) so they show even when git is quiet.