The git guard exists — it is auto-mode only
Claude Code's docs do name git reset --hard, git clean -fd and git checkout -- . as blocked by default, but only by the auto mode classifier: in Manual, dontAsk, or under a broad Bash(git *) allow rule nothing stops them, and /rewind cannot recover the work.
This advice is common and does not hold. The official page saying otherwise is linked below.
What this costs you if it is wrong
Ask and deny rules are prefix globs, not a security boundary: `Bash(git reset --hard*)` does not match `git -C /repo reset --hard`, a shell alias, or the same line buried in a script the model runs with `bash cleanup.sh`. Treat these rules as accident reduction. A blunt deny also blocks legitimate recovery work and nudges you into running the command by hand with less thought than the prompt would have given you, which is why `ask` is the better default here.
You copied the PreToolUse hook from the hooks docs. It greps the command for rm -rf and exits 2. You now believe destructive shell commands are covered, and you are running in Manual mode with Bash(git *) on your allow list because you got tired of approving git status.
That combination is the hole. The commands that actually erase unpushed work are not rm -rf. They are git reset --hard, git clean -fd, and git checkout -- . — commands that look like the safe, routine thing to run, so they slip past your attention as easily as the model's.
The correction
The widely repeated version of this tip says the official guidance only ever names rm -rf and never mentions the git commands. That is false, and the false version is worth correcting because it sends you off to build a guard you may already have.
The permission modes page lists, under what the auto mode classifier blocks by default: git reset --hard, git checkout -- ., git restore ., git clean -fd, git stash drop, git stash clear — described as commands the classifier presumes would discard uncommitted changes. The same page says Claude Code runs git status itself before a command that would discard uncommitted work, and shows the classifier whether staged, modified, or untracked work is present — reporting untracked files even when the repo sets status.showUntrackedFiles=no. The permissions page uses Bash(git clean *) as its worked example of an ask rule that survives subshells and command substitution.
So the knowledge is there. The gap is one of scope.
Where you actually stand, by mode
| Mode | What stops git reset --hard | The gap |
|---|---|---|
| Manual (default) | A permission prompt, like any command outside the read-only set | A broad allow rule such as Bash(git *) matches every git subcommand and approves it with no prompt |
| Auto | Classifier blocks it by default; broad allow rules are suspended on entry | A narrow allow rule still skips the classifier, so a destructive argument its prefix did not anticipate goes unseen |
| dontAsk | Everything that would otherwise prompt is auto-denied, and Claude Code "denies calls matching your explicit ask rules rather than prompting" | Your allow rules and PreToolUse approvals are the only routes through, so a broad Bash(git *) allow rule is the entire exposure |
| bypassPermissions | Deny rules, which "block in every mode, including bypassPermissions", and explicit ask rules, which no mode auto-approves | Allow rules have no effect here, so nothing you permit helps; only what you restrict does |
Two of those rows deserve reading twice. The permissions docs show Bash(git * main) matching git merge main and git push origin main — the wildcard stands in for the subcommand and every option before it. A Bash(git *) allow rule you added for convenience is a standing approval for git reset --hard.
And in auto mode, the narrow-rule hole has a named fix: setting autoMode.classifyAllShell suspends every shell allow rule for the session so the classifier sees every command. Requires v2.1.193 or later.
Rewind will not save you
The checkpointing page is unambiguous: checkpointing does not track files modified by bash commands, and those modifications cannot be undone through rewind. Only edits made through the file-editing tools are tracked.
git reset --hard is a bash command. So is git clean -fd. Every checkpoint you have is intact and none of them contains the work.
What to write
Prefer ask over deny. You genuinely need these commands sometimes, and a deny you have to keep working around gets deleted or bypassed.
{
"permissions": {
"ask": [
"Bash(git reset --hard*)",
"Bash(git clean *)",
"Bash(git checkout -- *)",
"Bash(git restore *)",
"Bash(git stash drop*)",
"Bash(git stash clear*)"
]
}
}Three things make ask and deny rules better than they look. They match past a leading variable assignment. They apply when any subcommand matches — including inside a subshell, a command substitution, or a loop body, so cd /tmp && git clean -f and echo "$(git clean -f)" both prompt. And they are the one lever that does not evaporate when you change mode: an explicit ask rule sits in the docs' list of actions no mode auto-approves, bypassPermissions included, and in dontAsk a matching ask rule becomes an outright denial rather than a prompt nobody is there to answer.
If you want the hook as well, widen the pattern the docs' example script greps for beyond rm -rf to cover git reset --hard, git clean -f, git checkout --, and the PowerShell Remove-Item -Recurse -Force form. Note that hooks and permission rules fail differently: a deny from a hook is authoritative in every mode including dontAsk, but the hook only sees the command string, same as the glob.
What is unverified here
Nobody has run this. The doc sentences above are quoted from the shipped documentation and are solid; the mode-by-mode table is derived from them rather than observed, and derivation is where errors hide. Specifically untested: whether a Bash(git *) allow rule really swallows git reset --hard in a live Manual-mode session, and whether a Bash(git reset --hard*) ask rule fires on git -C /some/path reset --hard — the documented anchoring says no, which is the whole reason for the caveat below.
The incident reports behind this tip are user-filed accounts of lost work, not reproductions. Version behavior in this area has moved repeatedly, and several of the guarantees above carry explicit minimum versions.
What would settle it: add the ask rules, open a scratch repo with one uncommitted file, and ask Claude to run each of the six commands plus a git -C variant. Whether you get a prompt is the answer.
Checkpoint
-
You are in Manual mode with
Bash(git *)allowed. What happens when Claude runsgit reset --hard HEAD~2?Show answerAnswer
It runs, with no prompt, and the uncommitted work goes with it.
Bash(git *)is a prefix glob whose wildcard stands in for the subcommand and every option before it — the permissions page showsBash(git * main)matching bothgit merge mainandgit push origin main— so the rule you added to stop approvinggit statusis a standing approval forgit reset --hard.What would have stopped it lives somewhere you are not. The permission modes page (
code.claude.com/docs/en/permission-modes) listsgit reset --hard,git checkout -- .,git restore .,git clean -fd,git stash dropandgit stash clearas blocked by default by the auto mode classifier — which is the page that refutes the widely repeated claim that the official guidance only ever namesrm -rf. That coverage is auto mode's, not Claude Code's in general, and Manual does not have it. Honest caveat: the mode-by-mode reading is derived from those sentences, not observed; nobody has watched a broad allow rule swallow this command in a live Manual session. -
Your work was destroyed by a bash command two prompts ago. Which
/rewindoption recovers it?Show answerAnswer
None of them. The checkpointing page is unambiguous: checkpointing does not track files modified by bash commands, and those modifications cannot be undone through rewind — only edits made through the file-editing tools are tracked.
git reset --hardandgit clean -fdare bash commands, so every checkpoint you have is intact and none of them contains the work. This is a case you have to prevent, not recover. -
Why does
Bash(git clean *)as an ask rule beat the same string as a deny rule for most people?Show answerAnswer
Because a deny you keep working around gets deleted or bypassed. You genuinely need these commands sometimes; a blunt deny blocks legitimate recovery work and nudges you into running the thing by hand with less thought than the prompt would have given it. An ask rule also survives a change of mode: no mode auto-approves an explicit ask rule,
bypassPermissionsincluded, and in dontAsk a matching ask rule becomes an outright denial rather than a prompt nobody is there to answer. What it is not is a security boundary — these are*globs, sogit -C /repo clean -f, an alias, or the same line inside a script the model runs withbash cleanup.shmay never match.