Pick and switch git worktrees from the shell. wt switch <branch> opens the worktree in a new Ghostty tab running your $EDITOR — fresh cwd, fresh LSP, nothing leaking between branches.
It drives Ghostty through its AppleScript dictionary, so wt can talk to a running Ghostty from anywhere — even when launched outside any terminal (Claude Code's Bash tool, a launchd job, a script). No session juggling: wt switch just opens a tab and Ghostty pops to the front.
Designed for workflows that create worktrees elsewhere (Claude Code, scripts, another terminal) and just want a fast way to jump into them.
- macOS (Ghostty's AppleScript support is macOS-only)
git- Ghostty 1.3 or newer (AppleScript support landed in 1.3)
macOS (Apple Silicon):
curl -fsSL https://raw.githubusercontent.com/johnpangalos/wt/main/install.sh | shInstalls to ~/.local/bin/wt. Override with PREFIX=/usr/local.
Resolving the latest release uses the GitHub CLI, so gh must be installed and authenticated (gh auth login) — this avoids the unauthenticated API rate limit that otherwise surfaces as a 403. To install without gh, pin a version with WT_VERSION=v0.1.0, which skips the release lookup entirely.
On first run, macOS may quarantine the unsigned binary. Clear it with:
xattr -d com.apple.quarantine ~/.local/bin/wtRequires Bun 1.2+ (only to build; the compiled binary has no runtime dep).
git clone https://github.com/johnpangalos/wt.git
cd wt
bun install
bun run build # produces ./bin/wt
ln -s "$PWD/bin/wt" "$HOME/.local/bin/wt" # put it on $PATHbin/wt is a self-contained native binary — Bun is not needed at runtime.
wt list # list worktrees (TSV)
wt list --json # list worktrees (JSON)
wt switch feature-x # open the feature-x worktree in a new Ghostty tab
wt switch /path/to/wt # same, by path
wt switch # re-open the worktree containing $PWD (= wt switch $(wt current))
wt switch feat --window # open in a new window instead of a tab
wt switch --split-right # split the front window with the current worktree
wt root # open the main (root) worktree in a new Ghostty tab
wt current # print the worktree containing $PWD
wt update # check GitHub for a new release and install it
wt --version # print the installed version
wt --help # usageIf Claude Code is installed, wt list joins git worktree list with claude agents --json on path == cwd and
annotates worktrees that belong to a background agent session. Agent rows gain an
agent flag plus two trailing columns — the session name and status
(with waitingFor folded in, e.g. waiting (permission prompt)):
/repo/.claude/worktrees/abc feat-x agent brave-otter waiting (permission prompt)
wt list --json adds sessionId, name, status, and waitingFor to those
rows when present. This is best-effort: if Claude Code isn't installed or no
agents are running, output is unchanged.
wt update uses the GitHub CLI (gh api) to check for a newer release, so it relies on your existing gh authentication and isn't subject to the unauthenticated API rate limit (which surfaces as a 403). gh must be installed and authenticated (gh auth login). If a newer version exists, it prints wt vCURRENT → vLATEST and prompts for confirmation before re-running install.sh with the same PREFIX the current binary was installed under.
To reduce friction, wt also runs a throttled background check (once per day) on every invocation and prints a single-line hint to stderr when a newer release is available:
wt: update available (0.1.0 → 0.2.0) — run: wt update
The cache lives at $XDG_STATE_HOME/wt/update-check (default ~/.local/state/wt/update-check). Set WT_NO_UPDATE_CHECK=1 to disable the background check entirely.
| Variable | Default | Purpose |
|---|---|---|
WT_CMD |
$EDITOR or vi |
Command to run in the new surface. Its executable is resolved to an absolute path before being handed to Ghostty (see below). |
WT_GHOSTTY_PLACEMENT |
new-tab |
new-tab | new-window | split-right | split-left | split-down | split-up |
WT_NO_UPDATE_CHECK |
— | set to any value to disable the daily background update check. |
wt switch and wt root also take a placement flag that overrides
WT_GHOSTTY_PLACEMENT for a single invocation: --tab, --window,
--split-right, --split-left, --split-down, --split-up, --split (alias
for --split-right), or --placement <name> / -p <name> for any of those
names.
exec nvim: not found? Ghostty runs the surface command through a non-login shell, and because it's launched via AppleScriptactivateit inherits the macOS GUI launchPATH(/usr/bin:/bin:…), not your interactive shellPATH. A barenviminstalled under/opt/homebrew/binwould then fail to launch.wtruns from your shell with the fullPATH, so it resolves theWT_CMD/$EDITORexecutable to an absolute path before handing it to Ghostty. A command that already contains a/, or whose executable isn't on yourPATH, is passed through unchanged.
The split-* placements split the focused terminal of Ghostty's front window in
that direction, so they only do something useful when a Ghostty window already
exists. new-tab (the default) and new-window always work — AppleScript
launches Ghostty first if it isn't running. A new tab joins the front window if
one is open, or opens the first window otherwise.
Note: Ghostty's
new tab/new windowAppleScript handlers open the surface but returnerrAEEventNotHandled(-1708), soosascriptexits non-zero withGhostty got an error: Can't continue new tab. (-1708)even though the tab opened.wtrecognizes that benign signature and treats it as success, so the spurious error no longer surfaces.
Tab/window titles: Ghostty's AppleScript surface configuration exposes the working directory and command but not a settable title, so
wtdoesn't name the window/tab after the branch (the old tmux-nbehavior). Ghostty titles surfaces from the running program / shell instead.
wt switch builds a short AppleScript and runs it with osascript:
tell application "Ghostty"
activate
set cfg to new surface configuration
set initial working directory of cfg to "/path/to/worktree"
set command of cfg to "nvim"
new tab with configuration cfg
end tellBecause AppleScript addresses the running Ghostty app directly, this works the
same whether wt runs inside a Ghostty terminal or from somewhere with no TTY
at all (Claude Code's Bash tool, a launchd job, a script) — there's no session
to find or cache. If Ghostty isn't open, activate launches it.
wt ships an agent skill that teaches coding agents when
and how to use the CLI (e.g. after git worktree add, run wt switch <branch>
so a Ghostty tab pops open for you). Install it with Vercel's
skills CLI:
npx skills add johnpangalos/wtThe interactive prompt lets you pick which agents to install into (Claude Code, Cursor, Codex, OpenCode, and many more). Non-interactive, e.g. for Claude Code user-wide:
npx skills add johnpangalos/wt -a claude-code -g -yPrefer manual installation? Copy the skill folder into your agent's skills directory, e.g. for Claude Code:
cp -R skills/wt ~/.claude/skills/wtIn Claude Code the skill is also user-invocable as /wt. Typical flow:
$ git worktree add ../repo-feat -b feat
$ wt switch feat
A new Ghostty tab pops open (Ghostty comes to the front) with your editor at the worktree's path.
bun install
bun run build # produces bin/wt
bun test # build + run all tests (48 tests)
bun run test:fast # run tests against the last-built binary
bun run typecheck # tsc --noEmitTests use real git repos in $TMPDIR and a fake osascript on $PATH that logs its argv — no mocks of our own code, and nothing actually talks to Ghostty.
Releases are automated by release-please. Commits to main must follow Conventional Commits:
feat: ...— minor bump (while pre-1.0; major after 1.0)fix: ...— patch bumpfeat!: ...or aBREAKING CHANGE:footer — major bumpchore:,docs:,refactor:,test:,ci:,build:,perf:— no bump; may appear in the changelog
release-please opens a chore(main): release X.Y.Z PR that bumps package.json and updates CHANGELOG.md. Merging that PR tags the release and the binary-upload workflow publishes wt-darwin-arm64 and SHA256SUMS to the GitHub Release.
Released into the public domain under the Unlicense.