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.
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.
- 1Working treeyou edit files
- 2Indexgit add records them
- 3HEADgit commit seals them
| State | What it holds | How to look at it |
|---|---|---|
| Working tree | The files on disk, including edits you have not staged | git diff |
| Index | The snapshot you have staged for the next commit | git diff --staged |
| HEAD | The commit you are standing on | git 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.
git switch release-2.xerror: 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.
AbortingThis 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.
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.
git switch release-2.x # README.md edit only: allowed git switch release-2.x # parser.js edit as well: refused
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.
| Hatch | Speed | Isolation | Where the work lives |
|---|---|---|---|
| Commit a WIP | fast | none | A throwaway commit on your branch |
| Stash | fast | none | A temporary commit off to the side |
| Second worktree | medium | high | Another directory, same repository |
| Second full clone | slow | total | A 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.
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.
git worktree add -b hotfix-501 ../hotfix origin/main| Stash | Worktree | |
|---|---|---|
| Checkouts | One, reused | Many, side by side |
| Shares | Everything, it is the same tree | One object database |
| Lifetime of the interruption | Minutes | Hours or days |
| Cost of the switch | Write and re-apply a diff | Check out files once |
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.
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.
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.
git status --short git stash push -m "wip: parser fix" git status --short
Push with a label, then confirm the tree is clean
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:
git stash list
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 READMEThe 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: 3f2a1b7 | stash@{0}: WIP on feature-a: 3f2a1b7 |
| stash@{2}: WIP on main: 9c41d02 | stash@{1}: WIP on main: 9c41d02 |
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.
git stash show stash@{1}
git stash show -p stash@{1}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}.
| Command | Restores changes | Entry afterward |
|---|---|---|
git stash apply | yes | kept on the stack |
git stash pop | yes | deleted |
git stash drop stash@{2} | no | that one entry deleted |
git stash clear | no | every 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.
git stash apply stash@{1}
git stash drop stash@{1}Restore, check the result, then remove the entry by hand
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.
- 1Find the basethe commit the stash was made on
- 2Create the branchsalvage starts at that commit
- 3Apply the stashno drift, so no conflicts
- 4Drop the entryonly if the apply succeeded
git stash branch salvage stash@{0}
git branch --show-currentTurn 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.
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
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.
git switch feature-a
git stash push -m "wip: lexer"
git switch main
git stash popThis pop succeeds on main without any warning
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.
tree = working-tree state
what git stash show -p diffs
HEAD at stash time
the base commit
tree = index (staged) state
always present
untracked files
only with -u or -a
| Part of the entry | What it holds | Present when |
|---|---|---|
| Commit tree | Working-tree state | Always |
| Parent 1 | HEAD at stash time (the base) | Always |
| Parent 2 | Commit whose tree is the index state | Always |
| Parent 3 | Commit holding untracked files | Only 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.
git stash push -u -m "wip: parser"
git cat-file -p refs/stashThree parent lines means -u was used
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
git log --graph --oneline refs/stash*-. c7d41e2 On main: wip: parser |\ \ | | * b84e5f1 untracked files on main: 3f2a1c4 lexer | * 71d0aa9 index on main: 3f2a1c4 lexer |/ * 3f2a1c4 lexer
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.
| Step | Command | What stash@{0} is |
|---|---|---|
| Start | (three entries exist) | entry C, the newest |
| Drop the top | git stash drop stash@{0} | entry B, the old stash@{1} |
| Push a new one | git stash push -m new | entry D, and B is now stash@{1} |
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:
git config --get gc.pruneExpire git config gc.reflogExpireUnreachable 90.days
No output from --get means the default of 2 weeks applies
| Setting | Governs | Default |
|---|---|---|
gc.pruneExpire | How long a dropped (unreachable) stash commit survives on disk | about two weeks |
gc.reflogExpireUnreachable | How long older entries stay listed in the refs/stash reflog | 30 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.
git fsck --unreachable | grep commit
git stash apply c7d41e2Illustrative SHA, use the one fsck gives you
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.
sha=$(git stash create) git stash store -m "snapshot before refactor" "$sha" git stash list
Snapshot the tree without clearing it
stash@{0}: snapshot before refactorA 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.
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.
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.
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.
| Axis | apply | pop |
|---|---|---|
| Safety | Keeps a backup copy of the work on the stack | Deletes the entry once it applies cleanly |
| Hygiene | Grows a graveyard of stale entries you no longer trust | Prevents stack rot; the stack stays short |
| After a conflict | Entry exists, so you can reset with git checkout -- . and retry | Entry 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.
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.
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 testOne 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 / pop | With --index | |
|---|---|---|
| Staged changes come back as | Unstaged | Staged, as before |
| If the index cannot be rebuilt | Falls back to everything unstaged | Fails 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.
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.
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.
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.
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.
| Flag | Tracked changes | Untracked files | Ignored files |
|---|---|---|---|
| (none) | stashed | left behind | left behind |
-u | stashed | stashed | left behind |
-a | stashed | stashed | stashed |
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.
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.
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 stash | everything tracked | only the staged changes |
| Left in the working tree | the staged version | your unstaged edits |
| Typical use | test exactly what you will commit | park finished work, keep editing |
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.
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.
git stash push -m "parser work" -- src/parser.js src/lexer.js git status --short
Only the two named files are stashed.
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.
| Goal | Command shape |
|---|---|
| Split one file's edits | git stash push -p |
| Stash chosen files only | git stash push -- path1 path2 |
| Chosen directory, new files too | git 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.
git stash push -m "wip"
git stash listA clean tracked tree: no entry is created.
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.
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.
| Combination | Behaves as expected? |
|---|---|
-u with a pathspec | yes, new files under that path are included |
-a with a pathspec | yes, but check what ignored files match |
-k with -p | no, the kept index is the old staged state |
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.
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.
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
| Form | Branch afterwards | Good for |
|---|---|---|
add ../hotfix hotfix-branch | Uses the existing branch | Work that already has a branch |
add -b fix-123 ../fix-123 | Creates fix-123 | Starting a new line of work |
add --detach ../scratch HEAD | None, detached HEAD | Experiments 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.
git worktree list
/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 worktree | Separate in each worktree |
|---|---|
| Objects: commits, trees and blobs | HEAD, the checked-out branch |
| Refs: branches and tags | The index (what is staged) |
| Config and remotes | The working files on disk |
| Hooks | Per-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 by | Extra disk used | Extra fetching |
|---|---|---|
git worktree add | One checkout of files | None |
git clone | Files plus a full copy of history | A second fetch to stay current |
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.
cd ../hotfix
git stash push -m "wip: hotfix idea"
cd ../repo
git stash liststash@{0}: On hotfix-branch: wip: hotfix ideaThe 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.
| Command | What it does |
|---|---|
git worktree remove ../hotfix | Deletes the directory and its admin record together |
git worktree prune | Clears records whose directory no longer exists |
git worktree lock ../on-usb --reason "removable drive" | Protects a worktree from prune |
git worktree move ../old ../new | Relocates 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.
git worktree remove ../hotfix rm -rf ../scratch git worktree prune git worktree list
/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.
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.
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.
$ 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.
| Entry | What it holds |
|---|---|
HEAD | Which branch or commit this worktree has checked out |
index | This worktree's own staging area |
logs/ | This worktree's own reflogs, including the one for its HEAD |
commondir | A relative path back to the shared .git, normally ../.. |
gitdir | The 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.
- 1Worktree .git filegitdir: /repo/.git/worktrees/hotfix
- 2Admin folderHEAD, index, logs, commondir, gitdir
- 3commondir../.. leads to the shared .git
- 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.
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.
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.
| Ref | Scope | Why |
|---|---|---|
HEAD | per-worktree | Each checkout is on its own branch or commit |
refs/bisect/* | per-worktree | A bisect in one tree must not disturb another |
refs/worktree/* | per-worktree | A place for tools to keep private refs |
branches (refs/heads/*) | shared | One branch name means one commit everywhere |
| tags and remotes | shared | One git fetch updates every worktree |
refs/stash | shared | All 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.
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.
$ 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.
git worktree add --detach ../scratch main
git worktree add -b review-main ../r mainBoth 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.
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.
| Command | Answers | In a linked worktree |
|---|---|---|
git rev-parse --git-dir | This worktree's private admin folder | /repo/.git/worktrees/hotfix |
git rev-parse --git-common-dir | The shared .git | /repo/.git |
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.
git config extensions.worktreeConfig true
git config --worktree core.hooksPath .hooksWithout 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.
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.
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.
| Situation | Branch switch | Stash | Worktree |
|---|---|---|---|
| Tree is clean, quick hop | Best fit | Unneeded | Overkill |
| 30-second interruption, small diff | Blocked if files conflict | Best fit | Overkill |
| Work lasting hours or days | Awkward | Rots on the stack | Best fit |
| Slow build or running dev server | Invalidates caches | Caches stay but tree is rolled back | Best fit |
| Need both versions on screen | Impossible | Impossible | Best fit |
| Admin overhead | None | Low | Create 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.
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.
- 1Feature treenode_modules and target/ are warm
- 2git worktree add -b fix-88 ../fix mainnew directory, no objects copied
- 3Build and test in ../fixpays the install cost once, there
- 4Push, then remove ../fixfeature tree was never touched
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.
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.
git stash && git pull --rebase && git stash popThe && 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.
git add -p
git stash push --keep-index -m "rest"
npm test && git commit
git stash popThe 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.
| Worktree | Second clone | |
|---|---|---|
| Objects | Shared with the original | Copied |
| Fetches needed | One | One per clone |
| Config and hooks | Shared | Isolated |
| Disk cost | One checkout of files | A whole second history |
| Choose it when | You want cheap parallel work | A tool mutates repo config or hooks |
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.
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.
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.
| Operation | Time cost | Disk cost |
|---|---|---|
git stash push / pop | Size of the changed set, plus one tree write | One commit, a few trees, and the changed blobs |
git worktree add | One full checkout of the tree; bound by file count | A few hundred bytes of admin files, plus the checked-out files |
Full git clone | Copies or fetches the whole object database | The 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.
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.
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.
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?
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.
| Stash | Worktree | |
|---|---|---|
| What you must remember | Which entry is which | Which directory holds which branch |
| Typical slip | Popping onto the wrong branch | Editing or building in the wrong directory |
| Where the clutter lives | In refs/stash, out of sight | On disk and in your editor's sidebar |
| Tooling risk | Scripts that hardcode stash@{n} | Tools that assume one checkout or one .git directory |
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.
- 1Remove worktrees properlygit worktree remove ../dir
- 2Prune stale entriesgit worktree prune after any manual delete
- 3Check the stash listdrop or promote entries you no longer need
- 4Run gcnow the roots match reality
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.
| Count | Stash | Worktrees |
|---|---|---|
| 1 to 2 | Fine | Fine |
| About 5 | Starts to degrade; hard to tell entries apart | Fine for Git; watch disk |
| More than 5 | Opaque stack, easy to pop the wrong one | Disk 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.
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.
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.
- 1Stash on mainbase is the commit main pointed at
- 2Switch to release-2.xhistory has diverged from that base
- 3Popmerge against the wrong ancestor
- 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.
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.
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
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).
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.
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
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.
stash@{0}: WIP on feature-a: 3f2a1 lexer
stash@{1}: On main: wip: parser fixoutput of git stash list
| Entry | Made on | Safe to pop here? |
|---|---|---|
| stash@{0} | feature-a | Only if you are on feature-a, or you want it elsewhere on purpose |
| stash@{1} | main | Only 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 directory | git worktree remove | |
|---|---|---|
| Files | Deleted | Deleted |
| Admin records | Left stale | Cleaned up |
| Branch lock | Kept until prune | Released |
| Follow-up | git worktree prune | None |
rm -rf ../hotfix
git worktree list # still lists ../hotfix as prunable
git worktree prunecleaning 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.
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
--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.
# 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.
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
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.
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.
| Slip | Fix |
|---|---|
| Pop after switching branches | git stash branch <name> |
| Stash without -u, then clean | Default to git stash push -u |
| git stash clear to tidy | git fsck --unreachable, within the roughly two-week gc.pruneExpire window |
| Entry left after a conflicted pop | git stash list, then drop |
| Popping on the wrong branch | Read the WIP on <branch> label first |
| rm -rf on a worktree | git worktree prune |
| Add on a checked-out branch | --detach or -b |
| Hardcoded .git path | git rev-parse --git-common-dir |
| Branch deleted under a worktree | Remove the worktree first |
| Unchecked pop in a script | Test 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.
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.
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
- 1Stage one halfgit add -p
- 2Stash the rest--keep-index
- 3Run testsonly staged code is on disk
- 4Committhe verified change
- 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.
git stash -u git switch -c feat/coupons git stash pop
Three commands, work lands on the right branch
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.
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 salvageReplay 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.
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
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.
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.
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.
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
| Recipe | Starting point | Why a worktree |
|---|---|---|
| Review a PR | pr-123 branch | Your dirty tree stays untouched |
| Hotfix | origin/main | Your build caches stay hot |
| Compare | v1.0 and v2.0 tags | Both 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.
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.
git worktree list
git worktree remove ../old-experiment
git worktree prune
git stash list
git stash drop stash@{2} # deliberately, one at a timePrefer drop over clear, so you choose each deletion
| Chore | Command | Clears |
|---|---|---|
| Finished checkout | git worktree remove <path> | Directory and its admin entry |
| Hand-deleted directory | git worktree prune | Stale entry in the list |
| Stale parked work | git stash drop stash@{n} | One chosen entry |
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.
| Stash | Worktree | |
|---|---|---|
| What it is | Hidden commits stacked under refs/stash | An extra directory with its own HEAD and index |
| Parents or links | 2 parents, or 3 with -u | .git file pointing into .git/worktrees/<name> |
| How you address it | Reflog position, stash@{n} | A path on disk, shown by git worktree list |
| Working trees involved | One, reused over time | Many, alive at once |
| Object storage | Shared repository | Shared repository |
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
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.
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.
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.
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.
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.
| Situation | Choose | Why |
|---|---|---|
| Interruption under 10 minutes, small diff | Stash | Instant to save and restore, history stays clean |
| Parallel work, expensive build state, or side-by-side comparison | Worktree | Each tree keeps its own warm build and both versions stay visible |
| Need isolated config or hooks, or a different remote | Separate clone | A worktree shares config, hooks and remotes with the main repo |
| Clean tree, just moving around | git switch | No admin overhead at all |
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.
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
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
popapplies 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 listright after resolving a conflicted pop, and usegit stash dropyourself 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.txtis gone for good. A plaingit stashignores untracked files, so it was never stored, andgit clean -fddeleted it from disk.- The edit to
app.jsis safe on the stash stack, but the new file was never recorded in any commit. - With
git stash push -uthe 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 mainfor a throwaway detached checkout. - Or make a new branch off it with
git worktree add -b review-main ../x main. --forcewould 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 becomesstash@{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/stashis shared like branches and tags, so the stack is common to every worktree. - That makes the label matter. Read the
WIP on <branch>orOn <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 listSummary
- Stash time-multiplexes one working tree. Each entry is a hidden commit with two parents, or three with
-u, kept in the reflog ofrefs/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.
applykeeps the entry andpopdrops it only on a clean apply. After any conflict, checkgit stash listyourself.- Label every stash with
-mand include untracked files with-u. The defaults cause most stash pain. - Stash indices shift when you drop an entry, and
refs/stashis not storage. Promote anything valuable to a branch withgit stash branch. - One branch can be checked out in only one worktree. Use
--detachor a new branch when Git says it is already checked out. - Remove worktrees with
git worktree removeand rungit worktree pruneafter any manual deletion.