Using `git stash` Safely
Stash is where most people bounce off git. It looks like a clipboard, behaves like something else, and silently does less than you expect. This page covers the mental model, the two traps that make it seem broken, and exactly how to get your work back when a stash pop conflicts.
Sanitized — no real hosts, paths, or project names. Continues the sensor-logger example and the three contributors from Pull-Commit-Push-on-a-Shared-Repo, which is where stash usually comes up: you have half-finished edits and need a clean tree to pull onto. Assumes the index and object-database model from Git-from-First-Principles.
Every command output below is from a real run, not from the manual.
The mental model: a stash is a commit
This is the fact that makes the rest make sense.
A stash is a real commit, parked off to the side, not on any branch. stash@{0} is a ref pointing at it — exactly like HEAD or a branch name points at a commit, and exactly as Git-from-First-Principles describes.
That single fact explains everything else:
- You can diff it:
git stash show -p stash@{0} - You can check files out of it:
git restore --source=stash@{0} -- sensor.h - It survives branch switches, resets, and failed merges — nothing you do in your working tree can destroy it.
- It’s a stack:
stash@{0}is the newest,stash@{1}the one before it.
It is not a clipboard, and it is not attached to your working tree. Once stashed, your work is banked in the object database, which per Undoing-Things-in-Git makes getting it back a retrieval problem rather than a recovery problem.
Two limits worth knowing up front: stashes are local only — they never push to a remote, so a stash is not a backup — and they don’t survive deleting the repo directory.
Trap 1 — stash ignores new files
This is the number one reason people say “stash didn’t work.”
git stash only stashes tracked files. A file git has never seen — one that shows as ?? in git status, in the sense Ignoring-Files-in-Git makes precise — is left sitting in your working tree:
$ git status --short
M sensor.h
?? calibrate.c
$ git stash push -m "wip"
$ git status --short
?? calibrate.c
calibrate.c is still there, so the tree is not clean. You stash, try to pull or switch branches, git still complains, and it looks like stash did nothing.
The fix is -u:
$ git stash push -u -m "wip: pin TMP-1 range"
$ git status --short
Empty. Genuinely clean. Use -a (--all) if you also need ignored files — build output, local env files — stashed as well, which is rarely what you want.
Habit worth forming: always
git stash push -u -m "what this is". The-usaves you from this trap; the-msaves you from a stack of anonymous stashes you can’t tell apart three days later.
Trap 2 — a conflicted pop does not drop the stash
You stashed, you pulled, and Ben had edited the same line — the probe-range change from Pull-Commit-Push-on-a-Shared-Repo. Now git stash pop conflicts:
$ git stash pop
Auto-merging sensor.h
CONFLICT (content): Merge conflict in sensor.h
On branch main
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: sensor.h
The stash entry is kept in case you need it again.
Read that last line. pop only drops the stash if it applies cleanly. On conflict, git keeps it:
$ git stash list
stash@{0}: On main: wip: pin TMP-1 range
Still there. Your work is safe. The file now has markers labelled by where each side came from, which is the one place git’s marker labels are genuinely helpful:
<<<<<<< Updated upstream
#define MAX_VALID 1023
||||||| Stash base
#define MAX_VALID 4095
=======
#define MAX_VALID 4095 /* TMP-1; see calibration table */
>>>>>>> Stashed changes
Three sections, and each label names an origin. Updated upstream is the branch — what you pulled, Ben’s switch to the 10-bit probe. Stashed changes is you, annotating the bound you thought was settled. Stash base in the middle is what you both started from, and it appears because you set merge.conflictStyle zdiff3 back in Start-Here-What-Git-Is; without that setting git shows you only the outer two and you have to infer the rest.
Reading it as intent rather than text: Ben changed the hardware, you documented the old assumption. Neither edit is wrong, and nobody’s work is in danger — these are the same conflict mechanics as Merging-Rebasing-and-Reading-History, only the labels differ.
Getting at the stashed version
Four options, in order of how much you want to fight.
1. Just look at it. Changes nothing:
$ git stash show -p stash@{0}
diff --git a/sensor.h b/sensor.h
--- a/sensor.h
+++ b/sensor.h
@@ -5,7 +5,7 @@
/* Raw ADC counts from a 12-bit probe. */
#define MIN_VALID 0
-#define MAX_VALID 4095
+#define MAX_VALID 4095 /* TMP-1; see calibration table */
#define SENSOR_OK 0
#define SENSOR_ERR_RANGE (-1)
2. Resolve the markers by hand. Usually right, since you generally want both changes:
# edit the file, delete the markers, keep what you want
git add sensor.h
git stash drop # <-- YOU MUST DO THIS. See below.
3. Take your version wholesale, ignoring the merge:
git restore --source=stash@{0} -- sensor.h
That pulls the file straight out of the stash commit — no markers, no merge. You have now thrown away the other side’s change to that file, so only do it when you know yours supersedes theirs. (git checkout stash@{0} -- sensor.h is the older spelling of the same operation; both work, and Undoing-Things-in-Git explains why restore exists.)
4. Bail out completely, then think:
git reset --hard HEAD # discard the half-merged mess
git stash list # stash@{0} still there — nothing lost
git reset --hard is safe here specifically because your work is banked in the stash commit. You are discarding a broken merge attempt, not your edits. That is the only circumstance on this page where that command is not a risk.
The drop you have to do yourself
A clean pop drops the stash for you. A conflicted pop does not — even after you resolve it:
$ git add sensor.h # resolved by hand
$ git stash list
stash@{0}: On main: wip: pin TMP-1 range
$ git stash drop
Dropped refs/stash@{0} (dd8c4b6d12510f4e08be2c2a466d84baeaec94d9)
Forget this and stashes pile up silently. That’s how people end up at stash@{7} with no idea what any of them contain.
The escape hatch: git stash branch
When the conflict is too tangled to be worth resolving in place:
git stash branch tmp1-range stash@{0}
This creates a branch at the commit you originally stashed from, applies the stash there, and drops it. There is no conflict, because it replays your changes onto the exact base they were written against. You then merge or rebase that branch normally, with your work as proper commits instead of a fragile stash entry.
This is the right move whenever a stash has sat around long enough for the branch to have moved on.
Command reference
| Command | Does |
|---|---|
git stash push -u -m "msg" | Bank tracked and untracked changes; clean the tree |
git stash list | Show the stack; stash@{0} is newest |
git stash show -p stash@{0} | View as a diff; changes nothing |
git stash pop | Apply newest and drop it — only drops on a clean apply |
git stash apply | Apply but keep the entry (safer; drop manually after) |
git restore --source=stash@{0} -- <file> | Restore one file’s stashed version verbatim |
git stash branch <name> stash@{0} | Replay onto its original base as a branch — no conflicts |
git stash drop stash@{0} | Delete one entry |
git stash clear | Delete all entries — no confirmation, no undo |
When in doubt, use
applyinstead ofpop.applyleaves the entry alone, so you can retry as often as you like and drop it once you’re satisfied.popis justapply+drop, and the drop is the only part that can bite.
When not to stash at all
Stash is for short interruptions — “pull this in, then carry on.” It is a poor filing cabinet: entries are near-anonymous, invisible to git log, local to one machine, and easy to forget across a reboot.
If your work is worth keeping for more than an hour, commit it on a branch instead:
git switch -c wip-tmp1-range
git add -A && git commit -m "wip: half-done TMP-1 range pin"
Now it’s in git log, it’s pushable, it’s reviewable, and you cannot lose it to a stray git stash clear. This is the same “commit early, commit ugly” policy Undoing-Things-in-Git argues for, and a wip: commit on a private branch costs nothing to tidy up later.
Next: Merging-Rebasing-and-Reading-History — what git is actually doing when it combines two lines of work, and why the conflict above had exactly the shape it did.