backend / git / 15_dangerous_commands_safety.md

Dangerous Commands and Safety

7 interview angles 6 min read source

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):

  1. 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.
  2. Then rewrite history with git filter-repo --invert-paths --path secrets.env or BFG Repo-Cleaner.
  3. Force-push to the remote (coordinate with team).
  4. 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-lease over --force, -d over -D).
  • Investigate before deleting unfamiliar state — it might be in-progress work.
  • Prefer revert over reset on shared history.
  • Use git stash as a cheap save-point before doing anything risky.

Common interview confusions

  • git reset --hard permanently deletes commits.” — committed commits stay in reflog (~90 days). Uncommitted changes are gone immediately.
  • --force-with-lease is just a slower --force.” — different semantics: lease refuses the push if the remote moved since your last fetch. Safer.
  • .gitignore will hide a tracked file.” — only affects future un-added files. Already-tracked files keep being tracked. Use git 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 --force and --force-with-lease?”--force overwrites the remote unconditionally; --force-with-lease refuses 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 run git clean -n first.
  • “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-repo and force-push; have everyone re-clone.
  • “How does .gitignore interact with already-tracked files?” — it doesn’t. .gitignore only affects future un-added files. Use git 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 -fd without explicit user confirmation in context. Investigate unexpected state before deleting it — could be someone’s in-progress work.