Appearance
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 branchThe 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:
| Layout | Shape |
|---|---|
| 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 topologyA 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 statusandgit rev-parse --show-toplevelboth returnfatal: 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 fetchandgit branchstill 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): thenameform 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 bypath:EnterWorktreeaccepts apathto an existing worktree as long as it appears ingit 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>/).
| Action | Command |
|---|---|
| Create | git worktree add <branch> <branch> |
| New branch | git worktree add <branch> -b <branch> origin/main |
| List | git worktree list |
| Remove | git worktree remove <branch> |
| Prune | git 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
- The container holds no working files. No
package.json, nonode_modules, no source at the container root. Its only contents are the git directory (.bare), the.gitpointer file, and worktree directories. This is the whole point: nothing between a worktree and the filesystem root is a checkout of the project. - Every worktree is a child directory,
mainincluded. 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. - Path equals branch, one to one. The worktree directory path, the worktree name, and the branch name are the same string. Branch
feature/authmeans directoryfeature/auth/. No prefixes, no slug transforms, no.worktrees/level. - Complete isolation. Each worktree has its own working files and its own dependency install (
npm install,pip install, etc.), sincenode_modules,venv, and similar are per-worktree. - 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/mainThe 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
- Branch name (the branch and the worktree directory, e.g.
feature/user-auth,bugfix/login-issue,quick-fix) - Base branch to create from for a new branch (typically
origin/main) - Branch existence at origin or needs creating
Steps
Run from the container root:
Determine branch status:
- Locally:
git branch -l <name> - At origin:
git ls-remote --heads origin <name>
- Locally:
Create the worktree:
- New branch:
git worktree add <name> -b <name> origin/main - Existing branch:
git worktree add <name> <name>
git worktree addcreates intermediate directories automatically, so branch names with slashes (feature/auth) work without extra steps.- New branch:
Install dependencies inside the new worktree:
cd <name>thennpm 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 setsNODE_ENV=production,npm installsilently skipsdevDependencies; 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:
| Branch | Worktree Path |
|---|---|
main | main/ |
feature/auth | feature/auth/ |
bugfix/login-null | bugfix/login-null/ |
experiment/new-router | experiment/new-router/ |
quick-fix | quick-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>| Issue | Branch | Worktree Path |
|---|---|---|
| #9 | issue-009-integrate | issue-009-integrate/ |
| #42 | issue-042-cache-headers | issue-042-cache-headers/ |
| #286 | issue-286-nested-fences | issue-286-nested-fences/ |
Rules:
- Zero-pad issue numbers to at least 3 digits so branches sort stably in alphabetical listings (
issue-009sorts beforeissue-042). - The slug describes the work (a verb or short phrase), not the artifact. Prefer
integrate,refactor,fix,auditover release names or version numbers. - Path/branch identity still holds: the full string
issue-009-integrateis 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/authOther 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:
- Create a pull request from the worktree's branch into the default branch, or merge directly if appropriate.
- Pull changes into
main/(if merged directly):shcd main # the main worktree, not the container root git pull origin main - Clean up the worktree (see below).
Removing a worktree
Required information
- Branch name of the worktree to remove
- Delete branch decision (whether to delete the branch after removal)
- Current location (ensure you are not inside the worktree being removed)
Steps
Run from the container root:
- Verify the worktree exists:
git worktree list. - Check current directory: confirm you are not inside the worktree being removed (
pwd). If you are,cdback to the container root first. - 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.
- Uncommitted changes:
- Remove the worktree:
git worktree remove <name>. Use--forceonly after user confirmation if there are uncommitted changes. - Delete the branch (if requested):
- Merged:
git branch -d <name> - Unmerged (force):
git branch -D <name> - Remote:
git push origin --delete <name>
- Merged:
- Prune stale references:
git worktree prune. - Verify cleanup:
git worktree listto confirm removalgit 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
-Dinstead of-dafter user confirms. - Remote deletion fails: may lack permissions or the branch is protected.
- Parent directories left behind: after removing
feature/auth/, the emptyfeature/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
| Approach | Pros | Cons |
|---|---|---|
| Worktrees | Full isolation, no switching | Disk space, dependency copies |
| Branch switch | Single working directory | Stash/commit needed to switch |
| Stashing | Quick context switch | Easy 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.