🌳 worktrunk

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.

Video: worktrunk (Timesavers video)
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.