The problem is two problems, not one

Switching branches in a single working directory conflates two genuinely different things: which source files are checked out, and what's been built or installed on top of them. Git handles the first part fine — that's literally what checkout does. It has no opinion at all about the second: node_modules, compiled build output, a Python virtualenv, anything your toolchain generated for the branch you were just on, all of it just sits there, potentially wrong for the branch you switched to, until something rebuilds it or you remember to clean it yourself.

The pairing that actually fixes both halves

Git already has a feature for the first half: git worktree gives a second branch its own working directory — git worktree add ../feature-a feature-a — while sharing the same underlying repository history and objects with the main checkout. No stashing, no losing your place in one branch to look at another; both exist as real directories on disk simultaneously. Pair that with the second half: one Velo workspace per worktree, each cloned from the same base image, so each branch also gets its own isolated toolchain, its own node_modules, its own build cache — none of it shared, none of it stale from a branch you're not currently working on.

What actually lives where

This follows the same host/guest split already covered in the one-workspace-per-project post: each git worktree directory is its own shared folder, mounted into its own workspace, so the source files and git history live on the host where your editor already expects them. What's different per branch lives inside each workspace's own disk instead — dependencies, build artifacts, anything a fresh install would generate — cloned once from a shared base image so every new branch workspace starts from the same known-good toolchain rather than a fresh setup each time.

main
 ├── worktree: main        → workspace: proj-main
 ├── worktree: feature-a   → workspace: proj-feature-a
 ├── worktree: feature-b   → workspace: proj-feature-b
 └── worktree: experiment  → workspace: proj-experiment

Naming and cleanup

Name each workspace after its branch, the same discipline the base-image naming post argues for — proj-feature-a rather than a generic name you'll have three duplicates of within a week. This is a middle case in the spectrum covered between disposability and snapshots: a per-branch workspace isn't thrown away after one run the way a CI workspace is, but it isn't meant to live forever either. Delete it (and its worktree) when the branch merges or gets abandoned, the same moment you'd delete the branch itself.

When this is overkill

A one-line hotfix branch doesn't need its own VM — the overhead of a new worktree and a new workspace costs more than the branch-switching friction it would save. This pattern earns its keep specifically when branches have genuinely divergent dependencies or long-running build state worth protecting from each other, not as a blanket habit for every branch you ever create.

Related reading: one workspace per project, base images as versioned infrastructure, and disposable VM vs. snapshot. Or download Velo Workspaces and try it yourself.