git reset hard is the command everyone eventually googles mid-panic, and
the honest answer is that "git reset" isn't one operation — it's a small family of commands
that all move a branch pointer, but disagree wildly on what else they touch. Some modes are
completely reversible. One mode can permanently delete uncommitted work in a fraction of a
second, with no confirmation prompt. This guide is a scenario-driven reference for the whole
git reset family: unstaging a file, resetting to an arbitrary commit, hard
resetting to match GitHub, and recovering when you got a mode wrong. If you specifically want
to undo your most recent commit and are deciding between --soft,
--mixed, and --hard for that one case, the
git undo last commit guide covers that scenario
end to end. This one covers everything git reset does beyond that.
Quick Answer: Every git reset Command at a Glance
If you just want to know how to reset hard, unstage a file, or reset to a commit right now, here are the six commands that cover nearly every real-world situation. Full explanations, including exactly what each one is safe to run against, follow in the sections below.
| Situation | Command | What it touches |
|---|---|---|
| Unstage a file, keep your edits | git restore --staged file | Index only |
| Undo the last commit, keep changes staged | git reset --soft HEAD~1 | Branch pointer only |
| Undo the last commit, keep changes unstaged | git reset HEAD~1 (mixed, the default) | Branch pointer + index |
| Discard the last commit and its changes entirely | git reset --hard HEAD~1 | Branch pointer + index + working tree |
| Reset to a specific older commit | git reset --hard commit-id | Branch pointer + index + working tree |
| Hard reset github: match a remote exactly | git fetch origin && git reset --hard origin/main | Branch pointer + index + working tree |
Notice the pattern: every mode moves the branch pointer, some also touch the index, and only
--hard touches your working tree files. That single axis — how far down the
stack a mode reaches — is the entire mental model behind git reset, and it's
covered properly in the next two sections. For the full official flag list, see the
git-reset
documentation.
What Does git reset Do? Moving a Branch Pointer
At its core, git reset does one thing: it moves where your current branch
points to. A branch in git is nothing more than a named pointer to a single commit — when
you commit, git creates a new commit object and moves the pointer forward to it.
git reset moves that pointer to a commit you specify instead, which can mean
going backward (undoing commits) or, less commonly, forward or sideways to a commit reachable
some other way.
Everything else git reset does is a side effect of which mode you choose, layered
on top of that pointer move. Run git reset with no commit argument at all and it
defaults to git reset --mixed HEAD — it doesn't move the pointer anywhere
(HEAD is already where you are), it just unstages everything currently staged.
That's the answer to "what does git reset do" when people run it bare and get confused that
nothing about their commit history changed: nothing was supposed to. Add a target — a commit
ID, a branch name, or a relative reference like HEAD~1 — and the pointer actually
moves, which is what undoes commits.
# Bare git reset: unstages everything, moves nothing
git reset
# Same thing, explicit
git reset --mixed HEAD
# With a target, the branch pointer actually moves
git reset HEAD~1
The command's power and its danger come from the same source: it directly rewrites what your
branch points to and, in --hard mode, what your files contain, without asking
for confirmation. There's no undo dialog. That's not a design flaw — git assumes you know what
you're targeting — but it's exactly why the reflog recovery workflow later in this guide is
worth reading before you need it, not after.
The Three Trees: HEAD, Index, Working Tree
Git tracks three separate representations of your project at once, and every reset mode is defined by exactly which of these three it updates. Once this model clicks, you stop needing to memorize which flag does what — you can derive it.
- HEAD — technically a pointer to your current branch, which in turn points to a commit. This is your last committed snapshot.
- The index (also called the staging area) — the snapshot that will become
your next commit if you run
git commitright now. This is whatgit addpopulates. - The working tree — the actual files sitting on your disk, the ones your editor has open.
Normal work flows left to right through these three trees: you edit files in the working
tree, git add copies the change into the index, and git commit
copies the index into a new commit, moving HEAD forward. git reset mode
selection is just choosing how far back through that pipeline you want to rewind:
--soft only rewinds HEAD, --mixed rewinds HEAD and the index, and
--hard rewinds all three. Git's own documentation calls this exact mental model
"Reset Demystified," and it's worth reading once in full — see the
Git Tools: Reset Demystified
chapter for the canonical explanation with its own diagrams.
git reset --soft vs --mixed vs --hard
With the three-trees model in place, the three main modes are almost self-explanatory:
# --soft: moves HEAD only. Index and working tree untouched.
# Changes from the "undone" commit land back as staged edits.
git reset --soft HEAD~1
# --mixed (the default if you omit the flag): moves HEAD and resets
# the index to match. Working tree untouched — edits become unstaged.
git reset HEAD~1
git reset --mixed HEAD~1
# --hard: moves HEAD, resets the index, AND overwrites the working
# tree to match. This is the only mode that can destroy uncommitted work.
git reset --hard HEAD~1 git reset soft head is the safest of the three to experiment with, because it
never touches a file on disk — worst case, you re-run git commit and you're back
where you started. The mixed reset sits in the middle: it unstages cleanly but leaves your
edits intact in the working tree, which is why plain git reset HEAD~1 is the
default behavior when you omit a mode flag entirely.
--hard is different in kind, not just degree. Because it overwrites the working
tree, running git reset --hard against a commit that's behind your current state
deletes any uncommitted edits sitting on top of it — instantly, silently, and with no
recovery path if those edits were never committed anywhere. That's the entire reason "git
reset soft vs hard" is such a common comparison search: the gap in risk between the two isn't
small, it's categorical.
A quick note on naming, since "git reset force" shows up in search fairly often: there is no
--force flag on git reset. People searching that phrase usually
mean --hard, which does force an overwrite of your working tree without asking —
or they mean git push --force, an unrelated command that forces a remote branch
to accept history it would otherwise reject. The two are easy to conflate because both mean
"stop checking and just do it," but they operate on completely different things: one rewrites
your local files, the other rewrites a remote branch.
Comparison Table: What Each Mode Touches
| Mode | HEAD / branch pointer | Index (staged) | Working tree (files) | Can lose uncommitted work? |
|---|---|---|---|---|
--soft | Moved | Unchanged | Unchanged | No |
--mixed (default) | Moved | Reset to match target | Unchanged | No |
--hard | Moved | Reset to match target | Overwritten to match target | Yes |
--keep | Moved | Reset to match target | Refuses if local changes conflict with the move | No — it aborts instead |
--keep is worth knowing even though it's rarely the first choice: it behaves like
--hard for files that don't conflict with the reset, but refuses to touch files
where your working-tree changes would collide with the target commit — an escape hatch when
you want most of a hard reset's behavior without the blind-overwrite risk. It's a narrow tool,
but it exists precisely because --hard's all-or-nothing overwrite is sometimes
more than you want.
git reset HEAD: Unstaging Files Without Losing Work
A large share of people who search "git reset head" aren't trying to undo a commit at all —
they staged a file with git add by mistake and want it back out of the index
without touching their edits. This is the single most common everyday use of
git reset, and it deserves to be treated as its own operation rather than a
footnote to commit-undo.
# Classic form: unstage one file, leave the edits in place
git reset HEAD path/to/file.js
# Unstage everything staged, leave all edits in place
git reset HEAD
# Modern equivalent (Git 2.23+), same result, clearer name
git restore --staged path/to/file.js
Both commands do the same thing here: they reset the index entry for that file back to how
it looked at HEAD, without touching the working-tree copy at all. Your edits are
never at risk in either form — this operation never reaches the working tree, which is exactly
why it's safe to run liberally. git reset HEAD~1 and git reset HEAD file
look similar but do fundamentally different things: the first moves the branch pointer and
undoes a commit; the second doesn't move HEAD at all, it just changes what's staged.
git restore is the newer, purpose-built command for this — it was split out of
git checkout and git reset in Git 2.23 specifically so that
"unstage a file" and "discard a file's edits" would each have one obvious, narrowly-named
command instead of overloading two commands that also do several other, unrelated things.
git restore --staged file unstages without touching the working tree — the same
outcome as git reset HEAD file. Drop --staged and
git restore file does something different and more destructive: it discards
working-tree edits, overwriting the file on disk to match the index. Confusing the two is an
easy mistake with a real cost, so it's worth reading twice before you run either against a
file you haven't committed.
git reset to a Commit ID or HEAD~n
Resetting isn't limited to the single commit right before your current one. You can reset HEAD to a commit any distance back, by relative offset or by exact commit ID, once you know which commit you're targeting.
# Find the commit you want to reset to
git log --oneline -10
# Reset three commits back, keeping your working tree files unchanged
git reset HEAD~3
# Reset to a specific commit by its SHA (a full or short hash both work)
git reset --hard a1b2c3d
# git reset to commit id, mixed mode — unstages but keeps the files as-is
git reset a1b2c3d HEAD~n counts commits backward along the first-parent line from your current
position — HEAD~3 means "three commits before where I am now," regardless of
what those commits contain. A commit ID targets one exact commit directly, which matters once
your history has merges, since HEAD~n counting gets ambiguous across merge
points while a SHA never does. git log --oneline is the fastest way to read off
the short hash you need before running the reset — always confirm the target commit is the
one you think it is before adding --hard to the command, since that's the point
where a wrong commit ID becomes a wrong, destructive reset.
Hard Reset to Match GitHub or Another Remote
This is the single most common reason developers land on "hard reset github" as a search: a local branch has drifted — through a bad rebase, a botched merge, or just an afternoon of experiments you don't want to untangle — and the fastest fix is to throw the local state away entirely and match whatever's on the server.
# 1. Fetch the remote's current state without touching your working tree
git fetch origin
# 2. Reset your local branch to match it exactly — pointer, index, and files
git reset --hard origin/main
# One-liner version of the same two steps
git fetch origin && git reset --hard origin/main git fetch updates your local copy of the remote's history
(origin/main) without changing your current branch or working tree at all —
that step is always safe to run. If the fetch fails or pulls from the wrong server, fix the
address before resetting: changing the origin
remote URL covers repointing after an org rename or host migration. The reset that
follows is where the destructive part
happens: git reset --hard origin/main moves your local main pointer,
index, and working-tree files to match origin/main exactly, discarding every
local commit, staged change, and uncommitted edit that isn't already reflected on the remote.
This is also the answer to "how to reset github" when what someone actually means is resetting their local copy of a GitHub repo, not anything on GitHub's servers themselves — a hard reset run locally never touches the remote at all, it only overwrites your machine. If you instead want the remote branch itself to match a local commit — the opposite direction — that's a force push, not a reset, and it's a much riskier operation because it rewrites history other people may already have.
Before running this against a branch with local commits you haven't pushed, check what you're about to lose:
# Commits that exist locally but not on the remote — these get discarded
git log origin/main..HEAD
# Uncommitted changes — these also get discarded by --hard
git status
If git log origin/main..HEAD comes back empty and git status is
clean, the hard reset changes nothing you'll miss. If either shows something, that's your
cue to stash, branch off, or copy the relevant file content out before you proceed — recovery
after the fact depends entirely on whether that work was ever committed, which the next
section on reflog recovery covers in detail.
Discarding All Uncommitted Changes Safely
Sometimes there's no remote involved at all — you just want to throw away every uncommitted edit in the working tree and get back to your last commit. The git reset hard command for that is simply:
# Discard every uncommitted change, staged or not, back to the last commit
git reset --hard HEAD
# Also remove untracked files git isn't tracking at all (new files, build output)
git clean -fd
# Preview what git clean would delete, without deleting anything
git clean -nd git reset --hard HEAD targets your current commit rather than an earlier
one, so it doesn't undo any commits — it only discards uncommitted edits and staged changes,
snapping the working tree and index back to exactly what's already committed. It's the
cleanest way to bail out of a messy in-progress edit that isn't worth salvaging.
git reset --hard never touches files git isn't tracking — new files you created
but never git added are invisible to it entirely, which surprises people who
expect a hard reset to be a full "clean slate" command. If you also want those gone,
git clean -fd is the separate command for it (-f forces the delete,
-d includes directories) — and it's worth running the -nd dry-run
variant first, since git clean has no reflog safety net at all: files it deletes
were never committed, so there's no commit object for git to recover them from later.
This is also a reasonable moment to reach for a plain visual diff tool rather than trusting
memory. Diff Checker, a free Chrome extension, opens a full-tab, Monaco-based editor with a
side-by-side or unified view — paste the file content you're about to discard into one pane
before you run --hard, and you have a snapshot to compare against afterward if
you second-guess the decision. It has no git awareness of its own; it's just a fast way to
keep a visual copy of text you're about to overwrite.
git reset vs git restore vs git revert vs git checkout
Git has accumulated four commands that all sound like they might do the same thing, largely
because git checkout historically did far too much and git reset
still does more than its name suggests. Here's the decision matrix that actually resolves
which one you want:
| You want to... | Command | Moves branch pointer? | Rewrites history? |
|---|---|---|---|
| Unstage a file | git restore --staged file | No | No |
| Discard working-tree edits to a file | git restore file | No | No |
| Undo commits on a private, unpushed branch | git reset (soft/mixed/hard) | Yes | Yes — commits disappear |
| Undo a commit already pushed / shared | git revert commit-id | Yes — forward, adds a new commit | No — adds history, doesn't remove it |
| Switch to a different branch | git switch branch | Yes | No |
The generational shift matters here: git switch and git restore
were both introduced in Git 2.23 (2019) specifically to split git checkout's
overloaded behavior — switching branches and restoring files — into two narrower, harder to
misuse commands. git reset predates both and still hasn't been split up, which
is exactly why it retains the widest blast radius of the four: it's the only one of these
commands that can both move your branch pointer and overwrite your working tree in
the same call.
git revert vs git reset is the comparison worth internalizing above the others:
revert adds a brand-new commit that undoes the effect of an earlier one, leaving
the original commit and the branch's public history intact — safe on shared branches because
nobody else's history gets rewritten out from under them. reset moves the branch
pointer and can make earlier commits unreachable from that branch entirely, which is only
safe on branches nobody else has already pulled. The full mechanics of git revert,
including reverting a merge commit and reverting a revert, are covered in the
git revert commit guide.
Two naming traps worth flagging explicitly, the way "git rm branch" trips people up elsewhere
in git: there is no git revert --hard — --hard is a reset-only flag,
and revert has no equivalent because it never touches your working tree or index in the first
place, it just makes a normal new commit. Likewise there is no git restore --hard
— restore's whole design point is being narrower than reset, and "hard" specifically means
"also overwrite the working tree," which for a command whose entire job is already the
working tree would be redundant. If you see either phrase in a tutorial, it's describing
git reset --hard, not restore or revert.
Recovering From a Bad Reset with git reflog
Here's the fact that should calm you down immediately after a reset you regret: git
reflog records every position HEAD has occupied recently, including the
one right before your reset ran. Undoing a reset is, in most cases, just running another
reset back to that recorded position.
# Show recent HEAD positions with SHAs and timestamps
git reflog
# Example output — look for the entry right before "reset:"
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# f4e5d6c HEAD@{1}: commit: fix payment retry logic
# Move HEAD back to the commit that existed before the reset
git reset --hard f4e5d6c Undo git reset soft is the easiest recovery case of all: because
--soft never touched your working tree, reflog recovery is exact — you get back
precisely the staged state you had a moment ago, no data was ever at risk to begin with. A
mixed reset recovery is also complete, since it only rearranged where changes sat between the
index and working tree, never deleting content from either.
Here is the honest limit, and it's the one detail most guides gloss over:
reflog recovery only restores committed work — commit objects that git had already
saved to .git/objects before the reset ran. The reflog only tracks where
HEAD and branches pointed, never the contents of an uncommitted working tree, so
for uncommitted edits there is simply no reflog entry to return to.
But "uncommitted" splits into two very different cases, and confusing them is why people give up on recoverable work:
- Staged but never committed — often recoverable. The moment you run
git add, git writes the file's contents into.git/objectsas a blob. A hard reset moves pointers and overwrites files, but it does not immediately delete that blob — it just leaves it unreferenced, or dangling. Until garbage collection prunes it, you can still fish it out. - Never staged, never committed — genuinely gone. If you edited a file and
ran
git reset --hardwithout ever runninggit add, git never had a copy of those bytes anywhere. No command recovers them. This is the one case where the data is truly unrecoverable.
To recover staged work after a hard reset, search the object database for dangling blobs
rather than the reflog. git fsck lists unreferenced objects, and
git show prints the contents of any blob you find:
# List unreferenced objects — staged-but-lost content shows up as "dangling blob"
git fsck --lost-found
# Inspect a candidate blob's contents (use the SHA from the output above)
git show a1b2c3d
# Write it back to a file once you've identified the right one
git show a1b2c3d > recovered-file.js
On a busy repository git fsck can list many dangling blobs with no filenames
attached — git stores blob contents, not paths, so identification is manual. Dumping the
candidates and diffing them against the current file is usually the fastest way to find the
right one, and it is a legitimate use for a side-by-side comparison tool: paste the recovered
blob into one pane and the current file into the other, and the correct match is obvious in
seconds. This route only exists for content that was staged at least once, which is a
practical argument for running git add early and often.
The one thing that would have saved them is git stash, run before the reset —
stashing creates real commit objects for your uncommitted changes, which is precisely why
stashed work survives a hard reset that would otherwise destroy it. That's the practical
takeaway: if you're not certain a hard reset is safe, git stash costs three
seconds and turns an unrecoverable mistake into a recoverable one.
# Before an uncertain hard reset, stash first — now the changes are a commit object
git stash
# Run the reset with confidence
git reset --hard origin/main
# If you needed the stashed changes after all, they're still there
git stash pop
For committed work, the recovery window is git's garbage collection schedule, not indefinite
— typically 30 days for entirely unreachable commits and 90 for commits still reachable
through some other ref, both configurable and both shorter if someone has run
git gc --prune=now on the repository. Recover sooner rather than later.
Why You Should Not Reset a Shared Branch
Every warning in this guide about --hard being safe to undo via reflog comes
with an unstated assumption: it's safe for you, on your machine. Reset a
branch that other people have already pulled, and their local history now disagrees with
yours — the commits your reset made unreachable are still sitting in their clones, and the
next time they push, git rejects it or, worse, someone force-pushes over your reset and the
confusion compounds.
This is the actual reason git push --force shows up so often paired with
git reset --hard in tutorials: after resetting a local branch backward, a normal
push fails, because the remote has commits your local branch no longer does. Forcing the push
is how the reset propagates outward — and force-pushing a shared branch is exactly the moment
a private, locally-recoverable reset becomes a team-wide incident, since everyone else's
unpushed work built on top of the discarded commits is now orphaned on their end too.
If what you actually need is to rewrite commit history — squashing, reordering, or editing
older commits — rather than simply discarding the tip, git rebase is usually the
better-fitting tool. There's no such thing as "git rebase hard" — rebase has no
--hard flag, because rebase already rewrites history by design; the caution that
applies to reset --hard on shared branches applies to any rebase on a shared
branch for the identical reason. The safe default for both: keep destructive history
rewrites, reset or rebase, confined to branches only you have pushed to, or that you've
explicitly coordinated a rewrite on with everyone who has a clone.
Once a branch's purpose is genuinely finished — merged and no longer needed locally or on the remote — deleting it outright is usually cleaner than resetting it back to some earlier state. The git delete local branch guide covers the full local-vs-remote deletion mechanics, including how to bulk-clean a pile of merged branches safely.
Verify What You're About to Destroy
Every destructive command in this guide has the same shape: it's instant, it's silent, and it
gives you no preview by default. Building a two-second verification habit before any
--hard command costs almost nothing and catches the mistakes that reflog can't
fix — the uncommitted-edit case from the recovery section above.
# Always run before --hard: what's staged, what's modified, what's untracked
git status
# What local commits would a hard reset to origin/main discard
git log origin/main..HEAD --oneline
# The actual content of a commit before you reset past it
git show HEAD --stat
For anything more than a glance — a file with a lot of uncommitted edits, or a commit you
want to confirm the exact line-level content of before it disappears — reading raw
git diff or git show output line by line is slower and more
error-prone than a real diff view. Pasting the two versions of a file into
a proper side-by-side comparison makes it
obvious at a glance whether the content you're about to discard is truly disposable, and Diff
Checker's local history (stored in IndexedDB, never leaves the browser) is a reasonable place
to stash a copy of a file's current state before you run a reset you're not fully sure about —
a lightweight safety net that costs nothing and sits entirely outside git's own recovery
mechanisms.
If you're comparing raw command-line output rather than files — two git log
dumps, or a before-and-after git status — the same
unified diff format git itself uses under the hood is
worth understanding, since it's what git diff prints by default and what most
patch-based tooling expects.
Frequently Asked Questions
What does git reset do?
git reset moves the current branch pointer to a different commit and,
depending on the mode you pick, updates the staging area (index) and working tree to
match. Run it with no arguments and no path and it defaults to
git reset --mixed HEAD, which unstages every staged file without moving the
branch pointer or touching a single line in your working tree — it only changes what's
staged for the next commit. Add a commit reference like HEAD~1 or a SHA and
it also moves the branch pointer, which is how git reset undoes commits. Add
--hard and it also overwrites your working tree files to match, which is the
only mode that can destroy uncommitted work.
How do I unstage a file without losing my changes?
Run git restore --staged <file> (Git 2.23+) or the older equivalent
git reset HEAD <file>. Both remove the file from the staging area and
move it back to being an unstaged modification — your edits stay exactly as they were in
the working tree, nothing is deleted. This is different from undoing a commit: you're not
moving the branch pointer at all, just changing what's queued for the next commit. If you
actually want to discard the edits themselves rather than just unstage them, that's
git restore <file> (no --staged flag) or
git checkout -- <file>, and that one does overwrite your changes.
How do I reset my local branch to match GitHub exactly?
Run git fetch origin followed by
git reset --hard origin/main (swap in your actual branch name if it isn't
main). The fetch updates your local copy of the remote's history without touching your
working tree; the hard reset then moves your local branch pointer, index, and working
tree to match that remote history exactly, discarding any local commits, staged changes,
or uncommitted edits that aren't on the remote. This is the standard fix for a local
branch that's diverged, conflicted, or just accumulated a mess you don't want to untangle
by hand — but it is destructive, so check git status and
git log origin/main..HEAD first to see exactly what you're about to throw
away.
Can I undo a git reset --soft?
Yes, and it's the easiest reset to undo because --soft never touches your
working tree or staging area — it only moves the branch pointer, so your files never
changed. Run git reflog to find the SHA the branch pointed to before the
reset (it will show as the entry right before the "reset: moving to" line), then run
git reset --soft <SHA> or plain git reset <SHA> to
move the pointer back. Because nothing but the pointer moved in either direction, this
recovery is exact — you get back precisely the commit and staging state you had before.
What is the difference between git reset and git restore?
git restore, added in Git 2.23, is the modern, narrower command for
restoring files — it only ever touches the working tree and, with --staged,
the index; it never moves the branch pointer and it has no --hard mode.
git reset does everything restore does for unstaging files, but it can also
move HEAD to a different commit, which restore cannot do. In practice: use
git restore --staged <file> to unstage, use
git restore <file> to discard working-tree edits, and reach for
git reset only when you actually need to move the branch pointer, such as
undoing a commit or resetting to match a remote.