Skip to content

Repository files navigation

wt

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.

Requirements

  • macOS (Ghostty's AppleScript support is macOS-only)
  • git
  • Ghostty 1.3 or newer (AppleScript support landed in 1.3)

Install

macOS (Apple Silicon):

curl -fsSL https://raw.githubusercontent.com/johnpangalos/wt/main/install.sh | sh

Installs 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/wt

Build from source

Requires 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 $PATH

bin/wt is a self-contained native binary — Bun is not needed at runtime.

Usage

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               # usage

Agent-aware listing

If 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.

Updating

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.

Configuration (env vars)

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 AppleScript activate it inherits the macOS GUI launch PATH (/usr/bin:/bin:…), not your interactive shell PATH. A bare nvim installed under /opt/homebrew/bin would then fail to launch. wt runs from your shell with the full PATH, so it resolves the WT_CMD/$EDITOR executable to an absolute path before handing it to Ghostty. A command that already contains a /, or whose executable isn't on your PATH, 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 window AppleScript handlers open the surface but return errAEEventNotHandled (-1708), so osascript exits non-zero with Ghostty got an error: Can't continue new tab. (-1708) even though the tab opened. wt recognizes 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 wt doesn't name the window/tab after the branch (the old tmux -n behavior). Ghostty titles surfaces from the running program / shell instead.

How it works (AppleScript)

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 tell

Because 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.

Agent skill (Claude Code & friends)

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/wt

The 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 -y

Prefer manual installation? Copy the skill folder into your agent's skills directory, e.g. for Claude Code:

cp -R skills/wt ~/.claude/skills/wt

In 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.

Development

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 --noEmit

Tests 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

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 bump
  • feat!: ... or a BREAKING CHANGE: footer — major bump
  • chore:, 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.

License

Released into the public domain under the Unlicense.

About

Pick and switch git worktrees from the shell. Opens each worktree in a new tmux/zellij window.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages