Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXV · ResetModes

Hard reset — the destructive mode and when it is acceptable

Advanced⏱ ~22 mingit

What you'll learn

  • Predict the state of HEAD, the index, and the working tree after `git reset --hard <commit>`
  • Identify the three things --hard destroys: commits, staged entries, and working tree bytes
  • Recognise the only acceptable use case: local-only mistakes on unpushed branches
  • Use `git reflog` to recover the previous HEAD after a mistaken --hard
  • Distinguish --hard from `git restore` for the working-tree-only discard case

Prerequisites

Practice

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

git reset --hard <commit> is the only reset mode that overwrites the working tree. It moves HEAD, resets the index, and rewrites every file in the working directory to match <commit>’s tree. Uncommitted edits are destroyed. Untracked files (anything Git has never been told about) are left alone, but everything tracked is replaced byte-for-byte with the version at <commit>. This is the destructive mode, and the production rule is unambiguous: --hard is acceptable only on local-only unpushed branches. On any branch that has been pushed, the safe undo is git revert, not git reset --hard.

What —hard does, precisely

A --hard reset performs three operations:

  1. The current branch pointer is rewritten to point at <commit>.
  2. The index is rewritten so every entry matches <commit>’s tree.
  3. Every file in the working tree that differs from <commit>’s tree is overwritten with the version from <commit>’s tree.
COMMIT=abc1234
git reset --hard $COMMIT
git status
# On branch main
# nothing to commit, working tree clean
git log --oneline -5
# tip is $COMMIT; the intervening commits are gone from history

After the command, the three trees are in lockstep with <commit>: HEAD points at it, the index matches its tree, and the working tree matches its tree. There are no unstaged changes, no staged changes, and no reachable commits between <commit> and the previous tip.

What —hard destroys

Three categories of work are destroyed by --hard:

  1. Uncommitted edits. Any modification, addition, or deletion in the working tree that is not yet committed is overwritten. The bytes are replaced with the version from <commit>’s tree. Untracked files (paths Git has never been told about) survive because they were never in the index to begin with.
  2. Staged changes. Any file in the index that does not match <commit>’s tree is unstaged by being overwritten in the index. The previous index entries are gone.
  3. Commits between the previous tip and <commit>. Any commit strictly between the previous branch tip and <commit> is now unreachable from any branch. The objects persist in .git/objects/, but they are eligible for garbage collection once the reflog entry that records them expires.

The first category is the one that loses work the engineer cares about right now. The second category is the one that makes --hard feel “thorough”. The third category is the one that makes --hard destructive on a shared branch.

flowchart LR
    A["previous tip"] --> B["undone commit"]
    B --> C["commit"]
    C --> D["target commit (--hard target)"]
    D --> E["parent of target"]
    style B fill:#fdd
    style C fill:#fdd
    style A fill:#fdd

The red-shaded commits are no longer reachable from any branch after --hard. They are not deleted; they are orphaned.

The only acceptable use case

git reset --hard is acceptable on:

  • A local-only feature branch that has never been pushed. The engineer is the only one with the branch, no one else has a remote-tracking ref pointing at the doomed commits, and the reflog retains the orphaned commits for 90 days.
  • A throwaway experiment. A branch created to try something, where the engineer has decided the experiment was wrong.

git reset --hard is unacceptable on:

  • main, master, release/*, or any default branch. Even if the engineer is the only one pushing, the default branch is read by every collaborator and every CI run.
  • Any branch that has been pushed to a shared remote. A --hard reset on a pushed branch must be followed by a --force push, which propagates the rewrite to every collaborator’s clone.
  • Any branch with in-flight pull requests. A --hard reset on the source branch of a pull request rewrites the commits the PR is based on, which silently invalidates the PR.

Recovery via reflog

The safety net for a mistaken --hard is the reflog. The reflog records every change to HEAD (and to every branch ref) in .git/logs/. After a git reset --hard <commit>, the reflog still contains an entry for the previous HEAD, marked as the moment the reset happened:

git reset --hard HEAD~3
# oh no, that was wrong
git reflog
# a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
# 7e8f9a0 HEAD@{1}: commit: terraform apply plan v3
# 1f2e3d4 HEAD@{2}: commit: bump ansible collection version
git reset --hard 7e8f9a0
# recovered

The reflog entry survives for the default 90-day window (configurable via gc.reflogExpire and gc.reflogExpireUnreachable). The recovery is a single git reset --hard <orphan-oid>. The orphan OID is the SHA printed by git reflog.

The recovery is not guaranteed. Two things can break it:

  1. Garbage collection has run and pruned the unreachable objects. git gc expires reflog entries per the configured policy. A --hard reset followed by git gc and the passage of the expiration window can make the orphan commits unrecoverable.
  2. The clone is fresh. A new clone does not inherit the reflog from the original clone; the reflog is local to each repository.

—hard versus git restore —hard

Git 2.23 added git restore to give the working-tree-only discard operation a clear name. The two commands are not the same:

git reset --hard HEAD~1       # rewind HEAD, reset index, rewrite WT
git restore .                 # rewrite WT to match index (no HEAD move)
git restore --source=HEAD~1 . # rewrite WT to match HEAD~1 (no HEAD move)

git reset --hard moves HEAD and overwrites the working tree. git restore overwrites the working tree but does not move HEAD. For the case “I want to discard my working tree edits but keep the current commit”, git restore is the right tool. For the case “I want to discard my working tree edits and rewind my commits”, git reset --hard is the right tool.

Production discipline

  1. Never --hard on a pushed branch. Use git revert for the commit-level undo and git restore for the working-tree-only undo. --hard is reserved for local-only mistakes.
  2. Verify with git status before --hard. A clean working tree means --hard will discard nothing important. An unclean working tree means --hard will destroy uncommitted edits; the right move is to commit or stash first.
  3. Commit the reflog recovery plan. When a --hard reset is necessary, the first follow-up is git reflog to record the pre-reset OID somewhere durable (a comment in the next commit, a ticket, a chat message). The 90-day window is not long in a long-running infrastructure repository.

Cross-course references

  • Git, CI/CD & GitOps — Part XIV (Revert) — the additive alternative on shared branches; revert undoes commits without rewriting history or touching the working tree.
  • Git, CI/CD & GitOps — Part VI (Resetting and restore) — the working-tree-only discard via git restore, the safer sibling of --hard.
  • Git, CI/CD & GitOps — Part XI (Rebase)--hard and interactive rebase share the history-rewrite property; both are unsafe on pushed branches.
  • Linux for Production Sysadmins — Part XXVII (Backup and Recovery) — the analogue is rm -rf versus mv to a holding directory: the destructive tool is faster in the moment and more expensive in every other moment.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer has just pushed a commit to a shared `main` branch and realises the change was wrong. The team needs the change undone. What is the production-safe command?

  2. Q2. `git reset --hard <commit>` overwrites the working tree bytes for every tracked file whose tree entry differs between `<commit>` and the current working tree.

  3. Q3. What is the safety-net command for recovering the previous HEAD after a mistaken `git reset --hard`, and what is its default retention window?

  4. Q4. Recover from a mistaken `git reset --hard` on a feature branch that lost two hours of working tree edits, using the reflog.

    An engineer ran `git reset --hard HEAD~1` thinking the most recent commit was the one to undo, but the actual most recent commit was the one containing the two hours of work. The working tree was clean before the reset, so the changes from the undone commit are gone from all three trees. The engineer needs the previous HEAD back.

Passing score: 75%. Answers are checked in this browser.