Run git worktree sandboxes, locally or on remote hosts over SSH, with minimal setup.
wt clones your repo into a bare mirror, then spins up isolated worktrees you can enter, run commands in, and throw away when you're done. Works on your machine or any box you can SSH into, with SSH keys, agents, environments, and more included.
wt
├── host
│ ├── add [name]
│ ├── ls
│ ├── check <name>
│ ├── rm <name>
│ ├── map <name> <localPort> <hostPort>
│ └── unmap <name> <localPort>
├── up [name]
├── local [name]
├── rename <old> <new>
├── enter <name>
├── run <name> <cmd>
├── sessions
├── gc
├── doctor
├── bootstrap
├── ls
├── rm <name>
└── status <name>
# with any package manager, but bun is recommended
bun install -g @abhi-arya1/wtRequires Bun (v1.3.3+). You can also build a standalone binary with bun build --compile if you prefer — no runtime needed, but the binary will be larger.
git clone https://github.com/abhi-arya1/wt.git && cd wt
bun install
bun run build
bun linkOr just run it directly during development:
bun run dev -- <command>You're in a git repo. You want an isolated copy to mess around in without touching your working tree.
wt local my-experiment --enter
# you're now in a worktree at .wt/sandboxes/my-experiment
# type `exit` to leave the sandbox shell
# or just let it pick the name from your current branch
wt local --enter
# sandbox named after your current branchYou have a server you can SSH into. Register it as a host, then spin up sandboxes there.
wt host add prod-box --ssh user@10.0.0.5 --root /srv/wt
wt up my-feature --host prod-box --enterwt rm my-experiment # remove a specific sandbox
wt gc # remove all sandboxes older than 7 days
wt gc --older-than 1d # more aggressive
wt gc --dry-run # see what would get deletedIf you are using an agent to make sandboxes for multiple agents to work within them, you can install wt then give agents the SKILL.md file. This will allow them to understand usage of wt and create sandboxes on your behalf.
-
Make sure the remote box has
gitinstalled and your SSH key is authorized. -
Add the host:
wt host add myserver --ssh me@myserver.com --root /home/me/wt-sandboxesThis registers the host and runs a connectivity check. If it passes, you're good.
- Check what's installed on the remote:
wt bootstrap --host myserver --tmux --agents claude,opencodeThis tells you what's present and what's missing. It doesn't install anything -- just reports.
- Verify everything works:
wt doctor --host myserver- Create a sandbox from any local git repo:
cd ~/projects/my-app
wt up test-sandbox --host myserver --enterYou're now in a shell on myserver inside a worktree of your repo. .env files from your local directory get copied over automatically.
- Run commands without entering:
wt run test-sandbox -- make build
wt run test-sandbox -- bun test- Use tmux for persistent sessions:
wt enter test-sandbox --tmux
# detach with ctrl-b d, reattach later with the same commandSay you need to test three branches at once.
wt up --branch feature/auth
wt up --branch feature/payments
wt up --branch fix/header-bug
wt ls
# NAME HOST REF CREATED
# auth local a1b2c3d4 2/8/2026
# payments local e5f6g7h8 2/8/2026
# header-bug local i9j0k1l2 2/8/2026
wt run auth -- bun test
wt run payments -- bun test
wt run header-bug -- bun test
# rename one if you want
wt rename auth login-revamp
# done, clean up
wt rm login-revamp
wt rm payments
wt rm header-bugUse -i to point at a specific key when adding a host, and -p for a non-standard port:
wt host add mybox --ssh deploy@10.0.0.5 --root /srv/wt -i ~/.ssh/id_mybox -p 2222These are stored in config and used for every SSH and SCP operation to that host. If you don't pass -i, wt uses whatever OpenSSH picks from your agent or default key.
When you run wt up --host myserver, wt tells the remote to git clone --bare --mirror <origin>. That means the remote host needs access to your git remote (e.g. GitHub). For private repos, you have two options:
Option 1: SSH agent forwarding (recommended)
Add ForwardAgent yes for the host in your ~/.ssh/config:
Host myserver
HostName 10.0.0.5
User deploy
ForwardAgent yes
Now your local SSH keys are available on the remote when wt runs git commands. No keys need to be deployed on the server.
Option 2: Deploy a key on the remote
Add an SSH key on the remote host and register it as a deploy key with your git provider (e.g. GitHub deploy keys). This works without agent forwarding but means the remote has standing access to the repo.
wt doesn't have explicit jump host flags, but it passes the SSH target directly to OpenSSH, so anything in your ~/.ssh/config is honored. To reach a host behind a bastion:
Host bastion
HostName bastion.example.com
User ops
Host internal-box
HostName 10.0.1.50
User deploy
ProxyJump bastion
ForwardAgent yes
Then register it using the alias:
wt host add internal-box --ssh internal-box --root /srv/wtwt will connect through the bastion transparently. The same applies to ProxyCommand, custom ControlMaster settings, or any other OpenSSH config directives.
Sandboxes are regular git worktrees — standard git commands work as expected. Push branches and create PRs the same way you normally would:
wt enter my-sandbox
git checkout -b my-feature
# make changes
git add .
git commit -m "my changes"
git push -u origin my-feature
# create a PR with the GitHub CLI
gh pr create --title "My feature" --body "Description of changes"You can also do this without entering the sandbox:
wt run my-sandbox -- git push -u origin my-feature
wt run my-sandbox -- gh pr create --title "My feature" --body "Description"Note: Because wt uses git clone --bare --mirror, the mirror fetches all remote refs including GitHub's internal PR refs (refs/pull/*/head). A plain git push with no arguments may try to push these back and produce harmless remote rejected errors. To avoid this, push specific branches explicitly (git push origin my-branch) or set git config --global push.default current.
Every sandbox can have a tmux session tied to it. Sessions are named wt-<sandboxId>.
wt enter my-sandbox --tmux # creates or reattaches to tmux session
wt sessions # list all wt-managed tmux sessions
wt sessions --host prod-box # list sessions on a remote hostCreate a sandbox worktree on a host. If name is omitted, it defaults to the branch name from -b / --ref, or the current branch. When -b is not provided, the sandbox stays on the same branch you're currently on.
| Flag | Description |
|---|---|
-H, --host <name> |
Target host (defaults to configured default, falls back to local) |
-b, --branch <name> |
Create or use a branch with this name |
-r, --ref <ref> |
Git ref to check out (branch, tag, or sha that must exist) |
-e, --enter |
Enter the sandbox after creating it |
--tmux |
Use tmux when entering (implies --enter) |
--json |
JSON output |
Shorthand for wt up [name] on the local host. Same options minus --host.
| Flag | Description |
|---|---|
-b, --branch <name> |
Create or use a branch with this name |
-r, --ref <ref> |
Git ref to check out (branch, tag, or sha that must exist) |
-e, --enter |
Enter the sandbox after creating it |
--tmux |
Use tmux when entering (implies --enter) |
--json |
JSON output |
Rename a sandbox.
| Flag | Description |
|---|---|
--json |
JSON output |
Open a shell inside a sandbox.
| Flag | Description |
|---|---|
--tmux |
Use a tmux session instead of a plain shell |
--json |
Print sandbox record as JSON without entering |
Run a command inside a sandbox. Streams output by default.
| Flag | Description |
|---|---|
--json |
Capture stdout/stderr and return as JSON (must come before --) |
--quiet |
Suppress non-error output (must come before --) |
Note: flags for wt run itself go before --. Everything after -- is passed to the command.
wt run my-sandbox --json -- git log --oneline -5List all sandboxes.
| Flag | Description |
|---|---|
-H, --host <name> |
Filter by host |
--json |
JSON output |
Remove a sandbox. Deletes the worktree directory, metadata, and config entry. Prunes the mirror's worktree references.
| Flag | Description |
|---|---|
--json |
JSON output |
Show sandbox details: host, ref, path, age, whether the directory exists, and whether a tmux session is active.
| Flag | Description |
|---|---|
--json |
JSON output |
List active wt-* tmux sessions.
| Flag | Description |
|---|---|
-H, --host <name> |
Target host |
--json |
JSON output |
Garbage-collect stale sandboxes. A sandbox is stale if it's older than the threshold or its directory no longer exists.
| Flag | Description |
|---|---|
-H, --host <name> |
Target host (omit for all hosts) |
--older-than <dur> |
Age threshold, e.g. 7d, 24h, 1w (default: 7d) |
--dry-run |
Preview what would be deleted |
--json |
JSON output |
Check that git, bun/node, and tmux are available on a host.
| Flag | Description |
|---|---|
-H, --host <name> |
Target host |
--json |
JSON output |
Check host readiness. Reports what's installed and what's missing. Does not install anything.
| Flag | Description |
|---|---|
-H, --host <name> |
Target host |
--tmux |
Include tmux in checks |
--agents <list> |
Comma-separated agent CLIs to check (e.g. claude,opencode) |
--json |
JSON output |
Register or update a remote host.
| Flag | Description |
|---|---|
-s, --ssh <target> |
SSH target (alias, user@host, or ssh://user@host:port) |
-r, --root <path> |
Remote base directory (absolute path) |
-d, --default |
Set as default host |
-p, --port <n> |
SSH port |
-i, --identity <path> |
Path to SSH identity file |
-t, --connect-timeout <s> |
Connection timeout in seconds (default: 10) |
-l, --labels <k=v,...> |
Comma-separated key=value labels |
--no-check |
Skip connectivity check |
--json |
JSON output |
List all configured hosts.
| Flag | Description |
|---|---|
--json |
JSON output |
Test SSH connectivity and capabilities of a host.
| Flag | Description |
|---|---|
--json |
JSON output |
Remove a host.
| Flag | Description |
|---|---|
-y, --yes |
Skip confirmation prompt |
--json |
JSON output |
Add a port mapping to a host. When you SSH into the host (via wt enter, wt up --enter, etc.), the mapping is applied as -L localPort:localhost:hostPort, forwarding traffic from your local port to the remote port.
| Flag | Description |
|---|---|
--json |
JSON output |
# Forward local port 3000 to remote port 8080
wt host map myserver 3000 8080
# Now when you enter a sandbox, localhost:3000 reaches the remote's port 8080
wt enter my-sandbox
curl localhost:3000 # hits remote:8080Mappings are stored in config and persist across restarts. If a mapping for the same local port already exists, it is updated.
Remove a port mapping from a host.
| Flag | Description |
|---|---|
--json |
JSON output |
wt host unmap myserver 3000wt creates a bare mirror of your repo, then uses git worktree add to spin up isolated checkouts. Each sandbox gets its own directory and metadata file.
.wt/ # local root (inside your repo)
mirrors/
<repoId>.git/ # bare mirror
sandboxes/
<name>/ # worktree checkout
meta/
<sandboxId>.json # sandbox metadata
Remote hosts use the same layout under the configured root path (e.g. /srv/wt). All remote operations go over SSH.
Config lives at ~/.config/wt/config.json and stores hosts and sandbox records. Every structured command supports --json for scripting.
Every command that produces structured output supports --json. Errors in JSON mode return:
{
"ok": false,
"error": "what went wrong"
}This makes it straightforward to compose wt with other tools, scripts, or agents.
Unsure if this will be added, but I'd like to add:
- Cloud provider abstractions (AWS, Fly.io, etc.)
- Automatic download for sandbox dependencies
- Better observability, error handling, cleanup, and devex