git worktree has eight subcommands: add, list, lock, move, prune, remove, repair and unlock. Three of them are daily work. The other five exist to repair situations you will eventually create by moving or deleting directories by hand, or to protect a worktree that lives on storage which comes and goes.
- Eight subcommands. Three are daily, five are repairs and safeguards.
list --porcelainis the machine-readable form. Never parse the human one.prunefixes what deleting a directory by hand breaks.repairfixes what moving a directory by hand breaks. They are not interchangeable.lockstopsprunetouching a worktree on removable or network storage.-vand--porcelaincannot be combined, and-zrequires--porcelain.
Every command
The complete subcommand surface, git 2.54, August 2026.
| Command | Does | How often |
|---|---|---|
git worktree add | Create a worktree | Daily |
git worktree list | Show all of them, with state | Daily |
git worktree remove | Delete one properly | Daily |
git worktree prune | Clear records for directories that are gone | Occasionally |
git worktree repair | Fix pointers after something moved | After a mistake |
git worktree move | Relocate one properly | Rarely |
git worktree lock | Protect one from pruning | Rarely |
git worktree unlock | Undo the lock | Rarely |
# add ────────────────────────────────────────────────────
git worktree add -b feature/x ../app-x # new branch (the usual)
git worktree add ../app-hot hotfix/login # existing branch
git worktree add ../app-spike # branch named after the dir
git worktree add -b review ../app-r origin/pr-42
git worktree add --detach ../app-v1 v1.0.0 # read-only look
git worktree add -B feature/x ../app-x main # reset the branch first
git worktree add --orphan -b docs ../app-docs # empty, unborn branch
git worktree add --no-checkout ../app-empty # create, populate later
git worktree add --lock --reason "usb" ../app-usb
# list ───────────────────────────────────────────────────
git worktree list # human
git worktree list -v # + lock and prune reasons
git worktree list --porcelain # for scripts
git worktree list --porcelain -z # NUL-terminated
git worktree list --expire 2.weeks.ago # annotate old records
# remove ─────────────────────────────────────────────────
git worktree remove ../app-x
git worktree remove --force ../app-x # even if dirty
git worktree remove -f -f ../app-usb # even if locked
# prune ──────────────────────────────────────────────────
git worktree prune -n -v # see first, always
git worktree prune
git worktree prune --expire 2.weeks.ago
# move / lock / repair ───────────────────────────────────
git worktree move ../app-x ../renamed
git worktree lock ../app-usb --reason "on an external disk"
git worktree unlock ../app-usb
git worktree repair # from the main worktree
git worktree repair ../moved-worktree # after moving one by hand
Five minutes, start to finish
If you have never used the command, this is the whole loop. Every line below was run against git 2.54.
Create a worktree on a new branch
$ cd ~/code/app
$ git worktree add -b feature/auth ../app-auth
Preparing worktree (new branch 'feature/auth')
HEAD is now at ad7da80 init
The directory is a sibling of the repository, which is the convention. A worktree inside the repository shows up as an untracked directory in git status and gets indexed twice by your editor.
Make it runnable
A worktree carries tracked files only, so gitignored things are absent. This is the step people forget, and the resulting failure looks like a broken worktree.
$ cd ../app-auth
$ ls node_modules
ls: node_modules: No such file or directory
$ cp ../app/.env .
$ pnpm install
Work, commit, push, open a pull request
Nothing here is worktree-specific. That is the point: the result rejoins your normal process instead of needing a special one.
$ git add -A && git commit -m "add JWT middleware"
$ git push -u origin feature/auth
$ gh pr create --fill
Check what you have open
$ git worktree list
/Users/you/code/app ad7da80 [main]
/Users/you/code/app-auth b91c4f2 [feature/auth]
Clean up
Removing the worktree does not remove the branch, so delete that separately once it is merged.
$ cd ../app
$ git worktree remove ../app-auth
$ git branch -d feature/auth
git worktree list, in detail
The command you will run most often after add, and the one with the most output modes. Plain list gives you path, commit and branch, with any interesting state as a trailing word.
$ git worktree list
/Users/you/code/app ad7da80 [main]
/Users/you/code/app-det ad7da80 (detached HEAD)
/Users/you/code/app-usb ad7da80 [usb] locked
/Users/you/code/app-gone ad7da80 [gone] prunable
/Users/you/code/app-auth b91c4f2 [feature/auth]
What the annotations mean.
| Annotation | Means | Next step |
|---|---|---|
[branch] | Normal, on a branch | Nothing |
(detached HEAD) | Created with --detach | Commit here and the commits are unreachable by name |
(bare) | The bare repository itself | It has no working files |
locked | Protected from prune | unlock before removing or moving |
prunable | The directory is missing | prune, or repair if you moved it |
-v adds the reason for each lock or prunable state on an indented line. It is the fastest way to answer "why will git not remove this".
$ git worktree list -v
/Users/you/code/app ad7da80 [main]
/Users/you/code/app-usb ad7da80 [usb]
locked: on an external disk
/Users/you/code/app-gone ad7da80 [gone]
prunable: gitdir file points to non-existent location
The machine-readable form
$ git worktree list --porcelain
worktree /Users/you/code/app
HEAD ad7da806c132de9e6d724d0c6893bddb0bb4d20c
branch refs/heads/main
worktree /Users/you/code/app-det
HEAD ad7da806c132de9e6d724d0c6893bddb0bb4d20c
detached
worktree /Users/you/code/app-gone
HEAD ad7da806c132de9e6d724d0c6893bddb0bb4d20c
branch refs/heads/gone
prunable gitdir file points to non-existent location
Every key the porcelain form can emit.
| Key | Value | Present when |
|---|---|---|
worktree | Absolute path | Always, and always first in a record |
HEAD | Full 40-character object id | Not bare |
branch | refs/heads/<name> | On a branch |
bare | No value | The bare repository |
detached | No value | Detached HEAD |
locked | Reason, or no value | Locked |
prunable | Reason | The directory is missing |
Recipes
Create one that actually runs
#!/usr/bin/env bash
# wt <branch> - a worktree you can immediately work in
set -euo pipefail
name="$1"
dir="../$(basename "$PWD")-${name//\//-}"
git worktree add -b "$name" "$dir"
for f in .env .env.local; do
if [ -f "$f" ]; then cp "$f" "$dir/"; fi
done
cd "$dir"
if [ -f pnpm-lock.yaml ]; then pnpm install
elif [ -f package-lock.json ]; then npm ci
elif [ -f bun.lockb ]; then bun install
fi
echo "$dir"
Jump between them
wtj() {
local dir
dir=$(git worktree list | fzf --height 40% | awk '{print $1}') || return
cd "$dir" || return
}
Remove every merged worktree
git worktree list --porcelain \
| awk '/^worktree /{p=$2} /^branch /{print p, $2}' \
| while read -r path ref; do
b="${ref#refs/heads/}"
[ "$b" = "main" ] && continue
git merge-base --is-ancestor "$b" main 2>/dev/null \
&& git worktree remove "$path" && git branch -d "$b"
done
Find the worktree that is not clean
git worktree list --porcelain \
| awk '/^worktree /{print $2}' \
| while read -r p; do
n=$(git -C "$p" status --porcelain | wc -l | tr -d " ")
[ "$n" -gt 0 ] && printf "%-40s %s changed\n" "$p" "$n"
done
A bare repository with only worktrees
git clone --bare git@github.com:you/app.git app/.bare
cd app
echo "gitdir: ./.bare" > .git
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch origin
git worktree add main
git worktree add -b feature/x feature-x
# git worktree list
# /Users/you/code/app/.bare (bare)
# /Users/you/code/app/feature-x ad7da80 [feature/x]
# /Users/you/code/app/main ad7da80 [main]
Scripting against it
The default output is aligned for humans and will break your parser the first time a path is unusually long or contains a space. The porcelain form will not.
Flags that matter when a program is reading the output.
| Flag | Why |
|---|---|
--porcelain | Stable key-value stanzas rather than aligned columns |
-z | NUL terminators, so newlines in paths are safe |
-n, --dry-run | On prune: report without changing anything |
-v, --verbose | On prune: say why each record is going |
--expire <time> | Only touch records older than that |
-q, --quiet | On add: drop the progress lines |
Questions people ask
Eight: add, list, lock, move, prune, remove, repair and unlock. Add, list and remove are the daily ones. The rest exist to repair situations created by moving or deleting directories by hand, or to protect a worktree on storage that is not always mounted.
git worktree list. Add -v to see the reason behind any locked or prunable annotation, or --porcelain for a stable machine-readable form when scripting. -v and --porcelain cannot be combined.
The registered directory is missing from disk. Usually you deleted it with rm -rf, in which case prune or remove clears the record. If you moved it with mv, the record is wrong rather than stale and git worktree repair is the right command.
remove deletes a worktree properly, directory and record together, and refuses if the tree is dirty. prune only clears records pointing at directories that no longer exist, so it can never lose work.
git worktree move <from> <to>. Using mv leaves the gitdir pointer naming the old path, git starts listing the worktree as prunable, and you need git worktree repair <new-path> to fix it.
It marks a worktree so prune leaves it alone, and blocks move and remove until you unlock it. It exists for worktrees on removable or network storage, which look exactly like deleted directories while unmounted. Always pass --reason.
Yes. Clone bare into app/.bare, write a .git file containing "gitdir: ./.bare", set the origin fetch refspec, and add a worktree per branch. It is a tidy arrangement when you routinely keep several checkouts and none of them is privileged.
Use git worktree list --porcelain, which prints blank-line-separated stanzas of key and value. Add -z for NUL terminators if a path could contain a newline. Never parse the default aligned output.
Sources
Every figure above was read from these pages on August 2026. Vendors reprice without notice; if you find a stale number, tell us.