A minimal CLI for managing git worktrees using a bare-repo layout.
wt is tool-agnostic — it handles the git plumbing and delegates everything else (dependency installation, editor setup, environment config) to hooks and custom commands.
After running wt init or wt convert, you get this structure:
my-project/
├── .bare/ # bare git repo
├── .wt/ # hooks and custom commands
│ ├── commands/
│ └── hooks/
├── main/ # primary worktree (named after the default branch)
├── feature-login/ # worktrees live at the root
└── fix-auth/
The primary worktree directory is named after the remote's default branch — main, master, or whatever the remote uses. It is detected automatically.
curl -fsSL https://raw.githubusercontent.com/cescoallegrini/worktrees/main/install.sh | bashThis installs wt to ~/.wt/ and symlinks it to ~/.local/bin/wt. Running it again updates to the latest version — your config and custom commands are preserved. Ensure ~/.local/bin is on your PATH.
~/.wt/config is created on first install. Edit it to set global defaults:
# ~/.wt/config
DEFAULT_TARGET_DIR="$HOME/Projects"| Variable | Description |
|---|---|
DEFAULT_TARGET_DIR |
Default parent directory for wt init. Also used for interactive project selection when running commands outside a project (see Project resolution). |
All commands except init and convert need to know which project to operate on. wt resolves this in order:
-c/--currentflag — use the project detected from$PWDdirectly. Fails if not inside a project. Takes precedence over-p.-p/--projectflag — explicitly specify the project path or name.- Current directory + picker — if inside a project and
DEFAULT_TARGET_DIRis set, presents a picker with the current project first, followed by other projects. WithoutDEFAULT_TARGET_DIR, uses the current project directly. - Interactive picker — if not inside a project but
DEFAULT_TARGET_DIRis set, presents a project picker. - Error — with guidance to use
-por setDEFAULT_TARGET_DIR.
# Use current project directly
wt -c list
# Explicit project
wt -p ~/Projects/api list
# From inside a project with DEFAULT_TARGET_DIR — picker with current first
cd ~/Projects/api/main
wt list
# → Select project:
# 1) api (current)
# 2) frontend
# Choice [1-2]:
# From anywhere with DEFAULT_TARGET_DIR set — project picker
cd ~
wt list
# → Select project:
# 1) api
# 2) frontend
# Choice [1-2]:| Flag | Description |
|---|---|
-c, --current |
Use the current project directly. Must be inside a project. |
-p, --project <path> |
Operate on a specific project. |
Clone a repository as a bare repo and set up the worktree container.
# With DEFAULT_TARGET_DIR set to ~/Projects:
wt init git@github.com:org/api.git
# → clones into ~/Projects/api/
# Or specify the target explicitly:
wt init git@github.com:org/api.git ~/work/apiThe remote repository must have at least one commit — empty repositories (freshly created on GitHub with no commits) are not supported.
Creates the .wt/commands/ and .wt/hooks/ directories for you to add custom commands and hooks.
Convert an existing git repository into the wt bare-repo layout. Run from inside the repo.
cd ~/existing-project
wt convert
# Or move to a new location:
cd ~/existing-project
wt convert ~/Projects/my-projectRequires an origin remote to be configured. Preserves your working tree state — staged changes, unstaged modifications, and untracked files are kept intact in the resulting worktree.
If you're on a branch other than the default, both worktrees are created: the default branch gets a fresh checkout, and your current branch preserves your in-progress work. Other local branches are kept in the bare repo but don't get worktrees — use wt create to check them out.
Create a new worktree.
wt create fix-login
wt create feature/v2 --from developBranch names containing / are normalized to - for the directory name (e.g. feature/login becomes feature-login).
If the branch already exists on the remote or locally, it checks it out. Otherwise, it creates a new branch from the default branch (or the ref specified by --from).
The --from flag auto-resolves short names — --from develop will use origin/develop if develop doesn't exist locally.
Runs pre-create and post-create hooks.
Check out a pull request into a new worktree. Requires the GitHub CLI (gh).
wt pr 42Fetches the PR's branch name from GitHub, then creates a worktree for it — same as wt create but resolved from a PR number. Only supports PRs from the same repository (not forks).
Runs pre-create and post-create hooks.
Fetch from origin and fast-forward the primary worktree.
wt syncRuns pre-sync and post-sync hooks.
Remove a worktree. Without arguments, presents an interactive picker listing non-base worktrees. The local branch is kept by default — use -d to delete it.
wt remove fix-login # remove worktree, keep branch
wt remove -d fix-login # remove worktree and delete branch
wt remove # interactive pickerWarns if there are uncommitted changes and asks for confirmation. Runs pre-remove and post-remove hooks.
List all worktrees with their current commit.
$ wt list
my-project — ~/Projects/my-project
main 4e864a0 Fix TTY detection...
feature cb6f456 Convert wt from...
hotfix abff7a3 Add --project...
Hooks let you define custom logic that runs at specific points in the worktree lifecycle. They live at <root>/.wt/hooks/ and are created as empty directories by wt init.
| Hook | Trigger | Use case |
|---|---|---|
pre-create |
Before wt create |
Validate prerequisites, provision resources |
post-create |
After wt create |
Install dependencies, copy env files |
pre-sync |
Before wt sync |
Stash work, check for conflicts |
post-sync |
After wt sync |
Reinstall dependencies after update |
pre-remove |
Before wt remove |
Clean up external resources, close connections |
post-remove |
After wt remove |
Notify, clean caches |
pre-* hooks abort the command on non-zero exit. post-* hooks warn but continue.
Hooks are executable files — any language works.
All hooks and custom commands receive these environment variables:
| Variable | Description | Available in |
|---|---|---|
WT_BARE_PATH |
Path to the bare repo (.bare/) |
commands, hooks |
WT_BASE_BRANCH |
Remote's default branch name | commands, hooks |
WT_BRANCH |
Git branch name as-is (e.g. feature/login) |
hooks |
WT_DIR_NAME |
Normalized directory name (e.g. feature-login) |
hooks |
WT_HOOK |
Hook name | hooks |
WT_ROOT_PATH |
Worktree container root | commands, hooks |
WT_WORKTREE_PATH |
Full path to the target worktree | hooks |
Node.js monorepo — .wt/hooks/post-create:
#!/bin/sh
# Symlink node_modules from the primary worktree
ln -s "$WT_ROOT_PATH/$WT_BASE_BRANCH/node_modules" "$WT_WORKTREE_PATH/node_modules"
# Symlink nested workspace node_modules
for nm in "$WT_ROOT_PATH/$WT_BASE_BRANCH"/packages/*/node_modules; do
[ -d "$nm" ] || continue
relative="${nm#$WT_ROOT_PATH/$WT_BASE_BRANCH/}"
mkdir -p "$WT_WORKTREE_PATH/$(dirname "$relative")"
ln -s "$nm" "$WT_WORKTREE_PATH/$relative"
donePython project — .wt/hooks/post-create:
#!/bin/sh
cd "$WT_WORKTREE_PATH" && python -m venv .venv && .venv/bin/pip install -e ".[dev]"Rust project — .wt/hooks/post-create:
#!/bin/sh
cd "$WT_WORKTREE_PATH" && cargo fetchNo hooks needed — just leave the hooks directory empty. Everything works without them.
Any command that isn't built-in gets dispatched to a custom command script. These can be defined at two levels:
| Location | Priority | Scope |
|---|---|---|
<root>/.wt/commands/<name> |
First | Per-project |
~/.wt/commands/<name> |
Fallback | Global, all projects |
Project commands take priority over global commands with the same name.
Custom commands receive WT_BARE_PATH, WT_BASE_BRANCH, and WT_ROOT_PATH as environment variables, plus any arguments passed after the command name.
Custom commands can source shared utility functions:
. "$HOME/.wt/lib/core/utils.sh"This auto-loads all functions from utils/. Available utilities:
| Function | File | Description |
|---|---|---|
wt_branch_path <branch> |
utils/branch-path.sh |
Resolve a branch name to its worktree filesystem path. |
wt_pick <prompt> |
utils/pick.sh |
Interactive picker — reads items from stdin, prints selection to stdout. Uses fzf if available, numbered menu fallback. Auto-selects when only one item. |
wt_select_branch |
utils/select-branch.sh |
Collect all worktree branches and present an interactive picker. Returns the selected branch name. |
A global command at ~/.wt/commands/open that opens a worktree in your editor:
#!/bin/sh
# ~/.wt/commands/open
. "$HOME/.wt/lib/core/utils.sh"
branch="${1:-$(printf '%s\n' "$WT_BASE_BRANCH" | wt_pick "Select branch")}"
wt_path="$(wt_branch_path "$branch")"
code "$wt_path"wt open fix-login # direct
wt open # interactive pickerA deploy command at <root>/.wt/commands/deploy:
#!/bin/sh
echo "Deploying from $WT_ROOT_PATH..."
# project-specific deploy logicwt deploy staginginstall.sh # curl | bash installer
wt.sh # Entry point — standalone bash script
config.default # Template copied to ~/.wt/config on first install
commands/
├── convert.sh # wt convert
├── create.sh # wt create
├── init.sh # wt init
├── list.sh # wt list
├── remove.sh # wt remove
└── sync.sh # wt sync
core/
├── commands.sh # Custom command dispatcher
├── config.sh # Loads ~/.wt/config
├── hooks.sh # Hook runner
├── root.sh # Project resolution and branch helpers
└── utils.sh # Auto-loads all utils/*.sh
utils/ # Shared utility functions (wt_pick, wt_branch_path, ...)
MIT