Agentic Coding

Git Stash and Worktrees: The Features I Wish I'd Learned Sooner

How I stopped polluting main with half-finished work — the two Git features that changed how I context-switch

For the longest time, my Git workflow had one move. I'd create a feature branch from main, build something, and then — inevitably — something else would come up. A bug. A new feature. A code review request. And every single time, I'd do the same thing: panic-merge my half-done work into main just so I could "clear the deck" and start fresh.

I was polluting main with broken code. I knew it was bad. I just didn't know there was a better way.

This post is the conversation I wish someone had had with me. It goes through git stash and git worktree in the exact order I discovered them — from the problem, to the first fix, to the better fix, to the one that actually changed how I work.

The Bad Habit: Merging to "Clear the Deck"

Here's what I used to do. Every time:

# Working on a feature, halfway done
git switch -c checkout-redesign
# ... build stuff, not finished, not tested ...

# Boss: "New bug, fix it now"
# Me: "Crap."

git switch main
git merge checkout-redesign   # WHY. Half-done code. In main.
git switch -c fix-urgent-bug
# ... fix the bug on a branch with broken feature code mixed in ...

Every merge like this left a scar. main had commits that didn't belong there. Feature branches had mangled histories. And I was doing it multiple times a week because I didn't know I had options.

The core problem: I thought branches needed to be "finalized" before I could move on. I treated them like tasks that had to be closed out. But a branch is just a label pointing at a commit. It doesn't need to be done. It doesn't need to be merged. It just needs to exist.

main ─── ● ─── ● ─── ●
           \
            ● ─── ● (checkout-redesign — half done, just sitting there)
           \
            ● ─── ● (another-feature — also half done, no problem)

You can have twenty branches with incomplete work. They don't interfere with each other. The only thing that stops you from switching between them is uncommitted changes in your working tree.

Discovering git stash

So the first real unlock was: you don't need to merge to switch branches. Just switch:

git switch main                 # go back to main
git switch -c new-feature       # branch off from main
# work on the new thing
git switch checkout-redesign    # come back — everything is as you left it

But there's a catch. If you have uncommitted changes on checkout-redesign, Git will block the switch:

error: Your local changes to the following files would be overwritten by checkout:
    src/checkout.js
Please commit your changes or stash them before you switch branches.

And sometimes you're not ready to commit. The code is a mess. It doesn't compile. You're halfway through a thought. This is where git stash comes in.

What stash actually does

git stash takes all your uncommitted changes and shelves them. It cleans your working tree so you can switch branches freely. Later, you bring them back.

# Halfway through a feature, need to switch context
git stash                       # shelve all uncommitted changes
git switch main                 # now this works — working tree is clean
git switch -c fix-urgent-bug    # create and switch to fix branch
# ... fix the bug, commit it ...
git switch checkout-redesign    # come back
git stash pop                   # your half-done work is restored

This alone was a huge upgrade from merging half-done code into main. But I quickly realized stash does more than just save you from interruptions.

Stash is a Stack: Last In, First Out

Every time you run git stash, it pushes your changes onto a stack. The most recent stash is at the top. git stash pop always pops from the top.

Think of it like a stack of plates:

git stash save "A"     git stash save "B"     git stash save "C"

  |     |                |     |                |  C  |
  |     |                |  B  |                |  B  |
  |  A  |                |  A  |                |  A  |
  +-----+                +-----+                +-----+

The last plate you put on top (C) is the first one you grab when you pop:

git stash pop    # → gives you C. Stack is now [B, A]
git stash pop    # → gives you B. Stack is now [A]
git stash pop    # → gives you A. Stack is now empty

You can see the whole stack any time:

git stash list
# stash@{0}: WIP on checkout-redesign: abc1234 Approach C
# stash@{1}: WIP on checkout-redesign: def5678 Approach B
# stash@{2}: WIP on checkout-redesign: ghi9012 Approach A

And — this is the key — you don't have to pop in order. You can reach past the top entries and grab a specific one:

git stash pop stash@{2}     # grabs A directly. B and C stay on the stack.

This changes everything about how you can experiment.

The "Try Multiple Approaches" Workflow

Here's the workflow that made me realize stash was more than just an interruption handler. It's an experimentation tool.

You've committed a stable base on your feature branch. Now you want to try three different approaches to the same problem — but without losing any of them:

# On feature-x, base is committed
git commit -m "base for experimenting"

# Try Approach A
# ... edit, edit, edit ...
git stash save "Approach A"

# Try Approach B (working tree is clean again, starting from same base)
# ... edit, edit, edit ...
git stash save "Approach B"

# Try Approach C
# ... edit, edit, edit ...
git stash save "Approach C"

Now all three approaches live on the stash stack. You decide Approach A is the winner. You don't need to pop C, then B, just to reach A. You jump straight to it:

git stash pop stash@{2}    # A is back in your working tree

# Test it, confirm it's the one, commit
git commit -m "Feature: final approach using recursion"

# Clean up the losing approaches
git stash clear             # or drop them one by one

Pro tip: Use git stash apply instead of pop if you want to bring a stash back but keep it on the stack too. apply is a copy; pop is a move.

This was my "aha" moment. Stash isn't just a temporary shelf — it's a lightweight branching mechanism. Not full branches with commit history, but a stack of diffs you can flip between on the same base commit.

Where Stash Falls Short

For quick experiments and short interruptions, stash is perfect. But the longer I used it, the more I hit its limits.

1. State loss. When you stash and switch branches, your dev server dies. Your editor tabs change to match the new branch's files. Breakpoints and debug state vanish. Terminal history is still there, but the context is gone. You're tearing down and rebuilding your workspace every time you context-switch.

2. No side-by-side comparison. You can't look at Approach A and Approach B at the same time. You're flipping between them with stash pop and stash save, holding one in your head while you evaluate the other. For anything that takes more than a few minutes to assess, this is exhausting.

3. Fragile state. Stash only tracks changes to tracked files by default. New untracked files need git stash -u. Ignored files? Not stashed. IDE config, temp files, local .env changes — easy to forget and lose. And if you stash from one branch and try to pop on another, you're asking for merge conflicts.

4. Bad for long-running work. If each "approach" takes days, not minutes, the stash workflow becomes painful. You're stashing gigabytes of state, popping, re-stashing, over and over. It's friction that compounds.

I needed something that let me live in two branches at the same time without tearing anything down.

Enter Worktrees

git worktree lets you check out multiple branches simultaneously — each in its own directory. Not separate clones. Not separate repos. Same .git/, separate working directories.

It is NOT a clone

This is the most important thing to understand. A clone duplicates the entire .git/ directory — all commits, all objects, all history. A worktree shares .git/. The new directory just gets a tiny pointer file that says "my real git data is over there →".

CLONE (separate repos):              WORKTREES (shared repo):

  /project                             /project
  ├── .git/  (all history, 500MB)      ├── .git/  (all history, 500MB)
  ├── src/                             ├── src/    ← feature-a checked out
  └── ...                              └── ...

  /project-clone                       /project-experiment
  ├── .git/  (all history AGAIN,       ├── .git → pointer back to /project/.git
  │           500MB duplicate)         ├── src/    ← main checked out
  ├── src/                             └── ...
  └── ...

Creating a worktree takes seconds, not minutes. It's cheap because it only copies the checked-out files — the actual source code at that branch's state. The entire git database is shared.

One restriction: You can't have the same branch checked out in two worktrees at once. Each worktree must be on a different branch (or detached HEAD). That's the only real constraint.

What scope does a worktree operate at?

git worktree always operates at the git repository level. You run it inside a repo, and it creates a full working copy of that repo. You can't create a worktree for just one subfolder — it's all or nothing.

If your project is a monorepo with multiple git repos, you'd cd into each and create worktrees independently:

cd /company-app/web
git worktree add ../../web-hotfix main

cd /company-app/api
git worktree add ../../api-hotfix main

But 99% of the time, you're in one repo, and you create one worktree for one purpose. The scope is the repo you're standing in.

Case Study: The E-Commerce Checkout Refactor

Let me ground this in something real. Here's a scenario that would have broken me before I knew about worktrees — and how it plays out with them.

Day 1 — Start the feature

git switch -c checkout-redesign
# Build the new checkout UI, wire up the cart, half-way through.
# Dev server running on :3000. Editor open with 8 tabs.
# Tests are red. It's a work in progress.

Day 2 — Emergency

Boss: "Customer credit cards are being double-charged. Fix it now."

Old me: Panic. Stash everything. Tear down the dev server. Switch branches. Hope nothing breaks. After the fix, unstash, restart everything, try to remember where I was.

With worktrees:

# Don't touch the checkout-redesign branch.
# Don't stash. Don't stop the dev server.
# Just spawn a worktree from main:

git worktree add ../shop-hotfix main

cd ../shop-hotfix
git switch -c fix-double-charge
# Write the fix. Commit. Push. Merge. Deploy.

# Meanwhile, /shop (original) still has:
#   - checkout-redesign checked out
#   - dev server running on :3000
#   - all 8 editor tabs intact
#   - half-done files untouched

# Fix deployed. Boss happy.
cd ../shop
# Everything exactly as you left it.

Day 5 — Spike two approaches

You've got the checkout working, but the payment integration has two possible libraries: Stripe SDK vs a raw API approach. Each takes a day to spike out. You want to try both and compare.

# /shop has the Stripe approach in progress

# Spawn a second worktree for the raw API spike:
git worktree add ../shop-raw-api checkout-redesign

cd ../shop-raw-api
# Build the raw API approach here

# Now you have two directories, two editors, side by side:
#
#   /shop (Stripe)                   /shop-raw-api (raw API)
#   ┌─────────────────────┐         ┌─────────────────────┐
#   │ const stripe = ...  │         │ const resp = await  │
#   │ await stripe.charge │         │   fetch('/api/pay') │
#   └─────────────────────┘         └─────────────────────┘
#
# Two dev servers on different ports. Compare behavior.
# Decide Stripe is better. Delete the experiment:
rm -rf ../shop-raw-api
git worktree prune   # tell git the worktree is gone

Day 10 — Code review splits

You've opened a PR for checkout-redesign. Reviewer says: "Looks good, but can you split this into two PRs? The cart changes should be separate from the payment changes."

Old me would have spent an afternoon untangling commits. With worktrees:

# In /shop, you're mid-refactor on checkout-redesign

# Spawn worktree from main, cherry-pick only cart commits:
git worktree add ../shop-cart-only main
cd ../shop-cart-only
git cherry-pick <cart-commit-1> <cart-commit-2>
# Push as a separate PR

# /shop still has the full checkout-redesign in progress.
# No undoing. No rebasing. No pain.

The Mental Model

Here's how I think about this now:

                    main
                      │
    ┌─────────────────┼─────────────────┐
    │                 │                 │
checkout-redesign  fix-cc-bug     spike-raw-api
 (main worktree)  (hotfix wt)     (experiment wt)
    │
  still working,
  dev server up,
  nothing disturbed

Every vertical bar is a worktree — a separate directory on disk, each with a different branch checked out. Same repo, same .git/, independent workspaces.

A branch is the what (which commit am I on). A worktree is the where (which directory on disk holds these files). They're separate concepts that happen to work beautifully together.

When to Use What

ScenarioToolWhy
"Brb, quick 10-min fix on main"git stashFast, zero setup, pop and done
"Try 3 quick approaches on same commit"git stash stackLIFO stack is perfect for short experiments
"Emergency production fix while mid-feature"git worktreeDon't disturb your working state
"Compare two approaches side-by-side"git worktreeTwo editors, two dev servers, real comparison
"Long-running experiment while main work continues"git worktreeMonths can pass, worktrees don't expire
"Need two branches open in two editors"git worktreeThis is the entire point of worktrees
"Code review asks to split into multiple PRs"git worktreeCherry-pick into separate worktrees, no undoing

And here's the thing: stash and worktrees complement each other. They're not competing solutions. Stash is for quick context switches — the "oh wait, let me check something on main" moments. Worktrees are for when you need to genuinely live in two branches at once.

I still use both. But I reach for worktrees way more often than I used to, because once you stop treating branch-switching as a teardown-rebuild cycle, your development rhythm gets a lot smoother.

The Real Lesson

The actual problem was never about stash or worktrees. It was about a broken mental model. I thought a branch was a task that had to be completed before I could move on. It's not. A branch is a label. It points to a commit. It can sit there for weeks. You can have twenty of them. They don't cost anything.

Once that clicked, everything else followed. Stash became my quick-undo and experiment-stack tool. Worktrees became how I handle anything that takes longer than 10 minutes. And I stopped merging half-done code into main.

That last part alone was worth the learning curve.