git-guard: catch destructive-local ops (reset --hard, clean -f, checkout ., branch -D) #25
Labels
No labels
already-shipped
bug
documentation
duplicate
enhancement
external-review
good first issue
help wanted
in progress
invalid
needs-decision
proposal
question
security
wontfix
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: crenshawdev/cadence-archived#25
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Source: skills-analysis session, 2026-07-18 (comparison against Matt Pocock's
git-guardrails-claude-code).Cadence's
git-guard.mjsPreToolUse hook guards push (always ask) and commit on a protected branch (git.on_protected). It does not guard destructive-local commands that can irreversibly wipe uncommitted work:git reset --hard,git clean -f/-fd,git checkout ./git restore .,git branch -D.This is the one class of git command that destroys work with no recovery, and it's currently unguarded.
Do NOT adopt Matt's skill — his is a bash+jq substring-grep blocklist that false-triggers on
echo "git push"and can't tell a dangerous flag combo from a safe one. Cadence's parser is strictly better. This is an extension of the existinggit-guard.mjs, not a replacement.Approach:
gitSubcommands()(or add a sibling check) to recognize the destructive set, flag-aware:resetonly when--hardis present (soft/mixed reset is fine)cleanonly with-f/-fdcheckout/restoreonly when the pathspec is.or--branch -Dgit.on_destructive: ask | deny | allow(defaultask), mirroringon_protected.planningRootwalk-up); silent elsewhere.Benefit: the "oh no" git commands stop and ask before running, so a stray
reset --hardcan't silently vaporize uncommitted work.Scoped for v1.2.0.
Picked into the cycle after v2.0.0, as the requirement TOK-02, and it changed that requirement's shape rather than riding along with it.
TOK-02 was scoped as a straight rip-out: delete
lib/shell-tokens.mjs, its test file, andreferences/git.md's rail-3 grammar and out-of-grammar table, and go back to a small regex that asks on a plaingit push. The motivation was proportionality. Three review rounds in a row kept generating rail-3 evasion findings (&>,!(...), brace-in-subcommand), against a rail whose own documentation concedes it is "a detection widener, not a security boundary" and whose adversary is the model issuing the command.This issue is why the rip-out is wrong as scoped. It needs precisely the parsing the rip-out deletes:
resetonly on--hard,cleanonly on-f/-fd,checkout/restoreonly when the pathspec is.or--. A twenty-line regex cannot make those distinctions without becoming the substring blocklist this issue explicitly says not to adopt.And it guards the stronger case. Rail 3 guards
push, where a miss costs a recoverable remote commit. This guards the one class that destroys uncommitted work with no recovery. The parser is currently spent entirely on the weaker case, which is the actual proportionality complaint, stated more precisely than the rip-out stated it.So TOK-02 becomes a redirection, not a deletion. Rail 3's evasion grammar and out-of-grammar table go. What remains is a small flag-aware parser, and it is spent here:
git.on_destructive(ask|deny|allow, defaultask) firing onreset --hard,clean -f/-fd,checkout/restorewith a.or--pathspec, andbranch -D.Two things from this issue's own text are kept as written: ask rather than hard-block, and extend
git-guard.mjsrather than adopt an external blocklist. One thing changes: the framing here is "an extension of the existing parser". It is now a redirection of it, because the surface it was serving is being removed in the same requirement.Superseding TOK-01's rail-3 claim rather than extending it, which is also why the cycle ships as a minor rather than a patch.
Closing as decided against, not as done.
The destructive-local rail was built and then deliberately removed by TOK-02 in v2.2.0.
cadence-core/bin/lib/destructive-git.mjsandlib/shell-tokens.mjsare deleted along withgit.on_destructivefrom the config schema, andreferences/git.md's rail-3 grammar went with them — 2,251 lines in total.The reason was measurement, not principle. The scan was O(K x N) in memory: 3.1 GB at 224 KB of input, V8 abort at 280 KB, inside a PreToolUse hook that runs on every Bash call and fails OPEN. So a long enough command line switched the guard off entirely and let the operation inside it run unprompted — the guard was least reliable exactly when the command was most unusual. Alongside that, three consecutive blocking review panels each found new escape shapes behind
bash -c,$(...)and aliases, and the reader still went silent ongit switch -f main.What stands today is a ~30-line anchored reader (
lib/git-segments.mjs) guarding push and commit-on-protected only, with the missed shapes pinned as explicit test rows rather than left as an absence. It is a detection widener, not a security boundary, and it says so.Re-open if the trade is worth revisiting — but it should be re-argued from the performance finding rather than resumed as filed.
Verified against the tree at the v2.3.0 close:
destructive-git.mjsabsent, noon_destructivein the schema orcadence-core/bin.