Skip to content

Worktrees ​

Separate working directories per branch, sharing one repository, for parallel development without file system conflicts.

The core rule: the directory named after the repository is a container, not a checkout. It holds the git directory and a set of worktree directories, one per branch, and nothing else. There is no working tree at the container root, not even for the default branch.

That rule governs what you create. It does not describe what you will find, and the difference matters more than it sounds: see Read any layout, create one before writing anything that inspects a repository you did not set up.

Why a container, not a checkout ​

The obvious layout, a normal checkout of main at the repository root with worktrees nested inside it (<repo>/.worktrees/<branch>/), has a silent, damaging flaw: it puts a second, fully installed copy of the project on every worktree's ancestor path.

Node resolves a bare import by walking up from the importing file, checking node_modules at each ancestor. From <repo>/.worktrees/<branch>/, that walk passes straight through <repo>/, a checkout of a different branch with its own package.json and its own installed node_modules. A worktree that never ran npm install, or installed a different dependency set, resolves into the root checkout's tree instead, silently and against the wrong branch:

$ cd <repo>/.worktrees/some-branch   # no node_modules of its own
$ node -e "console.log(require.resolve('gray-matter'))"
<repo>/node_modules/gray-matter/index.js      # the ROOT's copy, wrong branch

The failures are silent and directional: a dependency bump that still runs the old copy, a removed dependency that keeps working locally but breaks in CI, a runtime import misdeclared as a devDependency that only fails once the tree is standalone. node_modules is only the loudest symptom. Anything resolved by walking up ancestors is affected: node_modules/.bin, package.json fields (type, subpath imports, workspace-root detection), and config discovery for tools that search upward (tsconfig.json, eslint, vitest, .npmrc, .env, .editorconfig). It is not JavaScript-specific either: Bundler walks up for Gemfile, Python tooling for pyproject.toml, Cargo for the workspace root.

The container layout removes the second checkout from the ancestor path. Each worktree's nearest package.json is its own, and the only directories above it are container directories with no manifests in them. A forgotten install now fails loudly at the first import instead of resolving to a wrong answer:

$ cd <repo>/feature/auth   # no node_modules installed
$ node -e "require.resolve('leftpad')"
Error: Cannot find module 'leftpad'

Read any layout, create one ​

The container layout is a rule for repositories you set up. It is not an assumption you may make about a repository you find. Those are different jobs, and conflating them is how a tool that worked on your machine breaks on the next one.

How a clone is arranged is a fact about the machine it sits on, not about the repository. All four of these are live somewhere right now, and a fleet migrates one repository at a time, so mixed states are the normal condition rather than a transitional glitch:

LayoutShape
Lone checkout<repo>/ is a checkout, no worktrees
Nested (legacy)worktrees under <repo>/.worktrees/
Sibling (legacy)worktrees under <repo>-worktrees/
Container (create this)worktrees are children of <repo>/

Detect, never assume. git worktree list --porcelain reports every layout in the same shape, so read it rather than probing directories:

sh
git worktree list --porcelain     # every checkout, whatever the topology

A container announces itself with a bare entry for its .bare directory. That entry, like a detached HEAD, carries no branch line, and neither is a place work happens, so anything answering "where is this branch checked out" drops branchless entries. skills/github/parse-worktrees.js is the worked example, layout-agnostic by construction and tested against all four shapes.

Two consequences worth knowing before you write against a container:

  • Commands needing a work tree fail at the container root by design. git status and git rev-parse --show-toplevel both return fatal: this operation must be run in a work tree. Anything inferring "the current branch" or a project root must run inside a worktree. git worktree list, git fetch and git branch still work from the container.
  • Prose that names a directory ages badly. Prefer "the checkout that holds main" over "the root worktree" or "main/", since which of those is true depends on the layout in front of you.

To convert a repository to the container layout, see Worktree Migration. Nothing forces that conversion: a repository on a legacy layout keeps working, and every tool here is expected to keep reading it.

Tool selection: native vs. manual worktree management ​

Before following the manual git commands in this skill, check whether your runtime provides a native worktree tool. But note that the common native tools default to the broken nested layout, so they need care:

  • Claude Code (EnterWorktree): the name form creates a worktree inside .claude/worktrees/, relative to the session's working directory, which is inside the package root. Do not use it that way; it reintroduces the ancestor-contamination bug. Instead create the worktree manually from the container (git worktree add <branch> -b <branch> origin/main) and enter it by path: EnterWorktree accepts a path to an existing worktree as long as it appears in git worktree list.

  • VS Code + Copilot background agents: the runtime creates a worktree for the session automatically. You do not manage worktrees manually, but be aware its placement may not follow this layout.

  • OpenAI Codex (app): worktree creation is handled by the platform.

  • All other environments (Cursor, Aider, Cline, Roo, generic CLI): no native worktree tool exists. Follow the manual instructions below.

Decision rule: use a native tool only if you can point it at a worktree placed per this layout. Otherwise follow this skill's git commands directly.

When to use ​

Worktrees are the default here, not an optimization. Because the container has no root checkout, every branch you work on, including main, is a worktree. Common scenarios:

  • Working on a feature while reading or verifying on another branch
  • Comparing branches side by side
  • Running risky refactors in isolation
  • Multi-agent coordination where each agent needs its own branch
  • Avoiding constant stash/pop cycles

The pattern ​

Quick reference ​

All commands run from the container root (<repo>/).

ActionCommand
Creategit worktree add <branch> <branch>
New branchgit worktree add <branch> -b <branch> origin/main
Listgit worktree list
Removegit worktree remove <branch>
Prunegit worktree prune

Core structure ​

The container holds a bare git directory at .bare, a .git file pointing at it, and one directory per worktree. The default branch lives in main/ like any other branch:

project/                        # container: NO working files, NO node_modules
├── .bare/                      # the git directory (bare clone)
├── .git                        # file: "gitdir: ./.bare"
├── main/                       # worktree for the default branch
│   ├── src/
│   ├── package.json
│   └── node_modules/
├── feature/
│   └── auth/                   # worktree for feature/auth
│       ├── src/
│       ├── package.json
│       └── node_modules/
└── bugfix/
    └── login-null/             # worktree for bugfix/login-null
        └── ...

Key principles ​

  1. The container holds no working files. No package.json, no node_modules, no source at the container root. Its only contents are the git directory (.bare), the .git pointer file, and worktree directories. This is the whole point: nothing between a worktree and the filesystem root is a checkout of the project.
  2. Every worktree is a child directory, main included. There is no privileged root worktree. main/ is a worktree like every other, and every container has one, because reading, verifying, and post-merge pulls happen there.
  3. Path equals branch, one to one. The worktree directory path, the worktree name, and the branch name are the same string. Branch feature/auth means directory feature/auth/. No prefixes, no slug transforms, no .worktrees/ level.
  4. Complete isolation. Each worktree has its own working files and its own dependency install (npm install, pip install, etc.), since node_modules, venv, and similar are per-worktree.
  5. Zero conflicts. Agents and humans work in parallel without file collisions.

First-time setup (a fresh clone) ​

Set up a new container from scratch:

sh
mkdir <repo> && cd <repo>
git clone --bare git@github.com:<owner>/<repo>.git .bare
printf 'gitdir: ./.bare\n' > .git
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
git fetch origin
git worktree add -b main main origin/main

The remote.origin.fetch line is required, not optional. A bare clone ships without a fetch refspec for refs/remotes/origin/*, so without it git fetch populates nothing and origin/main never exists to branch from.

The final command creates the local main branch explicitly from origin/main. A bare clone has no local branches, so a plain git worktree add main that expects an existing local main would fail; the -b form shown above creates it. Use master in both positions where that is the repo's default branch.

To convert an existing repository that uses the old root-checkout layout, see Worktree Migration.

Creating a worktree ​

Required information ​

  1. Branch name (the branch and the worktree directory, e.g. feature/user-auth, bugfix/login-issue, quick-fix)
  2. Base branch to create from for a new branch (typically origin/main)
  3. Branch existence at origin or needs creating

Steps ​

Run from the container root:

  1. Determine branch status:

    • Locally: git branch -l <name>
    • At origin: git ls-remote --heads origin <name>
  2. Create the worktree:

    • New branch: git worktree add <name> -b <name> origin/main
    • Existing branch: git worktree add <name> <name>

    git worktree add creates intermediate directories automatically, so branch names with slashes (feature/auth) work without extra steps.

  3. Install dependencies inside the new worktree: cd <name> then npm install, pip install -r requirements.txt, or whatever the project needs. Each worktree needs its own install because dependency directories are not shared. If your shell sets NODE_ENV=production, npm install silently skips devDependencies; unset it for installs so build and test tooling is present.

Naming examples ​

The worktree directory path, worktree name, and branch name are always the same string:

BranchWorktree Path
mainmain/
feature/authfeature/auth/
bugfix/login-nullbugfix/login-null/
experiment/new-routerexperiment/new-router/
quick-fixquick-fix/

No renaming, no slug transformation, no prefixing. Branch names whose first path segment would be .git or .bare collide with the container's own entries; git rejects .git already, and you should never name a branch .bare.

Issue-backed branches ​

When the work corresponds to a tracked issue, use the pattern:

issue-<zero-padded-3-digit-number>-<slug>
IssueBranchWorktree Path
#9issue-009-integrateissue-009-integrate/
#42issue-042-cache-headersissue-042-cache-headers/
#286issue-286-nested-fencesissue-286-nested-fences/

Rules:

  • Zero-pad issue numbers to at least 3 digits so branches sort stably in alphabetical listings (issue-009 sorts before issue-042).
  • The slug describes the work (a verb or short phrase), not the artifact. Prefer integrate, refactor, fix, audit over release names or version numbers.
  • Path/branch identity still holds: the full string issue-009-integrate is the branch name, the worktree directory, and the identifier everywhere.

Working in a worktree ​

Once created, a worktree is a normal working directory. Run commands from inside it:

sh
cd feature/auth
# edit files, run tests, build, etc.
git add -A
git commit -m "Add auth middleware"
git push -u origin feature/auth

Other worktrees are unaffected by changes in this one. Note that commands requiring a work tree fail at the container root by design: git status there returns fatal: this operation must be run in a work tree. Doing work at the container root is structurally impossible, not merely discouraged. Commands that inspect the repository (git worktree list, git fetch, git branch) still work from the container, but git rev-parse --abbrev-ref HEAD reports the bare repo's HEAD, not any worktree's, so scripts that need "the current branch" must run inside a worktree.

Merging back ​

When work in a worktree is complete and pushed:

  1. Create a pull request from the worktree's branch into the default branch, or merge directly if appropriate.
  2. Pull changes into main/ (if merged directly):
    sh
    cd main            # the main worktree, not the container root
    git pull origin main
  3. Clean up the worktree (see below).

Removing a worktree ​

Required information ​

  1. Branch name of the worktree to remove
  2. Delete branch decision (whether to delete the branch after removal)
  3. Current location (ensure you are not inside the worktree being removed)

Steps ​

Run from the container root:

  1. Verify the worktree exists: git worktree list.
  2. Check current directory: confirm you are not inside the worktree being removed (pwd). If you are, cd back to the container root first.
  3. Check for uncommitted work:
    • Uncommitted changes: git -C <name> status --porcelain
    • Unpushed commits: git log <name> --not --remotes --oneline
    • If either exists, warn the user before proceeding.
  4. Remove the worktree: git worktree remove <name>. Use --force only after user confirmation if there are uncommitted changes.
  5. Delete the branch (if requested):
    • Merged: git branch -d <name>
    • Unmerged (force): git branch -D <name>
    • Remote: git push origin --delete <name>
  6. Prune stale references: git worktree prune.
  7. Verify cleanup:
    • git worktree list to confirm removal
    • git branch -l <name> to confirm branch deletion

Never remove main/: every container keeps a checkout of its default branch.

Note: <name> may contain slashes (feature/auth), which is normal.

Common issues ​

  • Modified files error: commit/stash changes or use --force (loses changes).
  • "Branch not fully merged": use -D instead of -d after user confirms.
  • Remote deletion fails: may lack permissions or the branch is protected.
  • Parent directories left behind: after removing feature/auth/, the empty feature/ directory may remain. Harmless; remove manually if desired.

Cleanup rules ​

  • If a worktree has no changes or commits beyond the base, remove both the worktree and its branch automatically (or offer to).
  • If a worktree has changes or commits, prompt the user whether to keep or remove.

Important rules ​

  • STOP and ASK if:
    • Branch name not specified
    • User hasn't specified whether to delete the branch
    • There are unpushed commits or uncommitted changes
    • Currently inside the worktree being removed
  • Never remove without checking the current directory first.
  • Warn about potential data loss before proceeding.

Listing worktrees ​

Run git worktree list from anywhere in the repository:

/path/to/project/.bare          (bare)
/path/to/project/main           abc1234 [main]
/path/to/project/feature/auth   def5678 [feature/auth]
/path/to/project/quick-fix      ghi9012 [quick-fix]

The .bare entry has no branch and is expected; tooling that consumes this output should skip the bare entry (filter on the presence of a branch). To prune stale references, run git worktree prune; worktrees whose directories were deleted outside git worktree remove.

Trade-offs ​

ApproachProsCons
WorktreesFull isolation, no switchingDisk space, dependency copies
Branch switchSingle working directoryStash/commit needed to switch
StashingQuick context switchEasy to lose or forget stash

See also ​

  • Worktree Migration: convert a repository from the old root-checkout layout to the container layout, in place, preserving every worktree and all uncommitted work.