Dangerous Commands and Safety
Git has commands that lose work permanently if used wrong. The good news: most are recoverable via reflog within ~90 days. The bad news: some really aren’t. This file is a checklist of what to fear and how to use the safer alternatives.
The hall of fame
| Command | What it can destroy |
|---|---|
git reset --hard |
uncommitted changes (no recovery) |
git clean -fd |
untracked files (no recovery) |
git push --force |
other people’s pushed commits (recoverable only if someone has them locally) |
git rebase on a shared branch |
other people’s commits (same as force-push) |
git branch -D <name> |
a branch (recoverable via reflog) |
git stash drop |
the stash (recoverable via fsck before GC) |
git checkout <commit> -- <file> |
uncommitted changes to that file |
rm -rf .git |
the entire repo (recoverable only from another clone) |
The pattern: anything with --force, -f, --hard, or that bypasses a safety check is the dangerous form.
git reset --hard — when to fear it
git reset --hard HEAD # discard all uncommitted changes
git reset --hard origin/main # match remote, discarding local commits AND uncommitted changes
Uncommitted changes are gone — they were never in the object database. Committed-then-reset commits stay in reflog (~90 days). See 05_reflog_recovery.md.
Safer alternatives:
git stash # save uncommitted changes first
git reset --hard origin/main # now safe
git stash pop # if you actually wanted them
Or:
git reset --keep origin/main # refuses if you have conflicting local changes
git clean -fd[x] — wipes untracked files
git clean -n # dry run — ALWAYS run this first
git clean -fd # delete untracked files + dirs
git clean -fdx # also delete .gitignored (kills .venv, node_modules, .env)
Untracked files have no recovery — they’re not in git’s object database. Habit: git clean -n first, every time. If the dry-run output looks right, then git clean -fd.
git push --force vs --force-with-lease
git push --force # overwrite remote with local, no questions asked
git push --force-with-lease # overwrite ONLY if remote hasn't moved since your last fetch
git push --force-with-lease=branch:expected-sha # even more explicit
--force-with-lease is the safer cousin. If a teammate pushed since your last fetch, the lease check fails and the push is refused — protecting their work.
--force |
--force-with-lease |
|
|---|---|---|
| Wipes remote unconditionally | yes | no |
| Detects “someone pushed since I fetched” | no | yes |
| Safe for branches you own | yes | yes |
| Safe for shared branches | no | safer, still avoid |
Rule: --force-with-lease always; --force never.
For protected branches (main), neither should be permitted by branch protection rules.
Rebasing pushed commits
If you’ve pushed feature and then rebased it locally:
git rebase main # rewrites history
git push --force-with-lease # required because history changed
Anyone who pulled the old feature will see weird duplicates next time they pull. Coordinate, or have them:
git fetch
git reset --hard origin/feature
Don’t rebase shared main ever.
git branch -D vs -d
git branch -d feature # refuses if not merged into upstream
git branch -D feature # force delete even if unmerged
-d (small) is safe — refuses to delete unmerged work. -D (capital) is the dangerous form.
Even after -D, the branch is recoverable via reflog (git reflog finds the SHA, git switch -c feature <sha> restores).
git checkout <commit> -- <file> (and git restore --source=<commit> <file>)
git checkout HEAD -- file.py # discard uncommitted changes to file.py
git restore file.py # modern equivalent
Wipes your uncommitted edits to that file. No recovery — they were never staged or committed.
Safer: stage first (git add file.py), then if you want to discard, you can git restore --staged --worktree file.py and at least the staged version was in the index briefly.
Things that look dangerous but aren’t
| Command | Why it’s actually safe |
|---|---|
git reset (default --mixed) |
only unstages — your file content stays |
git revert |
adds a new commit; nothing destroyed |
git rebase (without push) |
reflog has the original tip |
git merge |
adds a merge commit; nothing destroyed |
git stash |
saves to a hidden ref; recoverable |
git tag -d |
removes the tag locally; the commit it pointed to stays |
.gitignore traps
.gitignore does not untrack files that are already tracked.
echo "secrets.env" >> .gitignore
git add secrets.env # already tracked, still gets staged
To stop tracking:
git rm --cached secrets.env # remove from index, leave in working tree
git commit -m "stop tracking secrets.env"
If secrets.env was ever committed, it’s still in history. .gitignore from now on won’t help.
For really-secret leaks, you need history rewriting (git filter-repo) and a coordinated force-push, plus you must rotate the secret because the leaked version is forever in any clone made before the rewrite.
Secrets in history
If you commit a secret (API key, password, token):
- Rotate the secret immediately. Anyone with the commit (forks, mirrors, clones, GitHub indexers, archive.org) has it. The leak is permanent; you can’t un-leak.
- Then rewrite history with
git filter-repo --invert-paths --path secrets.envor BFG Repo-Cleaner. - Force-push to the remote (coordinate with team).
- Have everyone re-clone (their old clones still have the secret).
GitHub Secret Scanning catches common patterns and alerts. Pre-commit hook detect-private-key is the prevention.
Repo-level disasters
rm -rf .git deletes the entire history. Recovery options:
- Re-clone from origin if untouched and pushed.
- Restore from backup (file system, system snapshots).
- If both fail: gone.
Likewise git filter-repo rewrites entire history; do it on a clone, never on your only copy.
The CLAUDE.md “ask before destruction” pattern
Before any destructive operation, especially with AI tools, the rule is:
- Pause and confirm intent.
- Use the safer variant (
--force-with-leaseover--force,-dover-D). - Investigate before deleting unfamiliar state — it might be in-progress work.
- Prefer
revertoverreseton shared history. - Use
git stashas a cheap save-point before doing anything risky.
Common interview confusions
- “
git reset --hardpermanently deletes commits.” — committed commits stay in reflog (~90 days). Uncommitted changes are gone immediately. - “
--force-with-leaseis just a slower--force.” — different semantics: lease refuses the push if the remote moved since your last fetch. Safer. - “
.gitignorewill hide a tracked file.” — only affects future un-added files. Already-tracked files keep being tracked. Usegit rm --cached. - “Rotating the secret after a leak is optional if you rewrite history fast.” — the secret was public for some window. Anyone who pulled has it. Always rotate.
Interview angle
- “What’s the difference between
git push --forceand--force-with-lease?” —--forceoverwrites the remote unconditionally;--force-with-leaserefuses if the remote moved since your last fetch (protecting teammates’ pushes). Always prefer the lease form. - “You hard-reset and lost local commits — recoverable?” — yes via reflog, within ~90 days. Uncommitted changes are not recoverable.
- “You
git clean -fd’d and lost an untracked file — recoverable?” — no. Untracked files aren’t in git’s object database. Always rungit clean -nfirst. - “You committed a secret — what do you do?” — rotate the secret immediately (it’s leaked even if history is rewritten); then rewrite history with
git filter-repoand force-push; have everyone re-clone. - “How does
.gitignoreinteract with already-tracked files?” — it doesn’t..gitignoreonly affects future un-added files. Usegit rm --cached <file>to stop tracking; the file stays in history. - “What’s the safest way to keep a feature branch up to date with main?” — fetch, rebase locally,
push --force-with-lease. For shared/long-lived branches, merge instead of rebase to avoid wrecking collaborators. - “What’s the rule about destructive operations with AI agents (or junior devs)?” — never auto-run
--force,--hard,-D,clean -fdwithout explicit user confirmation in context. Investigate unexpected state before deleting it — could be someone’s in-progress work.