Handbooks / Git / Chapter 2

Stash & Worktrees

47 pages · ~81 min✓ Reviewed

Builds on Git Basics: Commit, Branch, Push. Next up: Resolving Merge Conflicts.

Part 1 · Stash & Worktrees

Stash and Worktrees: Switching Context Without Losing Work

Sooner or later a branch switch gets refused halfway through your work. You are deep in a parser fix, a production bug lands, and Git answers Please commit or stash them. The error looks random, because Git carries an edit across a switch when the file is identical in both commits and only fails on paths that conflict. Underneath it sits one real problem: your working tree, your index and your HEAD are all in use, and you need them for something else.

Git has two good answers to that problem, and they work in opposite ways. Stash parks your dirty state on a stack of temporary commits so one checkout can serve a short interruption. Worktrees give one repository several live checkouts that share a single object database, so parallel work never has to be parked at all. In short, stash trades time and worktrees trade space. Picking the wrong one is how people lose files, reapply the same change twice, or reinstall node_modules for a five-minute fix.

By the end of this chapter you will be able to stash with labels and untracked files, choose between apply and pop deliberately, and recover a dropped entry because you know a stash is just a commit. You will also be able to create, move, lock and prune worktrees, avoid the classic errors, and decide quickly whether a situation calls for a stash, a worktree, a second clone or a plain git switch.

Before you start

You need Git 2.35 or newer (for git stash --staged) and a throwaway repository with a couple of commits and branches. You should be comfortable with git add, git commit, git switch and the difference between the index and the working tree. Run git --version to check, and do the experiments in a scratch repo rather than one holding work you care about.

Part 2 · The Context-Switch Problem

Three States, One Repository

Every context-switch tool in Git works by moving changes between three places, so it helps to name them before anything else. The working tree is the set of files on disk that you open and edit. The index, also called the staging area, is what git add recorded: a proposed snapshot for your next commit. HEAD is the commit that is currently checked out, the last snapshot Git considers settled.

How a change travels
  1. 1Working treeyou edit files
  2. 2Indexgit add records them
  3. 3HEADgit commit seals them
StateWhat it holdsHow to look at it
Working treeThe files on disk, including edits you have not stagedgit diff
IndexThe snapshot you have staged for the next commitgit diff --staged
HEADThe commit you are standing ongit show HEAD

A file can differ in all three places at once: one version committed, a second staged, and a third with further edits on disk. Stash and worktrees are both ways of dealing with that mess when you need to be somewhere else. Stash takes the working tree and index and parks them out of the way. A worktree gives you a second working tree and a second index, so the first pair can stay exactly as it is.

The Refusal That Starts It

You are halfway through a change in src/parser.js when a release branch needs attention. You try to hop over to it, and Git stops you.

bash
git switch release-2.x
output
error: Your local changes to the following files would be overwritten by checkout:
        src/parser.js
Please commit your changes or stash them before you switch branches.
Aborting

This refusal is the trigger for everything in this chapter. Git will not silently destroy an edit that exists only in your working tree, so it makes you decide where that edit goes first.

The surprising part is that Git does not always refuse. A dirty switch is allowed whenever the files you modified are identical in the commit you leave and the commit you arrive at. In that case nothing in the target needs to be rewritten, so Git carries your edit across to the new branch. Only paths that differ between the two commits and also carry your local changes cause a failure. That is why the error feels random: the same dirty tree switches cleanly to one branch and is refused by another.

Will this dirty switch work?

Here is the same dirty tree against two different branches. You edited README.md, which is unchanged on release-2.x, and src/parser.js, which differs there.

bash
git switch release-2.x       # README.md edit only: allowed
git switch release-2.x       # parser.js edit as well: refused
Why it feels random

Whether the switch succeeds depends on the contents of the two commits, not on how many files you changed. A clean result on one branch tells you nothing about the next.

Four Escape Hatches

When Git refuses, you have four ways out. They differ in how quickly you can use them and in how well they keep the interrupting work separate from the work in progress.

HatchSpeedIsolationWhere the work lives
Commit a WIPfastnoneA throwaway commit on your branch
StashfastnoneA temporary commit off to the side
Second worktreemediumhighAnother directory, same repository
Second full cloneslowtotalA completely separate repository

The WIP commit is the most obvious, and it pollutes history. You record half-finished work under a meaningless message, and later you have to undo it with git commit --amend or squash it away in a rebase. If you forget, the noise reaches your teammates. Stash avoids that: your branch history stays clean. The price is that the work now sits off the branch where you cannot see it, and it is easy to forget it exists.

bash
git add -A && git commit -m "wip"        # in history, needs cleanup later
git stash push -m "wip: parser fix"      # out of history, easy to forget

The other two hatches differ in kind from those. The stash is a stack of temporary commits kept inside the repository you already have. You still have one checkout, and you use it for one task and then the other in turn, so it is time-multiplexed. A worktree gives you several checkouts of the same repository, all sharing one object database. Both tasks stay live at the same moment in different directories, so it is space-multiplexed.

bash
git worktree add -b hotfix-501 ../hotfix origin/main
StashWorktree
CheckoutsOne, reusedMany, side by side
SharesEverything, it is the same treeOne object database
Lifetime of the interruptionMinutesHours or days
Cost of the switchWrite and re-apply a diffCheck out files once
Rule of thumb

Use stash for interruptions measured in minutes. Use a worktree for parallel work measured in hours or days. Reach for a second clone only when you need a fully separate configuration.

The Blind Spot: Untracked and Ignored Files

Everything above assumes that the files Git tracks are the only ones that matter, and that is where people get caught. A branch switch ignores untracked files, meaning new files Git has never been told about, and ignored files such as build output and node_modules/. A plain git stash ignores them too. They stay in your working tree, whatever branch you are on.

bash
git status --short
# ?? src/new-lexer.js   <- untracked, a plain stash leaves it behind
git stash push -m "wip: lexer"
git switch release-2.x
git status --short
# ?? src/new-lexer.js   <- still here, on the wrong branch

The leftover file then follows you to the other branch. If that branch has its own src/new-lexer.js, the next switch or merge can collide with it. Because the stash never recorded the file, rolling back the tracked changes does not roll it back.

Common mistake

Stashing, switching and finding that half your feature is still sitting on the new branch. A plain stash only saves tracked changes. Later sections cover the flags that sweep untracked and ignored files in as well.

With the three states, the refusal, the four hatches and the blind spot in hand, the next section starts with the stash: how to save work and bring it back.

Part 3 · Stash Basics: The Save/Restore Loop

Parking Work On The Stack

The stash is a stack of parked changes. You push your dirty work onto it, your working tree goes back to a clean state, and later you take the work off again. git stash is shorthand for git stash push. It rolls every tracked modified file and every staged change back to match HEAD, and stores what it removed as a new entry on the stack.

Here is the first half of that loop. The tree starts with one staged file and one unstaged edit. After the push, git status --short prints nothing, because both changes have left the tree.

bash
git status --short
git stash push -m "wip: parser fix"
git status --short

Push with a label, then confirm the tree is clean

output
M  src/lexer.js
 M src/parser.js
Saved working directory and index state On feature-a: wip: parser fix

The -m flag attaches a label. If you leave it off, Git writes its own message in the form WIP on <branch>: <sha> <subject>, using the branch name and the latest commit subject. That message says where you were, not what you were doing. After three of them, the list looks like this:

bash
git stash list
output
stash@{0}: On feature-a: wip: parser fix
stash@{1}: WIP on feature-a: 3f2a1b7 Add lexer tokens
stash@{2}: WIP on main: 9c41d02 Fix typo in README

The labeled entry at index 0 tells you what it holds. The two auto-labeled ones only tell you which commit you happened to be on. Give every entry a -m label.

Reading The Stack Before You Touch It

git stash list prints entries as stash@{0}, stash@{1} and so on. Index 0 is always the newest. The numbers are positions, not permanent IDs. When you drop an entry, everything above it shifts down by one, so the same entry can have a different name a minute later.

Before dropping stash@{0}After dropping stash@{0}
stash@{0}: wip: parser fix(gone)
stash@{1}: WIP on feature-a: 3f2a1b7stash@{0}: WIP on feature-a: 3f2a1b7
stash@{2}: WIP on main: 9c41d02stash@{1}: WIP on main: 9c41d02
Common mistake: trusting an old index

You read stash@{2} in git stash list, drop another entry, then run a command on stash@{2}. That index now points at a different entry, or at nothing. Run git stash list again right before any command that names an index.

Before you restore an entry, look at what is in it. git stash show stash@{1} prints a diffstat, which lists the files and how many lines changed. Add -p to get the full patch.

bash
git stash show stash@{1}
git stash show -p stash@{1}
output
 src/lexer.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/lexer.js b/src/lexer.js
--- a/src/lexer.js
+++ b/src/lexer.js
@@ -12,7 +12,7 @@ function next() {
-  return tokens[pos];
+  return tokens[pos++];

The diffstat tells you which files an entry touches. The patch shows whether it is the change you remember. Both are read-only, so you can run them on any entry as often as you like.

Restoring And Removing Entries

There are two ways to bring an entry back, and they differ only in what happens to the entry afterward. git stash apply puts the changes back and keeps the entry on the stack. git stash pop puts them back and deletes the entry. Both default to stash@{0}, and both accept an index such as stash@{1}.

CommandRestores changesEntry afterward
git stash applyyeskept on the stack
git stash popyesdeleted
git stash drop stash@{2}nothat one entry deleted
git stash clearnoevery entry deleted

Use apply when you want a safety copy while you check that the work came back intact. Use pop when you are sure and want the stack to stay tidy. To delete a single entry, name it with git stash drop stash@{2}. Remember that the indices of the entries above it shift down by one.

bash
git stash apply stash@{1}
git stash drop stash@{1}

Restore, check the result, then remove the entry by hand

Common mistake: git stash clear to tidy up

git stash clear deletes every entry. It asks for no confirmation and gives you no simple way to undo it. If you only meant to remove one stale entry, use git stash drop with its index.

Recovery, Staging, And The Branch Trap

A stash that no longer applies: git stash branch

Sometimes apply or pop fails with conflicts, because the branch has moved on since you stashed. git stash branch newbranch stash@{0} is the clean way out. It creates newbranch at the commit the stash was originally made on, applies the stash there, and drops the entry if the apply succeeds. Your changes meet the exact files they were written against, so there is nothing to conflict with.

What git stash branch salvage stash@{0} does
  1. 1Find the basethe commit the stash was made on
  2. 2Create the branchsalvage starts at that commit
  3. 3Apply the stashno drift, so no conflicts
  4. 4Drop the entryonly if the apply succeeded
bash
git stash branch salvage stash@{0}
git branch --show-current

Turn a stale stash into a branch, then merge or cherry-pick from it

From salvage you can commit the work and merge it, or cherry-pick it onto the branch that moved. If the apply fails, Git keeps the stash entry, so nothing is lost.

Staged changes come back unstaged

A restore brings back the file contents but not the staging. A change that was staged when you stashed comes back as an ordinary unstaged edit. Pass --index to rebuild the original split between staged and unstaged. In the run below, the tree starts with src/parser.js staged and src/lexer.js unstaged. The first restore uses a plain pop. Then src/parser.js is staged again with git add, and the second restore uses pop --index.

bash
git status --short
git stash push -q
git stash pop -q
git status --short
git add src/parser.js
git stash push -q
git stash pop --index -q
git status --short

Plain pop loses the staging; pop --index keeps it

output
M  src/parser.js
 M src/lexer.js
 M src/parser.js
 M src/lexer.js
M  src/parser.js
 M src/lexer.js

The three status listings come from the three git status --short commands, in order. The first shows the starting state. The second, after the plain pop, shows src/parser.js demoted to unstaged. The third, after pop --index, shows the staged M src/parser.js and the unstaged M src/lexer.js exactly as they began.

A stash belongs to the repository, not the branch

The stack is shared by every branch in the repository. An entry pushed on feature-a can be popped on main, and Git will not warn you. The branch name in the WIP on <branch> text is only a label.

bash
git switch feature-a
git stash push -m "wip: lexer"
git switch main
git stash pop

This pop succeeds on main without any warning

Common mistake: popping on the wrong branch

You switch branches, run git stash pop out of habit, and the changes land on the wrong branch. Check which branch you are on and read the entry's label in git stash list before you pop.

Part 4 · Stash Internals: It's Just Commits

What a stash entry really is

Nothing about a stash is special storage. Every entry is an ordinary commit object in the same object database as your branch commits. What makes it a stash is its shape: it is a merge-like commit with two parents, or three when you stash untracked files with -u or -a.

Each parent records a different slice of your state at the moment you ran git stash push. Parent 1 is the commit you had checked out, so it is the base the stash applies to. Parent 2 is a commit that holds the index, meaning whatever you had staged. Parent 3 exists only with -u or -a, and it holds the untracked (and with -a, ignored) files.

Stash commit

tree = working-tree state

what git stash show -p diffs

Parent 1

HEAD at stash time

the base commit

Parent 2

tree = index (staged) state

always present

Parent 3

untracked files

only with -u or -a

Part of the entryWhat it holdsPresent when
Commit treeWorking-tree stateAlways
Parent 1HEAD at stash time (the base)Always
Parent 2Commit whose tree is the index stateAlways
Parent 3Commit holding untracked filesOnly with -u or -a

The split between the stash commit's tree and parent 2's tree is the reason git stash apply --index can put things back exactly. The working-tree state and the staged state are stored separately, so Git can restore the staged/unstaged separation instead of flattening everything into one unstaged blob. Without --index, only the stash commit's tree is used and staged changes come back unstaged.

Finding the stack: refs/stash and its reflog

Where do the entries live? There is exactly one ref, refs/stash, and it points at the newest stash commit. Older entries are not parents of each other and not branches. They survive only as lines in the reflog of refs/stash. That is why stash@{1} literally means reflog entry 1 of refs/stash.

You can check all of this yourself. git cat-file -p refs/stash prints the commit, including its parent list, and git log --graph --oneline refs/stash draws the shape. Here is a stash made with -u and one staged file. The SHAs below are illustrative, yours will differ.

bash
git stash push -u -m "wip: parser"
git cat-file -p refs/stash

Three parent lines means -u was used

output
tree 9c1e0b7a5d3f...
parent 3f2a1c4be8d7...
parent 71d0aa92c6e5...
parent b84e5f10a3c9...
author You <you@example.com> 1760000000 +0530
committer You <you@example.com> 1760000000 +0530

On main: wip: parser
bash
git log --graph --oneline refs/stash
output
*-.   c7d41e2 On main: wip: parser
|\ \
| | * b84e5f1 untracked files on main: 3f2a1c4 lexer
| * 71d0aa9 index on main: 3f2a1c4 lexer
|/
* 3f2a1c4 lexer
Read the shape, not the label

If git cat-file -p refs/stash shows only two parent lines, that entry has no untracked files in it. Three lines means -u or -a was used.

Indices shift, and dropped is not deleted

Because stash@{n} is a reflog position and not a name, the numbers move whenever the stack changes. Drop stash@{0} and the entry that was stash@{1} slides into slot 0. A script that remembers "my stash is stash@{1}" and then drops or pushes anything will quietly act on a different entry.

StepCommandWhat stash@{0} is
Start(three entries exist)entry C, the newest
Drop the topgit stash drop stash@{0}entry B, the old stash@{1}
Push a new onegit stash push -m newentry D, and B is now stash@{1}
Common mistake: hardcoded stash indices

Scripts that hardcode stash@{1} break as soon as anything else touches the stack, and the stack is shared by every worktree of the repo. Look an entry up by its message, or capture its SHA with git stash list --format='%H %gs' and use the SHA.

Dropping an entry removes its reflog line, and nothing else references that commit. No branch reaches a stash commit, so a dropped stash is unreachable. It is a candidate for garbage collection but not gone yet. The object stays on disk until git gc prunes unreachable objects older than gc.pruneExpire, which defaults to about two weeks. That is your recovery window for a dropped or cleared stash.

A second setting, gc.reflogExpireUnreachable, matters for entries still on the stack. Only the newest stash is reachable from refs/stash, so older reflog lines count as unreachable. By default gc expires such reflog lines after 30 days. Raise it if you park work for longer than that, though a branch is the safer home. Check or change it like this:

bash
git config --get gc.pruneExpire
git config gc.reflogExpireUnreachable 90.days

No output from --get means the default of 2 weeks applies

SettingGovernsDefault
gc.pruneExpireHow long a dropped (unreachable) stash commit survives on diskabout two weeks
gc.reflogExpireUnreachableHow long older entries stay listed in the refs/stash reflog30 days

Recovering a dropped stash and scripting with create/store

Since a dropped stash is just an unreachable commit, you can find it again. git fsck --unreachable lists every dangling object, and filtering for commits narrows it to candidates. Identify the right one with git show, then either replay it with git stash apply <sha> or pin it to a branch with git branch rescue <sha>. The branch is safer because it makes the commit reachable and ends the garbage-collection clock.

Rescue a dropped stash
bash
git fsck --unreachable | grep commit
git stash apply c7d41e2

Illustrative SHA, use the one fsck gives you

output
unreachable commit c7d41e2a90b4f6d3e1a85c02b7f4e9d6a1c3b5e8

The same object model gives you two scriptable primitives. git stash create builds the full stash commit and prints its SHA, but it does not touch the stack or your working tree. git stash store takes an existing stash-shaped SHA and pushes it onto refs/stash so it appears in git stash list.

bash
sha=$(git stash create)
git stash store -m "snapshot before refactor" "$sha"
git stash list

Snapshot the tree without clearing it

output
stash@{0}: snapshot before refactor
What to remember

A stash is a 2- or 3-parent commit. Parent 1 is the base, parent 2 the index, parent 3 the untracked files. refs/stash holds only the newest one, and its reflog holds the rest. A dropped entry stays recoverable for about two weeks (gc.pruneExpire), and git stash create plus git stash store let scripts build and file entries by hand.

Part 5 · apply vs pop: Choosing the Right One

pop is apply plus drop, with a catch

Both commands put a stash entry's changes back into your working tree. The difference is what happens to the entry afterwards. git stash apply restores the changes and leaves the entry on the stack. git stash pop restores the changes and then deletes the entry, as if you had run git stash apply followed by git stash drop.

The deletion is conditional. pop only drops the entry if the apply finished cleanly. If anything goes wrong on the way, Git keeps the entry so you do not lose the only copy of your work. That safety rule is also the source of the one trap in this section.

What git stash pop does

The conflicted pop

When pop hits a merge conflict, you end up in a half-way state. Some files are updated, the conflicting files contain conflict markers, and the stash entry is still on the stack. Git prints the conflict message and exits with a non-zero status. It never tells you loudly that the entry survived.

Here is the part people miss. Resolving the conflict, staging the files and even committing the result does not drop the entry. Git has no way to know that the stash is now redundant, so it stays put until you remove it yourself.

bash
git stash pop
# CONFLICT (content): Merge conflict in src/parser.js
# The stash entry is kept in case you need it again.

# ...edit src/parser.js, git add, git commit...

git stash list
# stash@{0}: On main: wip: parser fix   <- still here
git stash drop stash@{0}   # you must do this by hand

After resolving a conflicted pop, the entry is still on the stack.

The silent duplicate

Resolve a conflicted pop, commit, and forget to run git stash drop, and the entry stays. Days later a plain git stash pop on some other branch re-applies the same change a second time. Read git stash list right after every conflicted pop.

Comparing the two, and replaying one stash twice

Because the only real difference is whether the entry survives, the comparison comes down to three axes: how safe the command is, how tidy it keeps the stack, and what you can do after a conflict.

Axisapplypop
SafetyKeeps a backup copy of the work on the stackDeletes the entry once it applies cleanly
HygieneGrows a graveyard of stale entries you no longer trustPrevents stack rot; the stack stays short
After a conflictEntry exists, so you can reset with git checkout -- . and retryEntry also still exists, so the same reset and retry works

Both commands are recoverable after a conflict. The real risk with pop is not data loss but the duplicate described above. The real risk with apply is the opposite one: a stack full of entries so old that you cannot tell which ones still matter.

When the branch has moved far

If the target branch has moved a long way from the commit the stash was made on, prefer apply. A big merge is the situation most likely to go wrong, and you want the backup copy sitting there while you find out.

Replaying the same WIP onto two branches

Because apply leaves the entry in place, you can reuse it. Say you have a fix in progress and want to know whether it also works on release/2.x. Apply it on main, test, throw the result away, switch branches and apply the same entry again. pop cannot do this, because the first one would delete the entry.

bash
git stash apply stash@{0}      # on main
npm test
git checkout -- .              # reset the tree
git switch release/2.x
git stash apply stash@{0}      # same WIP, second branch
npm test

One entry, two targets.

The reset line only clears tracked files. If the stash also carried untracked files from -u, they come back as new files that git checkout -- . does not touch. In that case clear them with git clean before switching.

Index state, 3-way merging and a safe default

--index and the staged split

A stash remembers which changes were staged and which were not. By default both apply and pop flatten that: everything comes back as unstaged edits. Adding --index asks Git to rebuild the original split.

There is a trade-off. Plain apply degrades gracefully to 'everything unstaged'. With --index, if Git cannot reproduce the exact index state, for example because the staged part conflicts with the current branch, the command fails outright instead of giving you a close approximation.

Plain apply / popWith --index
Staged changes come back asUnstagedStaged, as before
If the index cannot be rebuiltFalls back to everything unstagedFails with an error

Applying a stash is a merge

It is tempting to think of a stash as a saved patch that Git pastes back. It is not. A stash entry is a commit, so applying it is a 3-way merge. Git takes the commit the stash was made on as the base, your current HEAD as one side and the stash's tree as the other side, then merges them.

This is why an apply can succeed even when a file has been renamed or moved since you stashed, or when the lines around your edit have changed. A plain patch would reject those hunks, but a merge can still line them up. It is also why conflicts, when they do happen, look like ordinary merge conflicts.

The practical default

Put these ideas together and one habit covers almost every case. Apply first, check that the result is good, then delete the entry yourself. The backup exists for exactly as long as you might still need it.

bash
git stash apply     # restore, keep the backup
npm test            # verify the result builds and passes
git stash drop      # now clean up

Two commands to restore and clean up, and nothing can be lost in between.

Choosing quickly

Use pop when the stash is small, recent and likely to apply cleanly, and still read the stash list afterwards. Use apply then drop whenever the branch has moved, the change matters, or you want to replay it elsewhere.

Part 6 · Stash Flags Worth Knowing

Choosing which files get swept up

A bare git stash push is narrower than most people assume. It records changes to tracked files, both staged and unstaged, and rolls them back to HEAD. Anything Git has never seen, and anything your .gitignore hides, stays exactly where it is. Three flags widen or narrow that net, and the first two are about which file categories join the stash.

-u: bring the new files along

Suppose you started a feature by creating src/cache.js and editing src/parser.js to call it. A plain stash parks the parser.js edit but leaves cache.js lying in the directory. Switch to another branch and that orphan file is still there, and if the other branch has its own src/cache.js, the switch or the later pop collides with it.

-u (long form --include-untracked) fixes this. Git saves the untracked files in an extra commit and attaches it as the third parent of the stash commit, then removes the files from disk. The same extra commit is why a -u entry has three parents instead of two.

bash
git status --short
git stash push -u -m "wip: cache layer"
git status --short

Before the stash both files show up; afterwards the tree is clean.

output
 M src/parser.js
?? src/cache.js
Saved working directory and index state On feature: wip: cache layer

-a: untracked and ignored

-a (--all) goes one step further and also stashes files matched by .gitignore. That is occasionally what you want, for instance to get a truly pristine tree. Around real projects it is a trap, because the ignored set usually holds node_modules/, build/, target/ and caches. Git will copy all of it into the object database and delete it from disk, which can mean gigabytes written, and a long reinstall or rebuild on the way back.

FlagTracked changesUntracked filesIgnored files
(none)stashedleft behindleft behind
-ustashedstashedleft behind
-astashedstashedstashed
Common mistake: a stash that forgot your new files

You stash, switch branches, and a new file you wrote an hour ago is still sitting in the tree, or blocks the pop later. Make -u your habit for anything that includes new files, and read git status before and after.

Common mistake: -a in a built project

Running git stash push -a in a repo with a populated node_modules/ stashes the whole folder and deletes it. Reach for -u by default and use -a only when you have checked what is ignored.

Staged against unstaged: -k and --staged

Your index holds the staged snapshot and your working tree holds everything else. Two flags let you treat these halves differently when stashing. They look like opposites because they are.

-k: stash it all, keep the staged version on disk

-k (--keep-index) still saves a complete stash entry, but afterwards it leaves the staged content in your working tree and index instead of resetting to HEAD. Unstaged edits disappear from disk. What remains is exactly what a commit would contain right now, which makes it the classic way to test a commit before making it.

bash
git add src/parser.js
git stash push -k -m "unstaged rest"
npm test
git commit -m "Fix parser edge case"
git stash pop

Tests run against exactly what the commit will contain.

--staged: stash only what is staged

Since Git 2.35, --staged does the mirror image. Only the staged changes go into the stash and out of the tree. Your unstaged work stays put, untouched. It suits the moment when you staged a finished piece and want to set it aside, without disturbing the half-written code you are still editing.

--keep-index--staged
Goes into the stasheverything trackedonly the staged changes
Left in the working treethe staged versionyour unstaged edits
Typical usetest exactly what you will commitpark finished work, keep editing
Common mistake: forgetting the -k entry still exists

After a -k stash the entry holds your staged changes too, so popping it later on top of a commit that already contains them can conflict or double-apply. Once the commit lands, read git stash list and decide whether to pop or drop.

Cutting the stash exactly: hunks and paths

Sometimes the line you need to draw runs through a single file, or between directories. Git gives you two precision tools, and they combine with the file-category flags from the first page.

-p: pick hunks interactively

git stash push -p (--patch) walks you through each changed hunk and asks whether to stash it. Answer y to park a hunk, n to keep it in the tree. This is how one messy edit becomes two: park the experimental half and keep the half you want to finish.

bash
git stash push -p -m "experimental retry loop"

Answer y for hunks to park, n for hunks to keep, q to stop.

Pathspec: stash only named files

Put paths after a -- separator and the stash covers only those. Everything else, including other modified files, stays in the tree exactly as it was.

bash
git stash push -m "parser work" -- src/parser.js src/lexer.js
git status --short

Only the two named files are stashed.

output
Saved working directory and index state On feature: parser work
 M README.md

A pathspec also works with -u. git stash push -u -- src/ stashes the tracked changes under src/ along with any untracked files there, and leaves the rest of the repo alone. That makes it a good way to move one directory's new files out of the way without disturbing the rest of the tree.

GoalCommand shape
Split one file's editsgit stash push -p
Stash chosen files onlygit stash push -- path1 path2
Chosen directory, new files toogit stash push -u -- dir/

Traps for scripts and flag combinations

Two behaviours here catch people who automate stashing or stack flags together.

Nothing to save

If there are no tracked changes, git stash push prints No local changes to save and creates no entry. In a script that is dangerous in a quiet way: the next step may assume a stash exists and run git stash pop, which then pops an older, unrelated entry from the shared stack. Untracked files do not count here, so a tree with only new files also reports nothing unless you passed -u.

bash
git stash push -m "wip"
git stash list

A clean tracked tree: no entry is created.

output
No local changes to save

Do not rely on the exit status alone to detect this case, since the command can report success while pushing nothing. Compare the stash tip before and after instead: capture git rev-parse -q --verify refs/stash ahead of the push and again afterwards, and only pop when the value changed.

Common mistake: popping after a push that saved nothing

A script that does git stash push && ... && git stash pop will pop someone else's older entry whenever the tree was clean. Verify that a new entry was created before you pop.

-k together with -p

Combining --keep-index with --patch does not do what most people expect. You might imagine that the hunks you decline to stash become the kept index. Instead, the index that is kept is the pre-stash index, the one you had staged before you started. Your hunk choices decide what goes into the stash, not what the test run will see.

CombinationBehaves as expected?
-u with a pathspecyes, new files under that path are included
-a with a pathspecyes, but check what ignored files match
-k with -pno, the kept index is the old staged state
Rule of thumb

If you need to test precisely the change you are about to commit, stage it first with git add -p, then use -k on its own. Do not ask -p to build the kept state for you.

Part 7 · Worktrees: One Repo, Many Checkouts

Adding a second checkout

Stash parks work on a stack so one checkout can change hands. A worktree goes the other way: it gives the same repository a second working directory, so two branches can be checked out at the same moment. Each directory has its own files, its own index and its own HEAD. Both draw on a single history.

The simplest form checks an existing branch out into a new directory. Say a bug report arrives while you are halfway through a feature. Instead of parking your edits, you run one command from inside your current checkout.

bash
git worktree add ../hotfix hotfix-branch

Creates ../hotfix, checked out to hotfix-branch, next to your current directory

Git creates ../hotfix, fills it with the files of hotfix-branch, and links it back to the repository you ran the command from. It does not copy the history. The new directory sees the same object database as the original, which is why the command finishes in about the time a checkout takes.

Creating the branch in the same step

Often the branch does not exist yet. Adding -b makes Git create the branch and the worktree together, so you skip a separate git branch. For throwaway experiments you may not want a branch at all. --detach checks out a commit directly and leaves HEAD detached, so nothing is left behind on a branch when you are done.

bash
git worktree add -b fix-123 ../fix-123
git worktree add --detach ../scratch HEAD

The first makes fix-123 and its directory; the second is a branchless scratch pad

FormBranch afterwardsGood for
add ../hotfix hotfix-branchUses the existing branchWork that already has a branch
add -b fix-123 ../fix-123Creates fix-123Starting a new line of work
add --detach ../scratch HEADNone, detached HEADExperiments you plan to discard

Main and linked worktrees

The checkout you started with is called the main worktree, because it holds the real .git directory. Every directory you add afterwards is a linked worktree. To see them all, run git worktree list. Each line shows the path, the short HEAD sha and the branch, or (detached HEAD) when there is none.

bash
git worktree list
output
/home/dev/repo       3f2a1c9 [feature-cart]
/home/dev/hotfix     8b7d0e4 [hotfix-branch]
/home/dev/fix-123    3f2a1c9 [fix-123]
/home/dev/scratch    3f2a1c9 (detached HEAD)

What is shared and what is not

Linked worktrees are not copies of the repository. They are extra views of one repository, and most of what makes it a repository is shared between all of them.

Shared by every worktreeSeparate in each worktree
Objects: commits, trees and blobsHEAD, the checked-out branch
Refs: branches and tagsThe index (what is staged)
Config and remotesThe working files on disk
HooksPer-worktree refs such as bisect state

The practical result is that you never fetch twice. A git fetch run in any worktree updates the remote-tracking refs for all of them, and a commit made in one is immediately visible by name in the others. There is no re-clone and no second download of history.

The disk cost

A linked worktree costs one working tree of files plus a few bytes of admin data. It does not cost a second copy of history. On a repository whose .git is large, that is a big saving compared with a second clone, which copies the whole object database.

Second directory made byExtra disk usedExtra fetching
git worktree addOne checkout of filesNone
git cloneFiles plus a full copy of historyA second fetch to stay current
Space, not time

Stash time-multiplexes one working tree. Worktrees space-multiplex several trees over a single history, and the history is paid for only once.

The stash stack is shared too

Each worktree has its own HEAD, its own index and its own files, so its working state is private. The stash is different. It lives in refs/stash, a shared ref, so the whole stack is common to every worktree. Anything you stash in ../hotfix shows up in git stash list in the main checkout, and a git stash pop anywhere can take it.

bash
cd ../hotfix
git stash push -m "wip: hotfix idea"
cd ../repo
git stash list
output
stash@{0}: On hotfix-branch: wip: hotfix idea
Common mistake: assuming each worktree has its own stash

The entry above was made in ../hotfix, yet stash@{0} in the main checkout is that entry. A bare git stash pop there would apply the hotfix edits to the wrong branch. Always label entries with -m and read git stash list before popping.

Looking after your worktrees

Git keeps a small record of each linked worktree inside the shared .git. That is why you should change a worktree through Git commands and not by hand. Each command below keeps the record and the directory in step.

CommandWhat it does
git worktree remove ../hotfixDeletes the directory and its admin record together
git worktree pruneClears records whose directory no longer exists
git worktree lock ../on-usb --reason "removable drive"Protects a worktree from prune
git worktree move ../old ../newRelocates the directory and repairs its pointers

Removing a worktree

When the hotfix is pushed, git worktree remove takes the directory away cleanly. It refuses if the worktree has uncommitted changes, which protects you from throwing work away by accident. If you deleted the directory yourself with rm -rf or a file manager, Git still lists it. git worktree prune removes those stale records.

bash
git worktree remove ../hotfix
rm -rf ../scratch
git worktree prune
git worktree list
output
/home/dev/repo       3f2a1c9 [feature-cart]
/home/dev/fix-123    3f2a1c9 [fix-123]

Locking storage that may disappear

Prune cannot tell the difference between a deleted directory and one that is merely missing. A worktree on a removable drive or a slow network share looks deleted whenever the storage is unmounted, so a prune would erase its record. git worktree lock marks it as protected, and the optional reason is shown by git worktree list.

bash
git worktree lock ../on-usb --reason "removable drive"

Moving a worktree

The shared .git stores the worktree's path, and the worktree stores a pointer back. git worktree move ../old ../new relocates the directory and rewrites both sides. Using plain mv breaks that link, and Git then treats the worktree as missing.

Which command do I want?
Common mistake: deleting or moving by hand

Using rm -rf or mv on a worktree directory leaves Git with a stale or broken record. After a manual delete, run git worktree prune. For a relocation, always use git worktree move.

Part 8 · Worktree Internals & Rules

What a linked worktree really is

A linked worktree looks like an ordinary checkout, but it has no repository of its own. Open the directory that git worktree add created and you will find your files, plus a .git that is a file rather than a folder. That file holds one line, which points into the main repository's .git directory.

bash
$ cat ../hotfix/.git
gitdir: /repo/.git/worktrees/hotfix

The .git entry in a linked worktree is a one-line pointer.

Git finds the repository by walking up from your current directory and looking for .git. When it finds a file starting with gitdir:, it follows the path. So every command you run inside ../hotfix ends up reading the admin folder that the main repository set aside for that worktree.

The per-worktree admin folder

The target of that pointer is .git/worktrees/<name>/. It is small, and it holds exactly the state that has to be private to one checkout, plus two files that tie it to the rest of the repository.

EntryWhat it holds
HEADWhich branch or commit this worktree has checked out
indexThis worktree's own staging area
logs/This worktree's own reflogs, including the one for its HEAD
commondirA relative path back to the shared .git, normally ../..
gitdirThe path of this worktree's .git file, so Git can tell whether the directory still exists

The two pointers form a loop. The worktree's .git file leads to the admin folder, and commondir in the admin folder leads back to the shared repository. The gitdir file in the admin folder leads back to the worktree. That last link is how Git later notices that a worktree directory has vanished.

How Git resolves a linked worktree
  1. 1Worktree .git filegitdir: /repo/.git/worktrees/hotfix
  2. 2Admin folderHEAD, index, logs, commondir, gitdir
  3. 3commondir../.. leads to the shared .git
  4. 4Shared .gitobjects, branches, tags, config, stash

This small program does what Git does with those two files. It reads the pointer line, strips the prefix to get the admin folder, then applies the commondir value to reach the shared directory.

python
import posixpath

dotgit = "gitdir: /repo/.git/worktrees/hotfix"
commondir = "../.."

admin = dotgit.split(": ", 1)[1]
common = posixpath.normpath(posixpath.join(admin, commondir))

print("admin :", admin)
print("common:", common)

Following the two pointers by hand.

output
admin : /repo/.git/worktrees/hotfix
common: /repo/.git

What is private and what is shared

The rule behind commondir is simple. Anything Git does not explicitly treat as per-worktree is looked up in the shared .git. That includes the object database, config, remotes, hooks, and nearly every ref. Only a short list of refs is private to a worktree.

RefScopeWhy
HEADper-worktreeEach checkout is on its own branch or commit
refs/bisect/*per-worktreeA bisect in one tree must not disturb another
refs/worktree/*per-worktreeA place for tools to keep private refs
branches (refs/heads/*)sharedOne branch name means one commit everywhere
tags and remotessharedOne git fetch updates every worktree
refs/stashsharedAll worktrees push onto the same stack

The shared stash is the one that surprises people. A git stash made in ../hotfix shows up in git stash list from the main tree, and a pop there can apply it. Treat the stack as repository-wide, and label entries with -m so you can tell whose they are.

Two questions settle almost every case

Is this a ref in the short per-worktree list, or is it something else? Per-worktree means it lives in .git/worktrees/<name>/. Anything else resolves through commondir to the shared .git.

The bisect case is a good use of the split. git worktree add --detach ../bisect gives you a throwaway checkout where you can run a long bisect without touching your feature tree. The bisect bookkeeping stays inside that worktree's admin folder.

One branch, one worktree

Because branches are shared but each worktree has its own HEAD and index, Git needs a rule to stop two checkouts from fighting over one branch. The rule is hard: a branch can be checked out in only one worktree at a time. The main checkout counts as a worktree too.

bash
$ git worktree add ../x main
fatal: 'main' is already checked out at '/repo'

The most common worktree error.

The reason is concrete. If two worktrees had main checked out, a commit in one would move the branch ref. The other worktree's index and files would then describe the old tip, and its next commit would silently undo the first. Refusing up front is far cheaper than untangling that afterwards.

This error is the #1 worktree mistake, and the fix is almost never to override it. You want a second checkout of the same code, so you ask for one that does not claim the branch name. The flowchart shows how to choose.

Adding a worktree for a branch that is already checked out
bash
git worktree add --detach ../scratch main
git worktree add -b review-main ../r main

Both start from main without taking the branch name.

A detached worktree sits on the commit and owns no branch. A new branch starts at the same commit and gets its own name. Either way, main stays with the checkout that holds it.

Common mistake: reaching for --force

git worktree add --force skips the check, and the result is two indexes racing on one ref. Use it only if you understand exactly what you are signing up for. Otherwise use --detach or a new branch.

Scripts, config, submodules and cleanup

Scripts must not hardcode .git/

In a linked worktree there is no .git/hooks or .git/config underneath you, because .git is a file. The Git directory also differs from worktree to worktree, so any script that builds paths like .git/hooks/pre-commit breaks as soon as someone runs it from a linked tree. Ask Git where things are instead.

CommandAnswersIn a linked worktree
git rev-parse --git-dirThis worktree's private admin folder/repo/.git/worktrees/hotfix
git rev-parse --git-common-dirThe shared .git/repo/.git
bash
HOOKS=$(git rev-parse --git-common-dir)/hooks
# correct in the main checkout and in every linked worktree

Hooks and config are shared, so use the common dir.

Use --git-dir for state that belongs to the current checkout, such as its index. Use --git-common-dir for state that all checkouts share, such as hooks, config and objects.

Config for one worktree only

Config is shared by default, so setting core.hooksPath in one tree changes it for all of them. To scope a value to a single worktree, turn on the extension first and then use --worktree. Git stores the value in that worktree's own config.worktree.

bash
git config extensions.worktreeConfig true
git config --worktree core.hooksPath .hooks

Without the extension, --worktree has no private file to write to.

Two rules that bite later

Submodules inside linked worktrees have a long history of rough edges, because each submodule has its own .git pointer logic layered on top of the worktree's. If your project uses them, test a fresh git worktree add with the submodule operations you depend on before building a workflow around it.

Deleting the directory by hand does not tell Git anything. If you run rm -rf ../hotfix, the admin folder in .git/worktrees/hotfix is still there and git worktree list keeps printing the dead entry until you clean it up.

Common mistake: removing worktrees with rm

Use git worktree remove ../hotfix so Git deletes the directory and the admin folder together. If you already used rm -rf, run git worktree prune. A worktree on removable storage can be protected from pruning with git worktree lock.

Part 9 · When Each Wins

Matching the Tool to the Interruption

Git gives you three ways to deal with an interruption, and none of them is best in every case. A plain branch switch costs nothing but needs a tidy tree. A stash parks dirty work on a stack so the switch can happen. A worktree skips the switch by giving the other branch its own directory. Which one wins depends on how long you will be away, how big your uncommitted diff is, and how much state your tools have built up around the current checkout.

Which tool for this interruption?

A branch switch wins when there is nothing to carry

If your tree is clean and you only need to look at another branch, make a one-line fix there, and come straight back, a plain git switch is the right answer. There is no stack to remember, no extra directory to delete, and no entry that can be forgotten. Reaching for a heavier tool here just adds cleanup you will have to do later.

A stash wins for short, small interruptions

A stash fits a 30-second interruption: someone asks you to check one thing on main, and your diff is small. It works well because the stash records your changes and then rolls the tree back to HEAD, so you stay in the same directory with the same build artifacts. Nothing long-running has to be restarted, because you are only away for a moment and expect to be back on the same branch.

A worktree wins when the work is long or the state is expensive

A worktree pays off when the parallel work is long-lived, when your checkout holds expensive build or install state, when a dev server is running that you do not want to kill, or when you need both versions visible at once. In those cases you spend a few seconds creating a second directory so you never have to disturb the first one.

SituationBranch switchStashWorktree
Tree is clean, quick hopBest fitUnneededOverkill
30-second interruption, small diffBlocked if files conflictBest fitOverkill
Work lasting hours or daysAwkwardRots on the stackBest fit
Slow build or running dev serverInvalidates cachesCaches stay but tree is rolled backBest fit
Need both versions on screenImpossibleImpossibleBest fit
Admin overheadNoneLowCreate and remove a directory

Where Worktrees Are Unbeatable

Three situations are hard or impossible to handle with a stash, and a worktree handles all of them naturally. What they share is that you need two checkouts to exist at the same moment, instead of taking turns in one.

Comparing two versions side by side

A stash only ever lets you look at one state at a time, because it empties the tree before you switch. If you need to compare how version 1 and version 2 behave, give each its own directory. You can then run both test suites at the same time, diff the outputs of the two programs directly, and open both folders in your editor in two windows. Stash cannot do this at all.

bash
git worktree add ../v1 v1.0
git worktree add ../v2 v2.0

(cd ../v1 && npm test > /tmp/v1.out)
(cd ../v2 && npm test > /tmp/v2.out)
diff /tmp/v1.out /tmp/v2.out

Two live checkouts of one repository, so the outputs can be compared directly

A build that takes ten minutes

When you switch branches, Git rewrites every file that differs between the two commits. Build tools then see changed inputs and throw away what they cached: node_modules gets reinstalled, target/ gets rebuilt, __pycache__ goes stale, and incremental compiler caches are invalidated. Switching there and back can cost you ten minutes each way. With a second worktree, each branch keeps its own directory, and therefore its own hot build.

Hotfix while the feature build stays warm
  1. 1Feature treenode_modules and target/ are warm
  2. 2git worktree add -b fix-88 ../fix mainnew directory, no objects copied
  3. 3Build and test in ../fixpays the install cost once, there
  4. 4Push, then remove ../fixfeature tree was never touched
bash
git worktree add -b fix-88 ../fix main
cd ../fix && npm ci && npm test
git push -u origin fix-88
cd ../your-feature && git worktree remove ../fix

Nothing was stashed, so nothing can be lost

A long code review

Reviewing a pull request can take an hour, and you may want to run the code, poke at it, and come back to it after lunch. Check the PR branch out in a sibling directory such as ../review. Your in-progress feature stays exactly as you left it, with its uncommitted edits, running server, and open editor tabs.

bash
git fetch origin pull/123/head:pr-123
git worktree add ../review pr-123
# read, run and test in ../review
git worktree remove ../review

The review lives in its own directory and is deleted when you are done

Where Stash Is Unbeatable, and Its Limits

Stash wins in two situations where there is no second branch to visit at all. You are not changing context. You just need your tree to be briefly clean, or briefly different, in the directory you are already in.

When git pull refuses because of local noise

git pull will stop if your uncommitted edits touch files that the incoming commits also change. Often these are small scratch edits you do not care about right now. Stash them, pull with a rebase, and bring them back. A worktree would be pointless here, since you want the updated branch in this same directory.

bash
git stash && git pull --rebase && git stash pop

The && chain stops at the first failure, so a failed pull leaves your work safe on the stack

Testing exactly what you are about to commit

After staging part of your work with git add -p, the tree still contains your unstaged edits, so a test run might pass or fail because of code you are not committing. --keep-index stashes the unstaged changes but leaves the staged version on disk. You then verify exactly the staged change, commit it, and restore the rest.

bash
git add -p
git stash push --keep-index -m "rest"
npm test && git commit
git stash pop

The tests run against the staged code only

Worktree or second clone?

If you need a second checkout, you have one more choice: a full clone. A worktree shares the object database, refs, config, remotes and hooks with the original, so it is cheap and a single git fetch updates every tree. A clone copies everything and shares nothing, which costs disk and a second fetch but gives you total isolation.

WorktreeSecond clone
ObjectsShared with the originalCopied
Fetches neededOneOne per clone
Config and hooksSharedIsolated
Disk costOne checkout of filesA whole second history
Choose it whenYou want cheap parallel workA tool mutates repo config or hooks
The deciding question

Ask whether anything you run will change .git/config or install hooks. If yes, a worktree would leak that change into your main checkout, so use a separate clone.

Common mistake: using the stash as long-term storage

The stash is a single ref, refs/stash, and its entries are unlabeled by branch, renumber when one is dropped, and can be wiped by git stash clear with no confirmation. Dropped entries become garbage-collection candidates. Anything you would be sad to lose belongs on a branch. Use git stash branch <name> to promote a stash you want to keep.

The short version

Switch when the tree is clean, stash for short interruptions in the same tree, and add a worktree when the work is long, the build is hot, or you need two versions in front of you.

Part 10 · Cost & Trade-offs

What each option costs in time and disk

Every answer to context switching has a price. Some of it is paid in seconds, some in gigabytes, and some in the effort of keeping track of what you did. The first two are easy to measure, so start there.

OperationTime costDisk cost
git stash push / popSize of the changed set, plus one tree writeOne commit, a few trees, and the changed blobs
git worktree addOne full checkout of the tree; bound by file countA few hundred bytes of admin files, plus the checked-out files
Full git cloneCopies or fetches the whole object databaseThe whole .git again

Stash is nearly free

A stash push has to look at what changed, write those files as blobs, write a tree, and wrap it in a commit. It does not touch the files you left alone. That makes the cost proportional to the changed set, not to the size of the repository, so for a typical diff of a few files it finishes before you notice. Pop is the same work in reverse: a three-way merge of the stash tree into your working tree.

Storage is just as modest. Each entry is one commit plus its trees and blobs, and Git deduplicates all of them against objects it already has. A file you stashed unchanged costs nothing extra. The one way to make a stash expensive is -a, which sweeps up ignored files too. Stash with -a next to a build directory and you write every compiled artifact as a fresh blob.

bash
git stash push -u -m "wip: cart"   # untracked, but not build output
git count-objects -vH              # see what the object store grew by

Prefer -u over -a unless you truly need the ignored files.

A worktree costs one checkout

Adding a worktree copies no objects. Git writes a small directory under .git/worktrees/ holding a HEAD, an index and a few pointer files, then checks out the tree into the new directory. The time is dominated by writing files, so it is I/O bound on file count. A repository with 200 files adds a worktree in a blink; one with 200,000 files takes noticeably longer, and an antivirus scanner on Windows makes that gap wider.

A clone pays for history twice

A full clone duplicates the whole object database. A repository with a 2 GB .git becomes 4 GB on disk, and each clone also needs its own fetches to stay current. Two options avoid the copy: git clone --shared and git clone --reference borrow objects from the source repository instead of copying them.

Shared clones are a fragile loan

A clone made with --shared or --reference depends on the source repository's object directory. If the source is repacked, pruned or deleted, the borrower can lose objects it still needs and become corrupt. A worktree gets the same saving, and Git knows about it. Treat a shared clone as a last resort.

The cost you actually feel: attention

The seconds and megabytes above rarely decide anything. What decides is how much you have to keep in your head, and the two tools cost you in different ways.

Stash: an opaque stack

A stash entry carries a message and a position, and that is all. If you never labeled it, the list shows WIP on main: 3f2a1 ... and nothing about what the work was for. An entry also has no tie to the branch it came from, so it will pop onto any branch without a warning. Indices renumber whenever an entry is dropped, so stash@{2} today is stash@{1} tomorrow. None of this slows Git down. It slows you down, and the damage grows with every entry you add.

bash
git stash list
# stash@{0}: WIP on feature-a: 3f2a1 lexer
# stash@{1}: WIP on main: 9bd04 parser
# stash@{2}: WIP on main: 9bd04 parser

Three entries, two of them identical in the listing. Which one do you want?

Unlabeled stashes rot

Run git stash push -m "what and why" every time. A message written now takes five seconds; reconstructing an entry from git stash show -p next week takes minutes and can still leave you guessing.

Worktrees: more places to be

A worktree moves the cost from the list to the filesystem. You now have several directories that look almost identical, and it is easy to run a command or save a file in the wrong one. Your editor or IDE may index each directory separately, which multiplies memory use, file watchers and search results. Some tools assume exactly one checkout: scripts that hardcode .git/, build systems that cache by absolute path, and hooks written for a single tree can misbehave in a linked worktree.

StashWorktree
What you must rememberWhich entry is whichWhich directory holds which branch
Typical slipPopping onto the wrong branchEditing or building in the wrong directory
Where the clutter livesIn refs/stash, out of sightOn disk and in your editor's sidebar
Tooling riskScripts that hardcode stash@{n}Tools that assume one checkout or one .git directory
Make worktrees easy to find

Put linked worktrees in sibling directories named for their purpose, such as ../hotfix or ../review, and run git worktree list before creating another one.

gc, scaling and the shared object store

gc must see every root

All worktrees share one object store, so git gc cannot decide what is garbage by looking at a single checkout. It has to treat the HEAD and index of every worktree as a root, alongside the branches and tags. That is the safe behavior: a commit you are working on in a detached HEAD in ../scratch will not be collected from under you.

The catch comes from directories that vanished without Git being told. If you rm -rf a worktree, its entry under .git/worktrees/ stays, and its old HEAD still counts as a root. Commits that only that worktree reached stay alive, and a worktree on removable storage that is currently unplugged looks exactly like one you deleted. This is why the hygiene order matters.

Cleanup order before gc
  1. 1Remove worktrees properlygit worktree remove ../dir
  2. 2Prune stale entriesgit worktree prune after any manual delete
  3. 3Check the stash listdrop or promote entries you no longer need
  4. 4Run gcnow the roots match reality
Locked means kept

A worktree marked with git worktree lock is never pruned, on purpose. If you locked one for a removable drive and forgot, its roots protect objects indefinitely.

How each one scales

Both tools are fine at one or two. They fail differently past about five. A stash stack degrades as an organizational problem: with more than five entries you cannot say what is in them, and indices keep shifting. Worktrees degrade mostly through the machine, not through Git. Each directory holds a full checkout and may have its own node_modules or build output, and each one may be indexed by your editor, so disk and indexing pressure show up first.

CountStashWorktrees
1 to 2FineFine
About 5Starts to degrade; hard to tell entries apartFine for Git; watch disk
More than 5Opaque stack, easy to pop the wrong oneDisk and editor indexing pressure

Concurrency and its limits

The strongest argument for worktrees is that they let two things happen at the same time. One working tree can only be in one state: while a long build runs against it, you cannot switch branches, and a second git status or test run would see half-finished files. Two worktrees have independent files, indexes and HEADs, so you can run a ten-minute build in one and git status, edits and even a second build in the other.

Can these two jobs run at the same time?

Parallel does not mean independent. The refs, the object store and refs/stash are shared, and Git protects them with lock files. Two worktrees that update the same branch ref, run a fetch that rewrites shared refs, or both push to the stash can collide, and one of them will report that it cannot create a .lock file. Each worktree's own index has its own lock, so git status in two trees never fights. The conflicts come only from the shared parts.

Pick by what you can afford

Stash costs almost nothing in time and disk, and its price is the memory you spend on an unlabeled stack. A worktree costs one checkout plus some directory and tooling overhead, and in return it gives you true parallelism. Neither beats a plain branch switch when your tree is clean.

Part 11 · Common Mistakes & Fixes

Stash slips: wrong base, lost files, cleared stack

Most stash and worktree trouble comes from a handful of repeated habits. This section walks through ten of them. Each one gets the slip, why Git behaves that way, and the fix. The first three are about losing or mangling work you parked on the stash stack.

1. Popping on a different base

A stash entry is a commit whose first parent is the HEAD at the moment you stashed. If you stash on one branch, switch to another and then pop, Git has to replay the change as a 3-way merge against a base that is no longer your starting point. Where the two branches differ around your edit, you get conflicts that have nothing to do with what you actually changed.

How the conflict appears
  1. 1Stash on mainbase is the commit main pointed at
  2. 2Switch to release-2.xhistory has diverged from that base
  3. 3Popmerge against the wrong ancestor
  4. 4Conflictsentry is kept, tree is half applied

The fix is git stash branch. It creates a new branch at the commit the stash was made from, applies the entry there, and drops it only if the apply succeeded. Because the base matches, the replay is exact.

bash
git stash push -m "wip: parser fix"
git switch release-2.x
git stash pop            # conflicts: wrong base

# the clean way out
git restore --staged --worktree .   # abandon the half-applied pop
git stash branch parser-fix stash@{0}

stash branch reapplies on the original base

2. Forgetting -u, then running git clean

A plain git stash records tracked files only. New files you have not yet added stay in the working tree, untouched and unsaved. If you then tidy up with git clean -fd, those files are deleted, and since they were never in any commit or stash, there is nothing to recover them from.

bash
git stash                 # tracked changes only
git clean -fd             # deletes new_module.py for good

# safer default
git stash push -u -m "wip: cart"

-u stores untracked files as the stash's third parent

Common mistake: stash without -u, then clean

Untracked files that were never stashed are unrecoverable after git clean -fd. Make git stash push -u your default, and run git clean -n first to see what would go.

3. Using git stash clear to tidy up

git stash clear asks nothing and removes the whole stack in one go. The commits are not deleted at that moment, but nothing points at them any more, so they are unreachable immediately. They survive only until garbage collection prunes unreachable objects, which by default is about two weeks (gc.pruneExpire).

bash
git stash clear
git fsck --unreachable | grep commit
git show 9fceb02          # check it is your WIP commit
git branch rescue 9fceb02  # pin it before gc runs

recovery is possible only inside the gc window

Recovery is guesswork, because fsck lists every unreachable commit, not only stashes. Prefer git stash drop stash@{n} for single entries, and never treat clear as housekeeping.

Stash slips: duplicates and the wrong branch

4. Reapplying a change you already committed

pop is apply followed by drop, and the drop happens only when the apply was clean. After a conflicted pop, Git leaves the entry on the stack on purpose, because it cannot know you will resolve everything. You resolve the files, commit, and carry on, but the entry is still there. A few days later a careless git stash pop applies the same change on top of work that already contains it.

After a conflicted pop
bash
git stash pop            # CONFLICT in src/lexer.js
# edit, git add, git commit
git stash list           # stash@{0} is still here
git stash drop stash@{0}

verify, then drop by hand

Common mistake: trusting pop after a conflict

Read git stash list right after you finish resolving. If the entry is still there and its change is already committed, drop it instead of leaving a trap for later.

5. Assuming a stash belongs to its branch

The stack lives in refs/stash, which is one ref per repository. Git records the branch name in the message only as a label. Nothing stops a stash made on feature-a from popping onto main, and Git will not warn you. Read the label before you pop.

output
stash@{0}: WIP on feature-a: 3f2a1 lexer
stash@{1}: On main: wip: parser fix

output of git stash list

EntryMade onSafe to pop here?
stash@{0}feature-aOnly if you are on feature-a, or you want it elsewhere on purpose
stash@{1}mainOnly on main, or a branch that still has main's context

Labelling with -m makes this readable, and git stash show -p stash@{0} shows exactly what you are about to apply.

Worktree slips: removal, branch clashes, paths, deleted branches

Worktrees add a second kind of state: a directory on disk plus administrative records under the shared .git. Mistakes here come from changing one without the other.

6. Deleting a worktree directory by hand

If you run rm -rf ../hotfix, the files disappear, but .git/worktrees/hotfix and its branch lock remain. git worktree list keeps showing the dead entry and the branch stays marked as checked out. git worktree prune clears records whose directories are gone. Better still, use git worktree remove in the first place.

rm -rf the directorygit worktree remove
FilesDeletedDeleted
Admin recordsLeft staleCleaned up
Branch lockKept until pruneReleased
Follow-upgit worktree pruneNone
bash
rm -rf ../hotfix
git worktree list    # still lists ../hotfix as prunable
git worktree prune

cleaning up after a manual delete

7. Adding a worktree on a branch that is already checked out

One branch can be checked out in only one worktree, because two indexes would race to move the same ref. The main worktree counts, so git worktree add ../x main fails while you are on main. Detach the new tree, or give it its own branch.

bash
git worktree add ../x main
# fatal: 'main' is already checked out at '/repo'

git worktree add --detach ../x main        # no branch, just the commit
git worktree add -b review-main ../x main  # new branch off main

two ways around the one-branch rule

Common mistake: reaching for --force

--force overrides the safety check and lets two worktrees fight over one branch. Use --detach or -b instead.

8. Hardcoding .git paths in scripts

In a linked worktree, .git is a small file containing a gitdir: pointer, not a directory. A path like .git/hooks or .git/config then points at nothing. Hooks and config live in the shared directory, and git rev-parse --git-common-dir returns it from any worktree. On Git 2.31 or newer you can add --path-format=absolute so the result does not depend on your current directory.

bash
# breaks in linked worktrees
HOOKS=.git/hooks

# works in main and linked worktrees
HOOKS=$(git rev-parse --git-common-dir)/hooks

--git-dir is per-worktree, --git-common-dir is shared

9. Deleting a branch a worktree has checked out

Git protects this case. Both git branch -d and git branch -D refuse to delete a branch that is checked out in any worktree, and -D only overrides the merged-ness check, not this one. The only way to orphan the worktree is to remove the ref behind Git's back, for example with git update-ref -d refs/heads/hotfix. The worktree's HEAD then names a branch that no longer exists.

bash
git branch -D hotfix        # refused: checked out in ../hotfix

# do it in this order instead
git worktree remove ../hotfix
git branch -d hotfix

release the branch first, then delete it

Common mistake: update-ref -d on a live branch

Deleting the ref by hand skips the check and leaves the linked worktree on an orphaned ref. Remove the worktree, or switch it to another branch, before the branch goes.

Scripts: pop is not atomic

10. Treating git stash pop as all-or-nothing in a script

A pop does two things: apply the changes, then drop the entry. A conflict can leave the tree partly modified and the entry still on the stack, and the command exits non-zero. A script that ignores that exit code carries on with a half-applied tree. The mirror problem exists on the way in: git stash push with nothing to save prints a message and does nothing, so a later pop would apply an older, unrelated entry.

bash
if [ -n "$(git status --porcelain)" ]; then
  git stash push -u -m "script: pre-pull" && saved=1
fi

git pull --rebase

if [ "$saved" = 1 ]; then
  if ! git stash pop; then
    echo "pop did not finish cleanly" >&2
    git stash list
    exit 1
  fi
fi

only pop what you pushed, and check the result

If your script must be strict, apply by SHA and drop only after you have verified the result, so you always know which entry you are touching.

SlipFix
Pop after switching branchesgit stash branch <name>
Stash without -u, then cleanDefault to git stash push -u
git stash clear to tidygit fsck --unreachable, within the roughly two-week gc.pruneExpire window
Entry left after a conflicted popgit stash list, then drop
Popping on the wrong branchRead the WIP on <branch> label first
rm -rf on a worktreegit worktree prune
Add on a checked-out branch--detach or -b
Hardcoded .git pathgit rev-parse --git-common-dir
Branch deleted under a worktreeRemove the worktree first
Unchecked pop in a scriptTest the exit code, then read the list

Part 12 · Recipes

Stash recipes for everyday interruptions

Everything so far comes together in a handful of repeatable moves. The first three recipes use the stash, which suits interruptions measured in minutes. Each one has the same shape: park the work, do the other thing, then bring the work back.

1. Interruption: fix a bug, then resume

A colleague reports a bug while your checkout flow is half-written. Park everything, including new files, under a label you will recognise later. -u brings untracked files along, so they cannot collide with the branch you are about to visit, and -m keeps the stack readable.

bash
git stash push -u -m "wip: checkout flow"
git switch -c fix/null-total main
# fix the bug, run the tests
git commit -am "Guard against a null order total"
git switch -
git stash pop
git stash list   # confirm the entry is gone

Park, fix, commit, resume

The last line matters. pop only drops the entry when the apply was clean, so a quick look at the list tells you whether anything is still parked.

2. Test only the staged change

Suppose your working tree holds two unrelated edits and you want to commit just one of them. Tests that run against the whole tree would test both. Stage the half you want with git add -p, then stash the rest with --keep-index, which leaves the staged version live on disk. Now the tests see exactly what you are about to commit.

bash
git add -p
git stash push --keep-index -m "unstaged rest"
npm test
git commit -m "Validate coupon codes before checkout"
git stash pop

What passes the tests is what gets committed

Staged-only test loop
  1. 1Stage one halfgit add -p
  2. 2Stash the rest--keep-index
  3. 3Run testsonly staged code is on disk
  4. 4Committhe verified change
  5. 5Popunstaged half returns

3. Move work to the right branch

You notice you have been editing on main when the work belongs on its own branch. Because a stash belongs to the repository and not to a branch, you can park the work, create the correct branch, and pop it there.

bash
git stash -u
git switch -c feat/coupons
git stash pop

Three commands, work lands on the right branch

Common mistake

Popping on the wrong branch is just as easy as popping on the right one, since Git will not warn you. Read the WIP on <branch> text in git stash list before you pop.

When a stash goes wrong: rescue and recovery

Two things can go wrong with a stash: it no longer applies cleanly because the branch has moved on, or it has been dropped. Both are fixable, and both are easier if you stay calm and avoid making new changes first.

4. Rescue a stash that no longer applies

If pop conflicts badly, stop fighting it. git stash branch creates a new branch at the commit the stash was made from, applies the stash there where it fits perfectly, and drops the entry once that succeeds. Commit on the new branch, then bring the result back with a merge or a cherry-pick, resolving the conflicts once, deliberately.

bash
git stash branch salvage stash@{0}
git add -A && git commit -m "Salvaged checkout work"
git switch main
git cherry-pick salvage   # or: git merge salvage

Replay the work on its original base, then move it

5. Recover a dropped stash

A dropped or cleared stash is not deleted, only unreachable. It stays in the object database until garbage collection prunes it, which is roughly two weeks by default (gc.pruneExpire). Ask git fsck for unreachable commits, then identify yours: a stash commit has a message that starts with WIP on or the label you gave it. Once you know the SHA, apply it like any stash.

bash
git fsck --unreachable | grep commit
git show <sha>            # is this the one?
git stash apply <sha>
git branch rescue <sha>   # safer: pin it to a branch

Dig the commit out, check it, apply it

Lost a stash?
Pin it to a branch

Once you have found a lost stash, create a branch at its SHA immediately. A branch makes it reachable, so gc can no longer collect it.

Worktree recipes for parallel work

When the other task will take hours, or you need two versions alive at once, a second checkout beats juggling the stash. Each recipe below ends with removing the worktree, because a worktree you forget about is just clutter that holds a branch hostage.

6. Review a PR without disturbing your work

Fetch the pull request into a local branch, check it out in its own directory, and review it there. Your feature tree, with its dirty files and warm build, stays exactly as it was.

bash
git fetch origin pull/123/head:pr-123
git worktree add ../review pr-123
cd ../review && npm ci && npm test
# read the code, leave comments
cd - && git worktree remove ../review

The pull/N/head ref is a GitHub convention

7. Emergency hotfix with a hot build

Your main tree has a ten-minute build you do not want to throw away. Create a new branch from the remote main in a sibling directory, fix, test, push, then remove it.

bash
git worktree add -b hotfix-501 ../hotfix origin/main
cd ../hotfix
# fix, test
git push -u origin hotfix-501
cd - && git worktree remove ../hotfix

Nothing was stashed, so nothing can be lost

8. Side-by-side comparison

To see how behaviour changed between two releases, check out both at once. Each directory has its own files, so you can run both programs and diff what they print.

bash
git worktree add ../v1 v1.0
git worktree add ../v2 v2.0
(cd ../v1 && ./run.sh > /tmp/v1.out)
(cd ../v2 && ./run.sh > /tmp/v2.out)
diff /tmp/v1.out /tmp/v2.out

Two live versions, one object database

RecipeStarting pointWhy a worktree
Review a PRpr-123 branchYour dirty tree stays untouched
Hotfixorigin/mainYour build caches stay hot
Comparev1.0 and v2.0 tagsBoth versions exist on disk at once

Bisect and the clean sweep

9. Bisect without losing your workspace

Bisecting checks out many commits in turn, which would trample your working tree. Do it in a detached throwaway worktree instead. Bisect state lives in refs/bisect/*, which is per-worktree, so your main checkout is not involved at all.

bash
git worktree add ../bisect --detach
cd ../bisect
git bisect start HEAD v1.0
git bisect run npm test
git bisect reset
cd - && git worktree remove ../bisect

The culprit is found without touching your tree

10. Clean sweep

Every so often, audit what you have left lying around. List the worktrees, remove each finished one with git worktree remove, and run prune to clear entries whose directories were deleted by hand. Then look at the stash list: each entry should be either applied, turned into a branch, or dropped on purpose.

bash
git worktree list
git worktree remove ../old-experiment
git worktree prune
git stash list
git stash drop stash@{2}   # deliberately, one at a time

Prefer drop over clear, so you choose each deletion

ChoreCommandClears
Finished checkoutgit worktree remove <path>Directory and its admin entry
Hand-deleted directorygit worktree pruneStale entry in the list
Stale parked workgit stash drop stash@{n}One chosen entry
The pattern behind every recipe

Stash time-multiplexes one tree for short detours, and worktrees give each long task its own directory over one history. Either way, finish by cleaning up, so nothing valuable hides in a stash or a forgotten checkout.

Part 13 · Summary & Decision Table

The Two Models in One Glance

Everything in this chapter comes down to two different answers to the same question: how do I do a second thing without wrecking the first? Both answers are built from ordinary Git objects, so the quickest way to keep them straight is to remember what each one actually is on disk.

A stash is a stack of hidden commits. Each entry is a commit with two parents, or three when you pass -u. Parent 1 is the HEAD you stashed from, parent 2 holds the index state, and the optional parent 3 holds untracked files. The newest entry sits under refs/stash, and the older ones are that ref's reflog, which is why you address them by position, as in stash@{0} and stash@{1}. All of it lives in the one working tree you already have.

A worktree is an extra working directory. It gets its own HEAD and its own index, but it reads and writes the same object store as the main checkout. Git keeps its bookkeeping in .git/worktrees/, one folder per linked tree, and a .git file inside the new directory points back at that folder.

StashWorktree
What it isHidden commits stacked under refs/stashAn extra directory with its own HEAD and index
Parents or links2 parents, or 3 with -u.git file pointing into .git/worktrees/<name>
How you address itReflog position, stash@{n}A path on disk, shown by git worktree list
Working trees involvedOne, reused over timeMany, alive at once
Object storageShared repositoryShared repository
bash
git cat-file -p refs/stash      # tree plus 2 or 3 parent lines
git stash list                  # the reflog of refs/stash
git worktree list               # path, HEAD sha, branch per tree
ls .git/worktrees               # one admin folder per linked tree

Four commands that show both models directly

One-line recall

Stash time-multiplexes one tree: the same files hold different work at different moments. Worktrees space-multiplex many trees over one history: different work sits in different directories at the same moment.

apply, pop and the Habits That Prevent Pain

The difference between apply and pop is small but it is where most stash accidents start. apply restores the changes and always keeps the entry. pop is an apply followed by a drop, and it drops only when the apply succeeded cleanly. After a conflict the changes are partly in your tree and the entry is still on the stack.

What pop does with the entry

Because of that, never assume a scripted pop finished the job. Check its exit status, then read git stash list and confirm the entry is gone. If you resolve a conflict and commit while the entry is still there, you will pop the same change again days later and apply it twice.

bash
git stash push -u -m "wip: checkout flow"
git stash branch salvage stash@{0}     # promote to a real branch
git worktree remove ../hotfix          # clean removal
git worktree prune                     # only after a manual rm

The four habits as commands

A dropped or cleared stash is not erased at once. Its commit becomes unreachable, and git fsck --unreachable can still find it until garbage collection removes it, which is about two weeks by default (the gc.pruneExpire setting). That is a safety net, not a plan, so branch anything you care about.

Common mistake

Running git worktree remove is skipped and the directory is deleted with rm -rf instead. Git keeps listing a tree that no longer exists. Run git worktree prune to clear the stale entry.

Common mistake

Plain git stash with no -u leaves new files in place. They survive the branch switch and then collide with the other branch's files, and an unlabeled entry gives you no clue what was inside.

The Decision Table

When an interruption arrives, ask how long it will last, how much state you would lose, and how isolated the second piece of work needs to be. The table below maps each situation to one tool, and the flowchart after it walks the same questions in order.

SituationChooseWhy
Interruption under 10 minutes, small diffStashInstant to save and restore, history stays clean
Parallel work, expensive build state, or side-by-side comparisonWorktreeEach tree keeps its own warm build and both versions stay visible
Need isolated config or hooks, or a different remoteSeparate cloneA worktree shares config, hooks and remotes with the main repo
Clean tree, just moving aroundgit switchNo admin overhead at all
Which tool for this interruption?

The two tools also combine well. A common sequence is to stash what you are holding, open a worktree for the urgent fix, push it, remove the worktree, and pop the stash to carry on.

bash
git stash push -u -m "wip: cart"
git worktree add -b hotfix-501 ../hf origin/main
# fix, test, push from ../hf
git worktree remove ../hf
git stash pop
git stash list                 # confirm the entry is gone

Interrupt, hotfix, resume

Remember this

Stash time-multiplexes one tree; worktrees space-multiplex many trees over one history. Label with -m, include untracked with -u, branch what matters, and verify git stash list after any conflict.

Part 14 · Check yourself

Quiz

Each question describes a situation. Work out the answer before you open it. The questions are about what Git does, not about remembering a command name.

You run git stash pop and it stops with a merge conflict in parser.js. You fix the file, git add it and commit. Weeks later git stash list is run. What do you expect to see, and what goes wrong if you pop again?
  • The entry is still listed. A conflicted pop applies only part of the change and keeps the entry, and resolving the conflict does not drop it.
  • Popping again reapplies the same change on top of the commit you already made, so you get a duplicate or a fresh conflict.
  • Run git stash list right after resolving a conflicted pop, and use git stash drop yourself once the result is good.
git stash pop
# CONFLICT (content): Merge conflict in parser.js
# ...fix parser.js, git add, git commit...
git stash list
# stash@{0}: WIP on feature: 3f2a1 lexer
Your tree has an edited app.js and a brand-new file notes.txt that was never added. You run git stash, then git clean -fd. What happens to notes.txt, and could the stash have saved it?
  • notes.txt is gone for good. A plain git stash ignores untracked files, so it was never stored, and git clean -fd deleted it from disk.
  • The edit to app.js is safe on the stash stack, but the new file was never recorded in any commit.
  • With git stash push -u the file would have been saved as the third parent of the stash commit. Default to -u.
git stash
git clean -fd
main is checked out in your original directory. You run the command below. What does Git do, and what are two correct ways to get what you wanted?
  • It refuses with fatal: 'main' is already checked out at '<path>'. A branch can be checked out in only one worktree at a time.
  • Use git worktree add --detach ../x main for a throwaway detached checkout.
  • Or make a new branch off it with git worktree add -b review-main ../x main.
  • --force would let it through, but then two indexes race on one ref.
git worktree add ../x main
The stack holds three entries: A is stash@{0}, B is stash@{1}, C is stash@{2}. A script wants to delete A and B and runs the two commands below. Which entries are left?
  • Only B is left. Stash entries are reflog positions of refs/stash, so indices renumber after every drop.
  • After the first drop, B becomes stash@{0} and C becomes stash@{1}.
  • The second command therefore drops C, not B.
  • Hardcoded indices break scripts. Drop from the highest index down, or look entries up by message first.
git stash drop stash@{0}
git stash drop stash@{1}
In a linked worktree ../hf you run git stash push -u -m "wip: cart". You switch to your main directory and run git stash list. Is the entry there, and why?
  • Yes. The entry is listed in the main directory too.
  • HEAD, the index and the working files are per-worktree, but refs/stash is shared like branches and tags, so the stack is common to every worktree.
  • That makes the label matter. Read the WIP on <branch> or On <branch> text before you pop, because Git will not stop you from popping on the wrong branch.
cd ../hf
git stash push -u -m "wip: cart"
cd ../main-dir
git stash list

Summary

  • Stash time-multiplexes one working tree. Each entry is a hidden commit with two parents, or three with -u, kept in the reflog of refs/stash.
  • A worktree is an extra directory with its own HEAD and index over a shared object store. Use one for parallel work, hot builds and side-by-side comparison.
  • apply keeps the entry and pop drops it only on a clean apply. After any conflict, check git stash list yourself.
  • Label every stash with -m and include untracked files with -u. The defaults cause most stash pain.
  • Stash indices shift when you drop an entry, and refs/stash is not storage. Promote anything valuable to a branch with git stash branch.
  • One branch can be checked out in only one worktree. Use --detach or a new branch when Git says it is already checked out.
  • Remove worktrees with git worktree remove and run git worktree prune after any manual deletion.