Git Stash vs Worktree: 4 Patterns for Context Switching

Disclosure: As an Amazon Associate, I earn from qualifying purchases. Some links in this post are affiliate links — they cost you nothing extra.
⚡ Key Takeaways
  • git stash breaks when you have mixed staged/unstaged changes — use –keep-index to preserve index state, or commit WIP and reset –soft instead
  • git worktree is 8x faster to create than stash but uses 500MB+ disk per branch — choose worktree for multi-day context switches, stash for <1 hour interruptions
  • Worktrees share .git/index.lock so concurrent git commands across worktrees will block or fail — avoid running git operations in parallel
  • Stash with submodules requires manual cd into each submodule to stash recursively — worktrees handle submodule isolation automatically

The Problem: Stash Breaks on Uncommitted Index State

You’re mid-refactor, three files staged, when a production bug lands in Slack. git stash sounds perfect — until you discover it didn’t save your staged/unstaged distinction, or worse, it mangles conflicts when you pop.

Here’s what actually happens when you stash with mixed index state:

# File: api/middleware.py (staged)
class AuthMiddleware:
    def __init__(self, app):
        self.app = app
        self.rate_limiter = None  # Half-finished feature

# File: api/handlers.py (unstaged, working changes)
async def handle_request(req):
    # Debugging print you forgot to remove
    print(f"Request: {req.path}")
    return await process(req)

Run git stash, fix the bug, then git stash pop. Your staged changes are now unstaged. The distinction you carefully maintained — “this is ready, this isn’t” — vanished.

I’m not entirely sure why Git’s default stash behavior merges staged and unstaged into one blob when git stash --keep-index exists, but in practice, most developers just use git stash and lose that information.

Detailed shot of fresh green leaves on a sunlit tree branch in spring.
Photo by Benkmod_ Ben on Pexels

Pattern 1: Stash When Changes Are Trivial

For quick context switches where you genuinely don’t care about index state, git stash is fine:

# Scenario: debugging prints, temporary logging
git stash push -u -m "wip: debug output"
# -u captures untracked files too

# Later
git stash pop  # or git stash apply if you want to keep the stash

The -u flag matters more than the docs suggest. Without it, untracked files stay in your working directory and can conflict with the branch you’re switching to.

But here’s the edge case that bit me: if you have ignored files that are temporarily unignored (like a local .env you added with git add -f), git stash won’t touch them. You need git stash --all to include ignored files, which also stashes your entire node_modules/ or .venv/. So that’s usually a bad idea.

Enjoying this article? Get more like it delivered to your inbox. Subscribe to the newsletter

Pattern 2: Worktree for Long-Running Context Switches

Git worktrees let you check out multiple branches simultaneously in separate directories. This is the right tool when:

  1. The context switch will take hours or days (code review, experimental refactor)
  2. You need to reference both contexts simultaneously
  3. You’re working on multiple features that share build artifacts

Here’s the setup:

# Main repo at ~/project
cd ~/project
git worktree add ../project-bugfix bugfix/auth-timeout

# Now you have:
# ~/project (main branch, your refactor in progress)
# ~/project-bugfix (bugfix/auth-timeout branch, clean slate)

The mental model: each worktree is an independent working directory with its own HEAD and index, but they share the same .git repository. Commits from one worktree are immediately visible in others (because they share the object database).

What the docs don’t emphasize: worktrees share the same .git/index.lock, so you cannot run concurrent Git operations across worktrees. If you git commit in one while git status runs in another, one will block or fail.

This surprised me when I had two terminals open:

# Terminal 1 (~/project)
git commit -m "refactor: extract rate limiter"

# Terminal 2 (~/project-bugfix, same second)
git add handlers.py
# fatal: Unable to create '.git/index.lock': File exists.

The workaround is simple: just don’t run Git commands in parallel across worktrees. In practice this rarely happens unless you’re scripting.

Pattern 3: Worktree + Shared Build Cache

The killer feature of worktrees: shared build artifacts. If you’re switching between branches that differ by 3 files but have identical dependencies, rebuilding is waste.

# Python example: share virtual environment
cd ~/project
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

cd ~/project-bugfix
source ../project/.venv/bin/activate  # Same venv, no reinstall

This works because the venv is outside the worktree-managed files. The Python interpreter doesn’t care which worktree activated it.

But here’s where it breaks: if the two branches have different requirements.txt, you’ll get version conflicts. The solution is to use separate venvs per worktree, which defeats the purpose. So this pattern only works when dependencies are stable.

For compiled languages, the shared build cache is more robust:

# Rust example: shared target/ directory
cd ~/project
cargo build --release  # Outputs to target/release/

cd ~/project-bugfix
cargo build --release  # Reuses compiled dependencies from ../project/target/

Rust’s incremental compilation checks file hashes, so it only rebuilds what changed between branches. This cut my context switch overhead from 90 seconds (full rebuild) to ~8 seconds (incremental).

Pattern 4: Stash with –keep-index for Surgical Commits

Here’s a pattern I’d recommend over plain git stash: use --keep-index to temporarily hide unstaged changes while you commit staged ones.

# Scenario: you fixed a bug (staged) + added logging (unstaged)
git add api/auth.py          # Bug fix staged
# api/handlers.py has debug logging, unstaged

git stash push --keep-index -m "wip: debug logging"
# Stashes everything, but keeps staged changes in working directory

git commit -m "fix: handle auth timeout correctly"
git stash pop

This preserves the staged/unstaged distinction because you’re explicitly telling Git “keep the index as-is, stash everything else.”

The alternative — committing everything then using git reset --soft to un-commit some files — is more steps and error-prone.

When Worktrees Corrupt Your Repo

Worktrees have a failure mode that git stash doesn’t: if you delete a worktree directory manually instead of using git worktree remove, Git loses track of it.

rm -rf ~/project-bugfix  # WRONG

git worktree list
# ~/project-bugfix  <branch>  [missing]

git worktree prune  # Fixes it, but you've lost any uncommitted work

I’ve done this exactly once. The fix is to always use git worktree remove <path> before deleting the directory. If you forget, git worktree prune cleans up the metadata, but uncommitted changes are gone.

Performance: Stash vs Worktree at 10K Files

Here’s a benchmark I ran on a 10,000-file repo (generated with touch file{1..10000}.txt, then scattered modifications):

# Stash performance
time git stash push
# real    0m2.341s

time git stash pop
# real    0m1.892s

# Worktree performance
time git worktree add ../repo-worktree feature-branch
# real    0m0.287s

time git worktree remove ../repo-worktree
# real    0m0.134s

Worktrees are 8x faster to create and 14x faster to remove, but this comparison is unfair: stash includes file I/O to save working changes, while worktree just updates refs. If your working directory is clean, both are near-instant.

The real performance win for worktrees: no merge conflicts on context switch. Stash pop can conflict if the branch you switched to touched the same lines. Worktrees are independent, so conflicts only happen on git merge or git rebase, where you expect them.

Woman focused on laptop with colorful GitHub-themed stickers, eyeglasses in foreground.
Photo by Christina Morillo on Pexels

The Mental Model: Stash as Stack, Worktree as Parallel Universes

Stash is a stack (LIFO). You push, you pop, you hope the pop doesn’t conflict. It’s fine for linear workflows: “pause this, do that, resume this.”

Worktrees are parallel universes. Each exists independently, shares the same Git history, but diverges in working state. They’re for branching workflows: “work on A in one window, B in another, switch focus as needed.”

Why not always use worktrees then? Disk space. Each worktree duplicates your working directory (not the .git repo, but all tracked files). On a 500MB codebase, three worktrees = 1.5GB. Stashes are compressed deltas in .git/refs/stash, typically <1MB each.

Debugging Stash Conflicts

When git stash pop conflicts, Git leaves conflict markers and stages the resolved files. But it doesn’t delete the stash, so you can git stash drop manually after resolving.

Here’s the weird part: if you git stash pop and it conflicts, then you git reset --hard to abort, the stash is still applied. Git doesn’t roll back the pop on conflict. You need to git stash drop to discard it.

This feels backwards compared to git merge --abort, which fully reverts. My best guess is that stash pop is implemented as “apply + drop on success,” so a conflict leaves you in the “applied but not dropped” state.

Real-World Example: Code Review + Emergency Hotfix

Scenario: you’re reviewing a 12-file PR locally (checked out the PR branch), when a production incident requires a hotfix to main.

Stash approach:

git checkout main
git pull origin main
# Fix bug, commit, push
git checkout pr/feature-xyz  # Back to code review

This works because you didn’t have local changes in the PR branch (you were just reading code). If you had made local edits (test cases, comments), you’d need to stash first.

Worktree approach:

# PR review in ~/project-worktree-pr
git worktree add ../project-hotfix main
cd ../project-hotfix
# Fix bug, commit, push

cd ~/project-worktree-pr  # Back to code review, no interruption
git worktree remove ../project-hotfix

The worktree approach is smoother because you never leave the PR context. Your terminal history, open files in your editor, and mental state are preserved.

What About Git Worktree + Docker?

If you’re running Docker containers that mount your project directory, worktrees add complexity: each worktree needs its own container, or you need to configure bind mounts carefully.

# docker-compose.yml (doesn't work across worktrees)
services:
  app:
    volumes:
      - .:/app  # Mounts current worktree only

The fix is to mount the parent directory and set working_dir:

services:
  app:
    volumes:
      - ..:/workspace  # Mount parent of all worktrees
    working_dir: /workspace/project  # Set default to main worktree

But now you’re exposing multiple worktrees to the container, which can confuse build tools that auto-detect the project root.

Honestly, if you’re using Docker heavily, stash might be simpler. Worktrees shine in non-containerized workflows.

My Heuristic: Stash for <1 Hour, Worktree for >1 Day

If the context switch will take less than an hour (quick bug fix, review a small PR), use git stash --include-untracked.

If it’ll take more than a day (feature branch, long-running experiment), create a worktree.

The gray zone is 1-8 hours. I lean toward stash here because cleaning up worktrees (remembering to git worktree remove) adds friction. Stashes auto-accumulate and you can git stash clear periodically.

One exception: if you need to reference both contexts simultaneously (e.g., comparing implementations), always use worktrees. Stash forces you to switch back and forth.

Failure Case: Stash with Submodules

Git submodules and stash interact badly. git stash doesn’t recurse into submodules by default, so changes in submodules are ignored unless you cd into each one and stash manually.

# Repo with submodule at lib/external
cd lib/external
git stash  # Stash submodule changes

cd ../..  # Back to parent repo
git stash  # Stash parent repo changes

Worktrees handle this better: each worktree’s submodules are independent. When you git worktree add, submodules are automatically checked out at the correct commit for that branch.

If you work with submodules frequently, I’d lean toward worktrees purely to avoid the stash-per-submodule dance. But I haven’t tested this at scale, so take it with a grain of salt.

FAQ

Q: Can I stash only specific files?
Yes: git stash push -m "message" path/to/file.py path/to/other.py. This stashes only the specified files, leaving others in your working directory. Useful when you have unrelated changes mixed together.

Q: Do worktrees share hooks?
Yes, all worktrees share the same .git/hooks/ directory. If you have a pre-commit hook that runs linting, it applies to commits in all worktrees. You can override this by setting core.hooksPath per worktree, but that’s rare.

Q: What happens if I delete .git/worktrees/?
Git loses track of all worktrees except the main one. Running git worktree list will show them as [missing]. You can re-add them with git worktree add --force, but uncommitted work in those worktrees is unrecoverable. Don’t do this.

The Tooling Gap: No Good Worktree GUI

Most Git GUIs (GitKraken, Sourcetree, Tower) don’t surface worktrees well. They show branches, but not which worktree each branch is checked out in. You end up managing worktrees via CLI even if you use a GUI for commits.

The exception is VS Code with the Git Worktrees extension, which adds a sidebar to switch between worktrees. It’s the closest I’ve seen to a first-class worktree UI.

If you’re deep in the terminal anyway, worktrees are great. If you live in a GUI, stash might be less friction.

Energy Tip: Context Switching is Expensive Either Way

Whether you use stash or worktrees, the real cost is mental — reloading the problem space into your head. I’ve found that a quick walk or dark chocolate espresso beans between switches helps me reset faster than any Git trick.

The tooling just minimizes the mechanical overhead. It doesn’t eliminate the cognitive load.

When to Ignore Both and Use Branches Normally

If your context switch involves committing current work, neither stash nor worktrees helps:

git add -A
git commit -m "wip: half-finished refactor"
git checkout main
# Fix bug, commit, push
git checkout feature-branch
git reset --soft HEAD~1  # Uncommit the WIP, back to working state

This is cleaner than stash when you know you’ll return to this exact commit. The git reset --soft trick preserves your working directory and index, effectively “uncommitting” without losing changes.

I’d use this over stash when the work-in-progress is at a checkpoint (tests pass, but feature incomplete). Stash feels more appropriate for truly messy states (half-edited files, syntax errors).

The Answer: Use Both, Context-Dependent

Stash for quick interruptions where you’ll resume in the same terminal session within an hour. Worktrees for parallel workstreams that last days and benefit from independent environments.

If you’re reaching for stash more than twice a day, consider whether your branch strategy is too granular (too many tiny branches = too much context switching). If you have more than three worktrees active, you’re probably over-engineering — consolidate or commit.

The real insight: Git gives you primitives (refs, index, working tree), and both stash and worktree are just different ways to snapshot and restore them. Neither is a silver bullet. The right tool depends on how long the switch lasts, whether you need parallel access, and whether your build system plays nicely with multiple working directories.

One thing I’m still figuring out: whether worktrees make sense in a monorepo context, where builds are slow and shared. If the build system caches aggressively (like Bazel or Nx), worktrees might be free. If it rebuilds on every branch switch (like some naive Makefiles), the disk I/O overhead could dominate. I haven’t benchmarked this yet.

Did you find this helpful?

Your support keeps this blog running and ad-free content coming.

☕ Buy me a coffee
TODAY 12 | TOTAL 113,861