A timesaver: work on several branches side by side, each in its own folder, with a few short commands instead of git worktree add and cd, with every command run for real.
▶Prefer to watch? This short video explains what worktrunk is for, then creates, switches, merges and removes Git worktrees on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.
worktrunk (the command is wt): Git worktrees addressed by branch name.
wt switch --create feature new branch, new worktree folder, and you are in it
wt switch feature move to that worktree (wt switch - is the previous one)
wt list every worktree, with changes, commits ahead and age
wt merge main merge the branch into main, then remove its worktree
wt remove remove this worktree (and its branch, if merged)
A Git worktree is a second (third, fourth) checkout of the same repository in its own folder, each on its own branch, so you can work on several tasks side by side without stashing. Plain git can do it, but you type the branch name three times and cd by hand: git worktree add -b feat ../repo.feat, then cd ../repo.feat. worktrunk makes the common steps one command, works out the folder names, and lists what is going on. Its README says it is designed for running AI coding agents in parallel, one worktree each, and it is just as useful for your own tasks.
This page is the written version of the Timesavers video. Every command below was run for real, in an Ubuntu 24.04 container with Git 2.43.0 and worktrunk 0.81.0, on a small sample repository (a three-commit Python shop) with made-up people. Nothing was run with an AI agent. The section "What was not run" lists what was not tried.
What worktrunk is for
Use worktrunk when you want more than one branch checked out at once:
a second task while the first is half done a new folder, no stash, no half-committed work
reviewing a branch check it out beside your own work
running agents or builds side by side one worktree for each, with no overlap
seeing what is in flight wt list shows all of them at once
Plain git has the same feature (git worktree), so worktrunk adds convenience, not a new ability: branch-name addressing, a folder naming rule, the shell moving you into the folder, a status list, and a merge that cleans up after itself. The README also describes hooks, an interactive picker, CI status in the list, pull request checkout, commit messages written by an AI model and build-cache sharing. Those were not run here.
Install
The project's README gives Homebrew, cargo, winget, Arch Linux (pacman) and conda or pixi routes, and each release on GitHub has a shell installer (worktrunk-installer.sh) and binary archives with a .sha256 file beside each. The lab downloaded the Linux (x86_64, musl) archive, checked the checksum by hand, and copied the two programs in it into ~/.local/bin:
$ B=https://github.com/max-sixty/worktrunk/releases/download/v0.81.0/worktrunk-x86_64-unknown-linux-musl.tar.xz
$ curl -fsSLO $B && curl -fsSLO $B.sha256
$ sha256sum -c worktrunk-x86_64-unknown-linux-musl.tar.xz.sha256
worktrunk-x86_64-unknown-linux-musl.tar.xz: OK
$ tar xJf worktrunk-x86_64-unknown-linux-musl.tar.xz
$ ls worktrunk-x86_64-unknown-linux-musl/
CHANGELOG.md
LICENSE
README.md
git-wt
wt
$ cp worktrunk-x86_64-unknown-linux-musl/{wt,git-wt} ~/.local/bin/
$ wt --version
wt v0.81.0
The archive holds wt and git-wt (the same program under a name that avoids a clash with Windows Terminal's wt on Windows). The tarball needs xz-utils to unpack. The installer script was not run.
Shell integration: so wt can change folder
A program cannot change the folder of the shell that started it. worktrunk solves that with a small shell function, so wt switch can move you into the worktree. Install it once:
$ wt config shell install
❯ Install shell integration? [y/N/?]
(-y answers yes.) For bash it adds one line to ~/.bashrc, and then you start a new shell:
$ tail -2 ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init bash)"; fi
Without the integration, wt still creates the worktree but warns that it cannot change directory. In the lab that happened on the first try. The line in ~/.bashrc only does anything if wt is already on the PATH at that moment, and as far as I could tell a login shell in the lab container read ~/.bashrc before ~/.profile added ~/.local/bin to the PATH. The message was "Cannot change directory — shell integration installed but not active". The fix was one line before it in ~/.bashrc:
export PATH="$HOME/.local/bin:$PATH"
In a desktop terminal your session usually has ~/.local/bin on the PATH already, so you may never see this. wt config show says whether the integration is active.
The sample repository
A tiny repository called shop, with three commits on main by one made-up person. At this point it has one worktree, the main folder:
$ git log --oneline
$ git worktree list
ubuntu@timesaver:~/shop$ git log --oneline
0e9e0c0 (HEAD -> main) Document how to run it
3b63afa Add count
4becf79 Start the shop
ubuntu@timesaver:~/shop$ git worktree list
/home/ubuntu/shop 0e9e0c0 [main]
ubuntu@timesaver:~/shop$
Create a worktree: wt switch --create
One command makes the branch, makes a worktree folder next to the repository (the name is the repository folder, a dot and the branch), and moves you into it:
$ wt switch --create discount
$ pwd
$ ls ..
ubuntu@timesaver:~/shop$ wt switch --create discount
âś“ Created branch discount from main and worktree @ ~/shop.discount
↳ To customize worktree locations, run wt config create
ubuntu@timesaver:~/shop.discount$ pwd
/home/ubuntu/shop.discount
ubuntu@timesaver:~/shop.discount$ ls ..
shop shop.discount
ubuntu@timesaver:~/shop.discount$
The new folder is ~/shop.discount, a sibling of ~/shop. The place and the name come from a template that can be changed (the output says wt config create makes a config file; in the lab the user config already existed). -c is the short form of --create. The branch starts from main by default, and -b chooses another base. The same two things in plain git are git worktree add -b discount ../shop.discount and cd ../shop.discount.
Work in it as usual
A worktree is a normal checkout. Edit, add and commit in it with plain git, and the main folder is not touched:
$ printf "DISCOUNT = 0.1\n" >> cart.py
$ git add cart.py && git commit -qm "Add a discount rate"
ubuntu@timesaver:~/shop.discount$ printf "DISCOUNT = 0.1\n" >> cart.py
ubuntu@timesaver:~/shop.discount$ git add cart.py && git commit -qm "Add a discount rate"
ubuntu@timesaver:~/shop.discount$
The commit is on the branch discount. main still has three commits.
A second task, and wt list
While discount is unfinished, start a second task from main. The new worktree comes from main, not from discount, because main is the default base. A small unstaged change in it, and then wt list shows everything:
$ wt switch --create wording
$ echo "Thanks!" >> README.md
$ wt list
âś“ Created branch wording from main and worktree @ ~/shop.wording
ubuntu@timesaver:~/shop.wording$ echo "Thanks!" >> README.md
ubuntu@timesaver:~/shop.wording$ wt list
Branch Status HEAD± main↕ main…± Path Commit Age Message
@ wording ! – +1 ../shop.wording 0e9e0c0 5d Document how to r…
^ main ^ . 0e9e0c0 5d Document how to r…
+ discount ↑ ↑1 +1 ../shop.discount 8fd55c0 now Add a discount ra…
○ Showing 3 worktrees, 1 with changes, 1 ahead, hidden: Remote⇅
ubuntu@timesaver:~/shop.wording$
One row for each worktree. The @ marks the one you are in, ^ is the main branch, and + is another worktree. In the lab: the wording row has a ! in Status and +1 under HEAD±, which is one changed file not yet committed. The discount row has ↑ and under main↕ the symbol ↑1 (one commit ahead of main), and under main…± the lines it changes (+1). Path and age follow, then the last commit message. The footer says what is hidden: the Remote⇅ column is not shown here, since the repository has no remote. wt list --full adds a CI column that needs a remote to be of use, and was run as well:
$ wt list --full
Branch Status HEAD± main↕ main…± Remote⇅ CI Path Commit Age Message
@ main ^ . 0e9e0c0 5d Document h…
+ a _ ../shop.a 0e9e0c0 5d Document h…
+ b _ ../shop.b 0e9e0c0 5d Document h…
â—‹ Showing 3 worktrees
ubuntu@timesaver:~/shop$
The CI column is empty here, as expected with no remote.
Move between worktrees
wt switch with a branch name moves you there. It needs no path:
$ wt switch discount
$ pwd
ubuntu@timesaver:~/shop.wording$ wt switch discount
â—‹ Switched to worktree for discount @ ~/shop.discount
ubuntu@timesaver:~/shop.discount$ pwd
/home/ubuntu/shop.discount
ubuntu@timesaver:~/shop.discount$
Shortcuts, from wt switch --help, also work in place of a branch name: ^ for the default branch, - for the previous worktree, @ for the current one, and pr:N or mr:N for a GitHub pull request or GitLab merge request (not run). The lab tried two:
$ wt switch -
â—‹ Switched to worktree for a @ ~/shop.a
ubuntu@timesaver:~/shop.a$
$ wt switch ^
â—‹ Switched to worktree for main @ ~/shop
ubuntu@timesaver:~/shop$
Without a branch name, wt switch opens an interactive picker (not run).
Merge and clean up: wt merge
When the work is done, wt merge main merges the current branch into main and then removes the worktree and the branch, and you land in the main folder:
$ wt merge main
$ git log --oneline | head -4
ubuntu@timesaver:~/shop.discount$ wt merge main
â—Ž Merging 1 commit to main @ 8fd55c0 (no commit/squash/rebase needed)
* 8fd55c0 Add a discount rate
cart.py | 1 +
1 file changed, 1 insertion(+)
âś“ Merged to main (1 commit, 1 file, +1)
â—Ž Removing discount worktree & branch in background (same commit as main, _)
â—‹ Switched to worktree for main @ ~/shop
ubuntu@timesaver:~/shop$ git log --oneline | head -4
8fd55c0 Add a discount rate
0e9e0c0 Document how to run it
3b63afa Add count
4becf79 Start the shop
ubuntu@timesaver:~/shop$
The branch had one commit, already made, so there was nothing to squash or rebase, and the merge was a fast-forward ("no commit/squash/rebase needed"). With more commits or with uncommitted changes, wt merge squashes, rebases and commits first, and for the commit message it can use an AI model if you set one up (the README describes it, and it was not tried). The options, from wt merge --help in the lab:
Usage: wt merge [OPTIONS] [TARGET]
Arguments:
[TARGET]
Target branch
Defaults to default branch.
Options:
-b, --branch <BRANCH>
Branch to merge (defaults to the current branch)
--no-squash
Skip commit squashing
--no-commit
Skip commit and squash
--no-rebase
Skip rebase; require the target to fast-forward to the resulting tip
--no-remove
Keep worktree after merge
--no-ff
Create a merge commit (no fast-forward)
--stage <STAGE>
What to stage before committing [default: all]
Possible values:
- all: Stage everything: untracked files + unstaged tracked changes
- tracked: Stage tracked changes only (like git add -u)
- none: Stage nothing, commit only what's already in the index
-h, --help
Print help (see a summary with '-h')
--no-remove keeps the worktree after the merge, --no-squash and --no-rebase skip those steps, and --no-ff makes a merge commit.
Remove a worktree: wt remove
wt remove removes the current worktree (or the ones you name) and the branch, if it is merged. It will not throw away uncommitted work:
$ wt switch wording
$ wt remove
$ git add -A && git commit -qm "Say thanks" && wt remove
$ wt list
ubuntu@timesaver:~/shop$ wt switch wording
â—‹ Switched to worktree for wording @ ~/shop.wording
ubuntu@timesaver:~/shop.wording$ wt remove
âś— Cannot remove worktree: wording has uncommitted changes
M README.md
↳ Commit or stash changes first, or to lose uncommitted changes, run wt remove --force wording
ubuntu@timesaver:~/shop.wording$ git add -A && git commit -qm "Say thanks" && wt remove
â—Ž Removing wording worktree in background
↳ Branch unmerged; to delete, run wt remove -D wording
â—‹ Switched to worktree for main @ ~/shop
ubuntu@timesaver:~/shop$ wt list
Branch Status HEAD± main↕ main…± Remote⇅ Path Commit Age Message
@ main ^ . 8fd55c0 now Add a discount rate
â—‹ Showing 1 worktree
ubuntu@timesaver:~/shop$
The first wt remove stopped: "Cannot remove worktree: wording has uncommitted changes", with the file and the hint wt remove --force. After the commit it removed the worktree but kept the branch, because the branch is not merged into main ("Branch unmerged; to delete, run wt remove -D wording"). Removing a merged branch takes the branch too, and -D forces the branch delete (both run in the lab):
$ wt remove a
â—Ž Removing a worktree & branch in background (same commit as main, _)
$ wt remove -D b
â—Ž Removing b worktree & branch in background (--force-delete)
The removal runs in the background, so the prompt comes back before the folder is gone (--foreground waits). The options of wt remove, from its help:
Usage: wt remove [OPTIONS] [BRANCHES]...
Arguments:
[BRANCHES]...
Branch name or worktree path [default: current]
Options:
--no-delete-branch
Keep branch after removal
-D, --force-delete
Delete unmerged branches
--foreground
Run removal in foreground (block until complete)
--reap
Kill processes started in the worktree [experimental]
Before removal, terminate processes whose working directory is under the worktree — dev servers, watchers, language servers. Processes holding a controlling terminal (interactive shells, terminal editors) are left alone. Unix only.
-f, --force
Force worktree removal
Remove a dirty worktree, including staged, modified, and untracked files. Without this flag, removal fails if the worktree has any uncommitted changes.
-h, --help
Print help (see a summary with '-h')
Other commands
wt --help lists the commands of this version:
Commands:
switch Switch to a worktree; create if needed
list List worktrees and their status
remove Remove worktree; delete branch if merged
merge Merge a branch into the target branch
step Run individual operations
hook Run configured hooks
config Manage user & project configs
wt step runs single pieces of the merge workflow, for when you want them one by one:
Commands:
commit Stage and commit with LLM-generated message
squash Squash commits since branching
rebase Rebase onto target
push Fast-forward target to current branch
diff Show all changes since branching
copy-ignored Copy gitignored files to another worktree
eval Evaluate a template expression
for-each Run command in each worktree
promote [experimental] Swap a branch into the main worktree
prune Remove worktrees and branches merged into the default branch
relocate [experimental] Move worktrees to expected paths
tether [experimental] Run a command; kill its whole process tree when its worktree is removed
wt switch has more than this page used. The key ones from its help: -x runs a program after the switch (the README uses it to start an AI agent in the new worktree, as in wt switch -c -x claude feature), -b chooses the base branch, and --path puts the folder somewhere else. wt hook runs hooks, and wt config manages the user and project configuration:
$ wt config show
USER CONFIG @ ~/.config/worktrunk/config.toml
skip-commit-generation-prompt = true
â—‹ Optional system config not found @ /etc/xdg/worktrunk/config.toml
PROJECT CONFIG @ ~/shop/.config/wt.toml
â—‹ Identifier: /home/ubuntu/shop
â—‹ Not found
SHELL INTEGRATION
â—‹ Shell integration active
â—‹ bash: Already configured shell extension & completions @ ~/.bashrc:121
if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init bash)"; fi
OTHER
â—‹ wt: v0.81.0
â—‹ git: 2.43.0
â—‹ Hyperlinks: inactive
What was not run in the lab
Only the commands on this page were run, on a sample repository with no remote. These were not: hooks (wt hook, and the hook settings in the project config), the interactive picker, -x with a program or an AI agent, AI-written commit messages and summaries, the CI status in wt list --full against a real remote, pull and merge request checkout (pr:N, mr:N), wt step and its subcommands (commit, squash, rebase, push, copy-ignored, prune and the rest), --reap, merging several commits with a squash and a rebase, conflicts, the worktree path template and its settings, the other shells (zsh, fish, nushell, PowerShell), Windows and macOS, and the installer script. The checksum was checked by hand for the release archive. The licence line in the repository's README is MIT or Apache-2.0, while GitHub's own licence field showed no answer, so read the files in the repository if the licence matters to you. Everything ran on Ubuntu 24.04 with worktrunk 0.81.0 and Git 2.43.0.
Common problems and tips
wt switch says it cannot change directory. The shell integration is installed but not active. Open a new shell, and make sure wt is on the PATH before the line in ~/.bashrc runs (see the section above). wt config show tells you the state.
wt remove refuses to remove the worktree. It has uncommitted changes. Commit or stash them, or use wt remove --force if you really want to lose them.
The branch is still there after wt remove. It is not merged into main. wt remove -D deletes it anyway.
Where did the folder go? It is next to the repository, named repo.branch. wt list shows the path of every worktree, and wt switch needs only the branch name.
A branch name with a slash. Not run in the lab, so check the folder name wt list shows before relying on it.
Tips. Keep one worktree per task, and keep main clean so that new worktrees start from it. Use wt list before you leave for the day, to see what is unfinished. Use wt merge main for work that stays local, and your usual pull request flow when you work with others (the README describes wt step commit, then gh pr create, then wt remove). And remember that every worktree is a full checkout, so a project with a big node_modules or target folder needs it once for each.