Skip to content

fix(hooks): re-gate the destructive git push forms - #2734

Merged
cameronraysmith merged 1 commit into
mainfrom
fm/vx-restore-destructive-push-guards
Aug 17, 2026
Merged

fix(hooks): re-gate the destructive git push forms#2734
cameronraysmith merged 1 commit into
mainfrom
fm/vx-restore-destructive-push-guards

Conversation

@cameronraysmith

Copy link
Copy Markdown
Owner

Why

5aadf369 lifted the git push confirmation gate by deleting push_is_safe outright, so push auto-permits in every form — including the three that can destroy published history.
The stall that motivated the lift came from one form only, git push --force-with-lease <remote> <task-branch>, which fails safe by design: it refuses when the remote moved.
The removal was broader than the defect it fixed.

What changed

push_is_destructive restores the destructive subset rather than the original helper:

Form Decision
-f / --force / --force-with-lease naming main, master, or the resolved default ref ask
delete refspec — :branch and -d/--delete ask
--mirror, --all ask
--force-with-lease to a task branch auto-permit
any non-force push, including to main auto-permit

Two departures from the original push_is_safe narrow it: it escalated on any token naming the default ref and on --force-with-lease anywhere, which is exactly what stalled workers; here the default-ref test applies only in combination with a force flag.
One widens: -d/--delete auto-permitted in the original despite performing the same remote-ref deletion as the :branch refspec it did escalate, so both are gated as one class.
--tags is not restored — publishing tags destroys nothing.

A force-flavored push carrying no refspec keeps auto-permitting.
Which ref it rewrites depends on push.default and the checked-out branch, neither of which a PreToolUse hook can observe, and escalating it would re-stall a worker that force-with-leases its own tracked branch.

nix run/nix shell are untouched; that lift stands.

Limitation stated rather than fixed

The gate matches the raw command bytes of the tool call, so a push performed inside a script is invisible to it — the repository's own merge helper fast-forwards main by pushing to it and this arm never fired, because the tool call was the script name.
The hook comment and the SKILL.md paragraph now say so instead of claiming protection the gate does not provide.
The related raw-byte behavior 5aadf369's PR already reports, where protected words inside quoted strings and heredocs trip other arms, is likewise out of scope here.

No OpenSpec requirement binds this hook: openspec/specs/ contains no reference to it, and the only mention under openspec/ is an archived task narrating a jj git push invocation.

Generated artifact

Driving the writeShellApplication output built by home-manager-crs58:

resolved default ref here: main
git push --force origin main                       -> ask
git push -f origin main                            -> ask
git push --force-with-lease origin main            -> ask
git push --force-with-lease=main origin main       -> ask
git push --force origin master                     -> ask
git push --force origin HEAD:main                  -> ask
git -C /some/path push --force origin main         -> ask
git push origin :some-branch                       -> ask
git push origin --delete some-branch               -> ask
git push -d origin some-branch                     -> ask
git push --mirror origin                           -> ask
git push --all origin                              -> ask
git push --force-with-lease origin fm/some-branch  -> AUTO-PERMIT (no output)
git push --force origin fm/main-guards             -> AUTO-PERMIT (no output)
git push origin main                               -> AUTO-PERMIT (no output)
git push                                           -> AUTO-PERMIT (no output)

Testing

nix build .#checks.aarch64-darwin.hook-gate-dangerous-commands is the one oracle driving this script.
Its case table pinned "git push auto-permits in every form"; the case asserting git push --force origin main allows is inverted, and eighteen cases are added — twelve pinning each restored arm, six pinning what must keep auto-permitting, including --force-with-lease to a task branch in bare, =<expect>, and git -C forms, which is the regression guard for the defect this change must not reintroduce.

Severity was checked by replaying the push cases against both prior scripts.
All twelve escalation cases fail against 5aadf369's script, so they pin the restoration rather than restating it.
Eight of the auto-permit cases — the force-with-lease trio, bare push, push to main, HEAD:main, git -C push, and a branch merely containing "main" — fail against the pre-5aadf369 push_is_safe, so the regression guard pins a real difference; the --delete pair fails against both.

Also built home-manager-crs58 (the activationPackage consuming both edited nix files, which runs shellcheck over the script through writeShellApplication), plus package-apm-skills-compose and eval-md-format for the SKILL.md edit.
just check-fast was deliberately not run: nothing outside this script, its check, and the two documentation surfaces changed behavior.

5aadf36 lifted the git push confirmation gate by deleting push_is_safe
outright, so push now auto-permits in every form, including the three
that can destroy published history: a force push over the default ref, a
delete refspec, and --mirror, which deletes remote refs that are absent
locally. The stall that motivated the lift came from one form only --
`git push --force-with-lease <remote> <task-branch>`, the rebase-then-push
flow every agent worker runs -- and that form fails safe by design,
refusing when the remote moved. The removal was therefore broader than
the defect it fixed.

push_is_destructive restores the destructive subset rather than the
original helper. It escalates a force-flavored push (-f, --force,
--force-with-lease) whose refspec names main, master, or the resolved
default ref; a delete refspec in either the `:branch` or the -d/--delete
form; and --mirror/--all. Everything else keeps auto-permitting with the
NOTICE 5aadf36 introduced.

Two deliberate departures from the original push_is_safe, both narrowing:
it escalated on any token naming the default ref and on --force-with-lease
anywhere, which is what gated the worker flow; here the default-ref test
applies only in combination with a force flag. One departure widens:
--delete/-d was not in the original flag list and auto-permitted there,
though it performs the same remote-ref deletion as the `:branch` refspec
the original did escalate, so it is gated here as one class. --tags is
not restored; publishing tags destroys nothing.

A force-flavored push carrying no refspec is left auto-permitting. Which
ref it rewrites depends on push.default and the checked-out branch,
neither of which a PreToolUse hook can observe, and escalating it would
re-stall a worker that force-with-leases its own tracked branch.

The gate matches the raw command bytes of the tool call, so a push
performed inside a script is invisible to it -- the repository's own merge
helper fast-forwards main by pushing to it and this arm never fired,
because the tool call was the script name. The hook comment and the
SKILL.md paragraph now say so rather than claiming protection the gate
does not provide. Out of scope here, as is the related raw-byte behavior
5aadf36's PR already reports, where protected words inside quoted
strings and heredocs trip other arms.

No OpenSpec requirement binds this hook: openspec/specs/ contains no
reference to it, and the only mention anywhere under openspec/ is an
archived task narrating a `jj git push` invocation.

Testing: `nix build .#checks.aarch64-darwin.hook-gate-dangerous-commands`,
the one oracle driving this script. Its case table pinned "git push
auto-permits in every form"; the case asserting `git push --force origin
main` allows is inverted, and eighteen cases are added -- twelve pinning
each restored arm and six pinning what must keep auto-permitting,
including `--force-with-lease` to a task branch in bare, `=<expect>`, and
`git -C` forms, which is the regression guard for the defect this change
must not reintroduce. Severity was checked by replaying the push cases
against both prior scripts: all twelve escalation cases fail against
5aadf36's script, so they pin the restoration rather than restating it,
and eight of the auto-permit cases -- the force-with-lease trio, bare
push, push to main, and a branch merely containing "main" -- fail against
the pre-5aadf369 push_is_safe, so the regression guard pins a real
difference; the --delete pair fails against both. Also built
home-manager-crs58, the activationPackage that consumes both edited nix
files and runs shellcheck over the script through writeShellApplication,
plus package-apm-skills-compose and eval-md-format for the SKILL.md edit.
`just check-fast` was deliberately not run: nothing outside this script,
its check, and the two documentation surfaces changed behavior.
@cameronraysmith
cameronraysmith marked this pull request as ready for review August 17, 2026 05:24
@cameronraysmith
cameronraysmith merged commit ce9c780 into main Aug 17, 2026
6 checks passed
@cameronraysmith
cameronraysmith deleted the fm/vx-restore-destructive-push-guards branch August 17, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant