Git hooks (.githooks/, copied into .git/hooks by npm install, where the sandbox can't rewrite them): pre-push rejects any push that would update refs/heads/main, however the command is spelled (bare push, +main, refs/heads/main, --all, --mirror, deletion). pre-commit and pre-merge-commit refuse commits while main is checked out. Covered by test/hooks.test.js.
Claude Code deny rules in .claude/settings.json for git push … main: an early stop only, since they match command text.
Gitea branch protection on main is the layer that can't be bypassed. It has to be set in the repo settings; the README documents all three.
Merge this before #1, whose workflow doc refers to these hooks.
Keeps `main` human-merge-only, in layers:
- **Git hooks** (`.githooks/`, copied into `.git/hooks` by `npm install`, where the sandbox can't rewrite them): `pre-push` rejects any push that would update `refs/heads/main`, however the command is spelled (bare push, `+main`, `refs/heads/main`, `--all`, `--mirror`, deletion). `pre-commit` and `pre-merge-commit` refuse commits while `main` is checked out. Covered by `test/hooks.test.js`.
- **Claude Code deny rules** in `.claude/settings.json` for `git push … main`: an early stop only, since they match command text.
- **Gitea branch protection** on `main` is the layer that can't be bypassed. It has to be set in the repo settings; the README documents all three.
Merge this before #1, whose workflow doc refers to these hooks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
christian
added 1 commit 2026-10-21 00:30:45 +02:00
Needs fixes Automated self-review by Claude; not an independent review.
The rules match command text, so many ways of pushing to main get past them:
.claude/settings.json:20 — A bare git push while on main (or with push.default=current) doesn't contain main, so it falls through to the ask rule.
.claude/settings.json:23 — HEAD:refs/heads/main, +main and +HEAD:main aren't matched. +main is also a force push that gets past the --force deny.
.claude/settings.json:20 — git push --all / --mirror update main without naming it.
.claude/settings.json:20 — git -C . push … / git -c k=v push … don't start with git push, so neither the deny nor the ask rule matches.
.claude/settings.json:22 — If :* is a raw prefix match, git push origin main-hotfix / maintenance would be hard-denied.
.claude/settings.json:13 — The existing --force deny only matches --force directly after push. -f, a trailing --force and +ref get past it.
.claude/settings.json:20 — Two of the four rules are redundant: * main covers origin main, and *:main covers HEAD:main.
Recommendation: treat these rules as a speed bump, not protection. The real guard is Gitea branch protection on main (no direct pushes, merge only via PR), which catches every variant above. A local pre-push hook that rejects refs/heads/main would be a second layer that doesn't depend on how the command is spelled.
**Needs fixes**
_Automated self-review by Claude; not an independent review._
The rules match command text, so many ways of pushing to `main` get past them:
1. `.claude/settings.json:20` — A bare `git push` while on `main` (or with `push.default=current`) doesn't contain `main`, so it falls through to the `ask` rule.
2. `.claude/settings.json:23` — `HEAD:refs/heads/main`, `+main` and `+HEAD:main` aren't matched. `+main` is also a force push that gets past the `--force` deny.
3. `.claude/settings.json:20` — `git push --all` / `--mirror` update `main` without naming it.
4. `.claude/settings.json:20` — `git -C . push …` / `git -c k=v push …` don't start with `git push`, so neither the deny nor the `ask` rule matches.
5. `.claude/settings.json:22` — If `:*` is a raw prefix match, `git push origin main-hotfix` / `maintenance` would be hard-denied.
6. `.claude/settings.json:13` — The existing `--force` deny only matches `--force` directly after `push`. `-f`, a trailing `--force` and `+ref` get past it.
7. `.claude/settings.json:20` — Two of the four rules are redundant: `* main` covers `origin main`, and `*:main` covers `HEAD:main`.
**Recommendation:** treat these rules as a speed bump, not protection. The real guard is Gitea branch protection on `main` (no direct pushes, merge only via PR), which catches every variant above. A local `pre-push` hook that rejects `refs/heads/main` would be a second layer that doesn't depend on how the command is spelled.
pre-push rejects any push that would update refs/heads/main, however the
command is spelled; pre-commit and pre-merge-commit refuse commits while
main is checked out. npm install enables them via core.hooksPath. The
README documents the three layers of protection for main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
christian
changed title from Deny Claude Code pushes to main to Protect main: git hooks + Claude Code deny rules2026-10-21 00:40:48 +02:00
christian
added 1 commit 2026-10-21 00:45:05 +02:00
Linking core.hooksPath to the tracked, sandbox-writable .githooks/ let a
sandboxed command rewrite a hook that git commit/push then run unsandboxed.
Copies in .git/hooks can't be changed from the sandbox. This also keeps
existing .git/hooks scripts working and lets npm install succeed outside a
git checkout. pre-merge-commit now names itself in errors; hook tests clean
up, ignore GIT_* env and cover the installer; docs state what each layer
does and doesn't stop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Needs fixes (stopping here: handing off to the human) Automated self-review by Claude; not an independent review.
Round 3 (after 24541f0). The round-2 findings are fixed, including the hook sandbox-escape (hooks are now copied into .git/hooks). New findings worth knowing:
scripts/install-hooks.sh:8 — Verified: if core.hooksPath is set globally, git rev-parse --git-path hooks returns that global directory, so the installer would put these hooks there and block main in every repo. It should copy to $(git rev-parse --git-common-dir)/hooks.
scripts/install-hooks.sh:14 — It overwrites an existing local pre-commit/pre-push hook without a backup.
package.json:10 — prepare fails all of npm install when .git/hooks isn't writable; it could warn and exit 0 instead.
.githooks/pre-commit:6 — It blocks the merge commit from git pull on a local main that has diverged.
The rest (deny-rule pattern gaps, stale hooks after removal, test isolation from global git config) are minor or already documented as limitations.
**Needs fixes** (stopping here: handing off to the human)
_Automated self-review by Claude; not an independent review._
Round 3 (after 24541f0). The round-2 findings are fixed, including the hook sandbox-escape (hooks are now copied into `.git/hooks`). New findings worth knowing:
1. `scripts/install-hooks.sh:8` — **Verified:** if `core.hooksPath` is set globally, `git rev-parse --git-path hooks` returns that global directory, so the installer would put these hooks there and block main in *every* repo. It should copy to `$(git rev-parse --git-common-dir)/hooks`.
2. `scripts/install-hooks.sh:14` — It overwrites an existing local pre-commit/pre-push hook without a backup.
3. `package.json:10` — `prepare` fails all of `npm install` when `.git/hooks` isn't writable; it could warn and exit 0 instead.
4. `.githooks/pre-commit:6` — It blocks the merge commit from `git pull` on a local main that has diverged.
The rest (deny-rule pattern gaps, stale hooks after removal, test isolation from global git config) are minor or already documented as limitations.
christian
added 1 commit 2026-10-21 00:55:53 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Keeps
mainhuman-merge-only, in layers:.githooks/, copied into.git/hooksbynpm install, where the sandbox can't rewrite them):pre-pushrejects any push that would updaterefs/heads/main, however the command is spelled (bare push,+main,refs/heads/main,--all,--mirror, deletion).pre-commitandpre-merge-commitrefuse commits whilemainis checked out. Covered bytest/hooks.test.js..claude/settings.jsonforgit push … main: an early stop only, since they match command text.mainis the layer that can't be bypassed. It has to be set in the repo settings; the README documents all three.Merge this before #1, whose workflow doc refers to these hooks.
🤖 Generated with Claude Code
Needs fixes
Automated self-review by Claude; not an independent review.
The rules match command text, so many ways of pushing to
mainget past them:.claude/settings.json:20— A baregit pushwhile onmain(or withpush.default=current) doesn't containmain, so it falls through to theaskrule..claude/settings.json:23—HEAD:refs/heads/main,+mainand+HEAD:mainaren't matched.+mainis also a force push that gets past the--forcedeny..claude/settings.json:20—git push --all/--mirrorupdatemainwithout naming it..claude/settings.json:20—git -C . push …/git -c k=v push …don't start withgit push, so neither the deny nor theaskrule matches..claude/settings.json:22— If:*is a raw prefix match,git push origin main-hotfix/maintenancewould be hard-denied..claude/settings.json:13— The existing--forcedeny only matches--forcedirectly afterpush.-f, a trailing--forceand+refget past it..claude/settings.json:20— Two of the four rules are redundant:* maincoversorigin main, and*:maincoversHEAD:main.Recommendation: treat these rules as a speed bump, not protection. The real guard is Gitea branch protection on
main(no direct pushes, merge only via PR), which catches every variant above. A localpre-pushhook that rejectsrefs/heads/mainwould be a second layer that doesn't depend on how the command is spelled.Deny Claude Code pushes to mainto Protect main: git hooks + Claude Code deny rulesNeeds fixes (stopping here: handing off to the human)
Automated self-review by Claude; not an independent review.
Round 3 (after
24541f0). The round-2 findings are fixed, including the hook sandbox-escape (hooks are now copied into.git/hooks). New findings worth knowing:scripts/install-hooks.sh:8— Verified: ifcore.hooksPathis set globally,git rev-parse --git-path hooksreturns that global directory, so the installer would put these hooks there and block main in every repo. It should copy to$(git rev-parse --git-common-dir)/hooks.scripts/install-hooks.sh:14— It overwrites an existing local pre-commit/pre-push hook without a backup.package.json:10—preparefails all ofnpm installwhen.git/hooksisn't writable; it could warn and exit 0 instead..githooks/pre-commit:6— It blocks the merge commit fromgit pullon a local main that has diverged.The rest (deny-rule pattern gaps, stale hooks after removal, test isolation from global git config) are minor or already documented as limitations.