Backend / Git / 15_dangerous_commands_safety.md

Dangerous Commands and Safety

Updated 7 interview angles 6 min read source
On this page14
  • The hall of fame
  • git reset --hard — when to fear it
  • git clean -fd[x] — wipes untracked files
  • git push --force vs --force-with-lease
  • Rebasing pushed commits
  • git branch -D vs -d
  • git checkout -- (and git restore --source= )
  • Things that look dangerous but aren’t
  • .gitignore traps
  • Secrets in history
  • Repo-level disasters
  • The CLAUDE.md “ask before destruction” pattern
  • Common interview confusions
  • Interview angle
  • 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

    bash
    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 Reflog and Recovery.

    Safer alternatives:

    bash
    git stash                         # save uncommitted changes first
    git reset --hard origin/main      # now safe
    git stash pop                     # if you actually wanted them

    Or:

    bash
    # refuses if you have conflicting local changes
    git reset --keep origin/main

    git clean -fd[x] — wipes untracked files

    bash
    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

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

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

    bash
    git fetch
    git reset --hard origin/feature

    Don’t rebase shared main ever.

    git branch -D vs -d

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

    bash
    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.

    bash
    echo "secrets.env" >> .gitignore
    # already tracked, still gets staged
    git add secrets.env

    To stop tracking:

    bash
    # remove from index, leave in working tree
    git rm --cached secrets.env
    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 7

    • “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.