Mastering git stash
A deep dive into the use cases, workflow patterns, and hidden pitfalls of one of Git's most underappreciated commands.
Table of Contents
- What is git stash?
- How it works internally
- Core use cases
- Essential commands
- Advanced patterns
- Bottlenecks & pitfalls
- Stash vs. alternatives
- Quick reference cheatsheet
1. What is git stash?
git stash is a Git command that temporarily shelves (stashes) changes you've made to your working directory so you can switch context — then come back and re-apply them later. Think of it as a clipboard for your in-progress work.
It captures both staged and unstaged modifications, stores them on an internal stack, and restores your working directory to match the last commit — giving you a clean slate without forcing you to make a premature commit.
Note: The stash is local to your machine. It is never pushed to a remote, which makes it ideal for temporary, personal work-in-progress storage rather than sharing incomplete work.
2. How it works internally
Under the hood, git stash creates two (or three) commits in your repository's object store and records them as a special reference under refs/stash. The stash is structured as a merge commit.
Working tree (dirty) → git stash → Clean tree (HEAD) → Work elsewhere → git stash pop → Restored
Git stores three objects per stash entry:
- A commit for the index (staged changes)
- A commit for the working tree (unstaged changes)
- Optionally a third commit for untracked files when
-uis used
The stash reference forms a linked list — a stack — where stash@{0} is always the most recent entry.
3. Core use cases
Emergency hotfix
You're mid-feature when a critical production bug lands. Stash your work, fix the bug on main, release — then pop your stash and carry on.
git stash push -m "WIP: user auth refactor"
git checkout main
# fix the bug, commit, push
git checkout feature/auth
git stash pop
Branch switching
Git won't let you switch branches with a dirty working tree if the changes conflict. Stash eliminates the friction of making a temporary commit just to change context.
git stash
git checkout other-branch
# do your thing
git checkout original-branch
git stash pop
Experiment isolation
Run your test suite from a clean baseline. Stash your experimental changes, test, then restore — without polluting your commit history.
git stash push -m "experimental: new caching layer"
npm test # test against clean HEAD
git stash pop # restore experiment
Pulling upstream changes
Avoid merge conflicts during a pull by stashing local uncommitted changes first, pulling, then popping.
git stash
git pull --rebase
git stash pop
Wrong branch commit prep
Started work on the wrong branch? Stash the changes, check out the correct branch, and pop. Your changes land exactly where they should.
# Oops — we're on main
git stash
git checkout feature/correct-branch
git stash pop
Code review baseline
Reviewing a colleague's PR while mid-feature? Stash yours, check out their branch cleanly, review, then return to exactly where you left off.
git stash push -m "WIP: payment flow"
git fetch origin
git checkout pr/teammate-branch
# review their code
git checkout my-feature-branch
git stash pop
4. Essential commands
Saving a stash
# Basic stash — saves tracked modified files
git stash
# With a descriptive message (highly recommended)
git stash push -m "WIP: refactor auth middleware"
# Include untracked files
git stash push -u
# Include everything — tracked, untracked, AND ignored
git stash push -a
# Stash only specific files (partial stash)
git stash push -m "only auth" src/auth.ts src/middleware.ts
Listing and inspecting stashes
# List all stash entries
git stash list
# stash@{0}: On main: WIP: refactor auth middleware
# stash@{1}: On feature/payments: add stripe webhook handler
# stash@{2}: WIP on hotfix/login: fix null pointer
# Show what's in a stash (summary)
git stash show stash@{1}
# Show full diff of a stash
git stash show -p stash@{1}
Restoring a stash
# Pop: apply latest stash AND remove from stack
git stash pop
# Apply: apply but KEEP the stash entry (safer)
git stash apply stash@{2}
# Apply to a new branch (avoids conflicts on current branch)
git stash branch feature/new-branch stash@{0}
Removing stashes
# Drop a specific stash entry
git stash drop stash@{1}
# Nuke the entire stash list — irreversible!
git stash clear
5. Advanced patterns
Partial stashing with interactive mode
One of the most powerful but underused features is --patch mode, which lets you selectively stash only specific hunks within a file — not the entire file.
# Interactively choose which hunks to stash
git stash push --patch
# Prompt options:
# y = stash this hunk
# n = skip this hunk
# s = split into smaller hunks
# e = manually edit the hunk
# q = quit (stash what's been selected so far)
Preserving index state on restore
By default, git stash pop doesn't restore your staging area (index) — it dumps everything into the working tree as unstaged. Use --index to preserve what was staged vs. unstaged.
# Restore both working tree AND index state
git stash pop --index
# Or with apply (keeps stash entry in stack)
git stash apply --index stash@{0}
Creating a branch from a stash
If your stash would conflict with your current branch because the codebase has diverged, create a new branch from the commit that existed when you stashed:
# Branches from the stash's original parent commit
git stash branch feature/recovered-work stash@{0}
# Switched to a new branch 'feature/recovered-work'
# Changes to be committed:
# modified: src/auth.ts
This is particularly useful when you've been sitting on a stash for a long time and the branch has moved on significantly.
Stashing with a keep-index option
If you only want to stash the unstaged changes (leaving your staged changes intact and visible in the index), use --keep-index:
# Stash only unstaged changes; keep staged changes as-is
git stash push --keep-index -m "only unstaged"
Recovering a lost stash
If you accidentally drop a stash before it was garbage-collected, you can recover it:
# Find dangling commit objects
git fsck --lost-found
# Look through the output for commit objects
# Then inspect them
git show <dangling-commit-hash>
# If it's your stash, apply it directly
git stash apply <dangling-commit-hash>
Warning: This only works before
git gcruns. Once garbage collection collects the dangling object, it is gone permanently.
6. Bottlenecks & pitfalls
1. Stash entries are easy to lose
git stash clear and git stash drop are irreversible under normal usage. Once dropped, entries are not shown in git log and become dangling objects, eventually collected by git gc. Lost stashes can only be recovered via git fsck --lost-found — and only before garbage collection runs.
2. No automatic inclusion of untracked files
By default, git stash only saves tracked files. New files you haven't run git add on are completely ignored. This silently leaves behind context — a new config file, a test fixture — leading to confusing "it works on my machine" situations when you switch branches.
# This does NOT stash new-file.ts if it was never git add-ed
git stash
# This does:
git stash push -u
3. Merge conflicts on pop
If the branch has changed significantly since you stashed, git stash pop can produce merge conflicts. When this happens, Git applies the changes but does not automatically drop the stash entry — you're left in a conflicted state with the stash still in the list, leading to accidental double-pops if you're not careful.
# Safer pattern: use apply, resolve conflicts, then drop manually
git stash apply stash@{0}
# ... resolve conflicts ...
git stash drop stash@{0}
4. Index state is not preserved by default
When you pop or apply a stash, Git does not restore which files were staged vs. unstaged unless you explicitly pass --index. This breaks your carefully prepared commit structure — suddenly everything is unstaged and you have to re-add files before committing.
# Without --index: all changes land unstaged
git stash pop
# With --index: staged/unstaged split is preserved
git stash pop --index
5. The stash stack grows silently
Developers often stash, forget, stash again, and accumulate a pile of entries with useless default messages like WIP on main: abc1234 some commit. Weeks later, it's impossible to know which stash is which without laboriously inspecting diffs.
Best practice: Always use git stash push -m "descriptive message". Treat stash entries like sticky notes — label them or they become clutter.
6. Not cross-machine or collaborative
Stashes live entirely in your local .git/ directory. They are never pushed to the remote. This means you can't resume stashed work on a different machine, and you can't hand off a stash to a teammate. For any kind of collaboration or remote backup, a WIP commit on a draft branch is far superior.
7. Stash doesn't track the originating branch
A stash records the commit it was made from, but not the branch name (except in the description). If you apply a stash to a different branch, Git won't warn you — context mismatches can introduce subtle bugs that are hard to trace.
8. Submodule and binary file behaviour
Git stash has historically had inconsistent behaviour with submodules — changes inside a submodule may not be stashed, or may be stashed incompletely. Similarly, large binary files can make stash operations unexpectedly slow since Git stores entire objects, not deltas, for binary content.
7. Stash vs. alternatives
| Approach | Best for | Drawbacks |
|---|---|---|
git stash |
Short-lived, local context switches. Emergency hotfixes. | Local only, no history, stack can accumulate |
| WIP commit on a branch | Longer-running work-in-progress. Cross-machine work. | Slightly more ceremony; must remember to amend or squash later |
git worktree |
Running two branches simultaneously (e.g. review + develop) | Uses more disk space, less familiar to most developers |
| Draft PR / branch push | Sharing WIP, cross-machine access, remote backup | Pollutes remote with WIP branches unless managed carefully |
git checkout -b new branch |
When the stash work might become a full feature anyway | Creates a branch — but that's often the right answer |
Rule of thumb: If you think you'll need the stashed changes for more than a few hours, or if you might need to access them from another machine, skip the stash and make a WIP commit on a feature branch instead. Stash is powerful for short, local interruptions — but it's a poor long-term storage mechanism.
8. Quick reference cheatsheet
# --- SAVING ---
git stash # Stash tracked modified files
git stash push -m "message" # Stash with a descriptive label
git stash push -u # Include untracked files
git stash push -a # Include untracked + ignored files
git stash push --patch # Interactively choose hunks to stash
git stash push --keep-index # Stash only unstaged changes
git stash push file1.ts file2.ts # Stash specific files only
# --- INSPECTING ---
git stash list # List all stash entries
git stash show stash@{N} # Summary of a stash entry
git stash show -p stash@{N} # Full diff of a stash entry
# --- RESTORING ---
git stash pop # Apply latest stash, remove from stack
git stash apply stash@{N} # Apply a specific stash, keep in stack
git stash pop --index # Restore staging area state too
git stash apply --index stash@{N} # Apply with index state preserved
git stash branch <branch> stash@{N} # Apply stash as a new branch
# --- REMOVING ---
git stash drop stash@{N} # Delete a specific stash entry
git stash clear # Delete ALL stash entries (irreversible!)
# --- RECOVERY ---
git fsck --lost-found # Find dangling objects (lost stashes)
git show <hash> # Inspect a dangling commit
git stash apply <hash> # Apply a recovered stash by hash
Golden rules for using git stash safely:
- Always label stashes with
-m "descriptive message".- Prefer
git stash applyovergit stash popwhen uncertain — it keeps the entry until you explicitly drop it.- Run
git stash listregularly and clean up stale entries.- Use
-uif you have new untracked files you care about.- For anything lasting more than a few hours, use a WIP commit on a branch instead.
9. Deep dive — understanding --index and tracked vs. untracked files
This section answers two of the most common points of confusion when using git stash day-to-day.
9.1 The three zones of Git
Before --index makes sense, you need to clearly see the three zones Git works with:
| Zone | What it is |
|---|---|
| Working tree | Your actual files on disk — what you see in your editor |
| Index (staging area) | A snapshot you've explicitly prepared with git add, ready to become a commit |
| Repository (HEAD) | The last committed state |
git add moves a file from the working tree into the index. git commit moves everything in the index into the repository. git stash saves both the working tree and the index — and puts HEAD back in your working tree.
9.2 What --index actually does
When you run git stash, Git secretly saves two separate snapshots internally:
- One snapshot of your working tree (all modified files)
- One snapshot of your index (which files were staged)
But when you run git stash pop to restore, by default Git only replays the working tree snapshot. It throws away the index snapshot entirely — so every file comes back as "modified but unstaged", regardless of what was staged before.
git stash pop --index tells Git: "restore both snapshots — put the staged files back into the staging area, leave the rest as just modified."
Without --index:
# Before stash: index.py → unstaged, index2.py → staged
git stash
git stash pop
# After pop: index.py → unstaged, index2.py → ALSO unstaged (staged status lost!)
With --index:
# Before stash: index.py → unstaged, index2.py → staged
git stash
git stash pop --index
# After pop: index.py → unstaged, index2.py → staged (perfectly restored)
Rule of thumb: If you ran git add on anything before stashing, always pop with --index. If nothing was staged, it makes no difference.
9.3 Tracked vs. untracked files — why index.py might not get stashed
This is the other major surprise. By default, git stash only saves files that Git is already tracking — meaning files that have appeared in at least one previous commit.
Scenario A — index.py is a previously committed file (tracked):
# index.py has been committed before — Git knows about it
# index2.py is staged
git stash
# Result: BOTH files are stashed. index.py is included
# because Git tracks it, even though it wasn't staged.
Scenario B — index.py is a brand new file, never committed (untracked):
# index.py was just created — never committed, never git add-ed
# index2.py is staged
git stash
# Result: only index2.py is stashed.
# index.py is completely ignored — it stays sitting in
# your folder but is NOT saved in the stash at all.
This is exactly why you might see index.py still in your folder after a stash — Git didn't touch it because it doesn't know it exists yet.
The fix — use -u to include untracked files:
git stash push -u
# Now both index.py (untracked) and index2.py (staged) are stashed
| Flag | What gets stashed |
|---|---|
git stash |
Tracked modified files only |
git stash push -u |
Tracked files + new untracked files |
git stash push -a |
Everything including files in .gitignore
|
9.4 The perfect round trip for your scenario
Putting it all together — if you have index.py (untracked/unstaged) and index2.py (staged):
# Step 1 — stash everything including the untracked file
git stash push -u -m "WIP: both files"
# Step 2 — go do your other work ...
# Step 3 — restore exactly as you left it
git stash pop --index
# Result:
# index.py → back as unstaged (or untracked, depending on scenario)
# index2.py → back as staged, ready to commit
The two flags -u (on push) and --index (on pop) are a matched pair. Use them together whenever your stash contains a mix of tracked, untracked, staged, and unstaged files.
Top comments (0)