A fast Git/Jujutsu worktree workflow tool for AI coding agents (CLI: ws).
Spin up isolated worktrees in milliseconds, run an agent in each, then merge and
clean up — all from one short command. Distributed as a single static binary.
AI coding agents work best with isolated environments — one agent per worktree, no stepping on each other's files, no half-finished branches polluting your main checkout:
- Parallel execution — run multiple agents simultaneously without interference; each gets its own working directory and branch.
- Clean separation — a feature's work-in-progress never touches your main checkout. Throw the worktree away and your repo is pristine.
- Snap mode — a "use and discard" loop: create a worktree, run an agent in it, and on exit
wswalks you through merge-or-keep and tidies up. - Near-instant creation — Copy-on-Write (reflink) cloning makes
ws newnear-instant even on multi-gigabyte monorepos, using almost no extra disk. - Native git and Jujutsu (
jj) — both backends are first-class; colocated repos default to jj. - Terminal-tab aware — on Windows Terminal / iTerm2 / GNOME Terminal / WezTerm (and tmux),
ws newandws cdopen the worktree in a fresh tab/window and leave your current shell put.
ws new fix-auth -s claude # new worktree + branch, run Claude in it, merge on exit
ws ls # see every worktree and its base branch
ws cd fix-auth # jump into one (new tab if your terminal supports it)
ws merge -d # squash-merge back to the base branch and delete the worktree
ws clean # garbage-collect worktrees already merged- git ≥ 2.x (always required, including for colocated repos). Optionally
jjfor the Jujutsu backend. - A POSIX shell (bash / zsh / fish) or PowerShell — needed for the
cd-changing commands (ws setupinstalls the integration). - Copy-on-Write is opportunistic: a reflink-capable filesystem (Windows ReFS / DevDrive, Linux Btrfs / XFS, macOS APFS) enables it; everything else falls back to a plain copy automatically.
macOS / Linux:
curl -fsSL https://github.com/ZelAnton/agent-workspace/releases/latest/download/install.sh | shWindows (PowerShell):
iwr https://github.com/ZelAnton/agent-workspace/releases/latest/download/install.ps1 -UseBasicParsing | iexInstalls ws to ~/.agent-workspace/bin, adds it to your PATH, and runs ws setup for shell integration.
npm install -g @zelanton/agent-workspacews updatews update detects how ws was installed (~/.agent-workspace/install_channel):
- Shell installer — downloads the latest release from GitHub and atomically replaces itself (uses
self_replacefor the Windows .exe rename-trick). - npm — re-runs
npm install -g @zelanton/agent-workspace@latest.
Shell integration is installed automatically by both channels. To reinstall manually:
ws setupSupported shells: bash, zsh, fish, PowerShell
Remove the shell wrapper installed by ws setup:
ws uninstallIf the ws binary is missing or broken, run the standalone script instead — it works without a functioning binary:
# macOS / Linux
curl -fsSL https://github.com/ZelAnton/agent-workspace/releases/latest/download/uninstall.sh | sh
# Windows (PowerShell)
iwr https://github.com/ZelAnton/agent-workspace/releases/latest/download/uninstall.ps1 -UseBasicParsing | iexThe uninstall step does not delete ~/.agent-workspace/ (binary, channel marker, cached config) or any worktrees you created — both scripts print follow-up commands for those. If installed via npm, also run npm uninstall -g @zelanton/agent-workspace.
# Create a worktree and enter it
ws new feature-x
# ... develop, commit ...
# Merge back (merges to the branch you were on when creating)
ws merge # keeps worktree
ws merge -d # deletes worktree after mergeOther useful commands:
ws ls # List all worktrees (with BASE branch info)
ws cd feature-y # Switch to another worktree
ws cd # Return to main repositoryOne-liner for AI agent workflows:
ws new -s claude # Random branch name
ws new fix-bug -s codex # Specified branch name
ws new -s "claude --dangerously-skip-permissions" # Command with argumentsArgument quoting —
-stakes a single token. Use quotes whenever the command has flags or arguments (-s "agent --flag"), otherwise the shell hands the trailing args tows newinstead.Nested snap is refused — running
ws new -sfrom inside an existing worktree exits with an error. Runws cdto return to the main repo first.
Flow: Create worktree → Enter → Run agent → [Develop] → Agent exits → Check changes → Merge → Cleanup
After the agent exits — whether normally or with a crash / Ctrl+C — ws
checks the worktree state:
- No changes: Worktree cleaned up automatically
- Only commits (nothing uncommitted):
[m] Merge into base branch [q] Exit snap mode - Uncommitted changes:
[r] Reopen agent (let agent commit) [q] Exit snap mode (commit manually)
base_branch must still exist — if the worktree's base branch was deleted while the agent ran,
[m]errors out. Usews merge --into <branch>to pick an explicit target instead.
| Command | Description |
|---|---|
ws new [branch] |
Create worktree from current branch (random name if omitted) |
ws new --base <branch> |
Create from specific base branch (default: current branch) |
ws new -s <cmd> |
Create + snap mode |
ws new --no-cow |
Skip Copy-on-Write cloning for this creation |
ws new --no-tab / --in-new-tab |
Override terminal-tab behavior for this creation |
ws cd [branch] |
Switch to worktree (omit branch to return to main repo) |
ws ls |
List worktrees |
ws ls -l |
Show full path for each worktree |
ws mv <old> <new> |
Rename worktree (use . for current) |
ws rm <branch> |
Remove worktree (use . for current) |
ws rm -f <branch> |
Force remove with uncommitted changes |
ws clean |
Remove worktrees with no diff from their base branch (falls back to trunk); dirty worktrees are skipped |
ws clean --dry-run |
Preview which worktrees would be cleaned |
| Command | Description |
|---|---|
ws merge |
Merge to base branch (falls back to trunk, default: squash) |
ws merge -s <strategy> |
Merge with strategy (squash/merge) |
ws merge --into <branch> |
Merge to specific branch (overrides base) |
ws merge -d |
Delete worktree after merge (default: keep) |
ws merge -H |
Skip pre-merge hooks |
ws sync |
Sync from base branch (falls back to trunk, default: rebase) |
ws sync -s <strategy> |
Sync with strategy (rebase/merge) |
ws sync --from <branch> |
Sync from specific branch (overrides base) |
ws sync --continue |
Continue after resolving conflicts |
ws sync --abort |
Abort sync |
| Command | Description |
|---|---|
ws status |
Show current worktree info (also reports in-progress ws sync rebase/merge with recovery hints) |
ws repo-info |
Show the cached per-repo metadata (file count, size, origin URL, GitHub slug) |
ws repo-info --refresh |
Force-refresh that cache (otherwise auto-refreshed every 30 days) |
ws update |
Update to the latest version |
| Command | Description |
|---|---|
ws config list |
List supported per-repo keys with descriptions |
ws config get <key> |
Read a setting (e.g. workspace.alias, workspace.use_path_hash) |
ws config set <key> <value> |
Write a setting to the repo-level config |
ws config unset <key> |
Remove a setting |
ws exclude |
Open the TUI tree picker for [copy] exclude patterns |
ws exclude <pattern>... |
Add CoW exclude patterns (e.g. target node_modules '**/*.iso') |
ws exclude --list |
Show current exclude patterns |
ws exclude --remove <pattern> |
Drop a pattern |
ws exclude --clear |
Remove all patterns |
Both
ws configandws excludewrite the local repo-level.workspace.tomland keep it out of git automatically (via.git/info/exclude).
| Command | Description |
|---|---|
ws setup |
Install shell integration (auto-detect) |
ws setup --shell zsh |
Install for specific shell |
ws uninstall |
Remove shell integration (inverse of setup) |
ws init |
Initialize project config |
ws init --trunk <branch> |
Initialize with specific trunk branch |
ws init --merge-strategy <strategy> |
Set default merge strategy (squash/merge) |
ws init --sync-strategy <strategy> |
Set default sync strategy (rebase/merge) |
ws init --copy-files <pattern> |
Files to copy to new worktrees (repeatable) |
ws is built to be driven by AI agents, so every read/result command can emit a
stable JSON object instead of scraped text. Add the global --format json flag:
ws ls --format json # { "worktrees": [ { "branch", "base_branch", "is_current", "commits", ... } ] }
ws status --format json # { "branch", "merge_target", "commits", "uncommitted", "in_progress", ... }
ws repo-info --format json # { "repo_name", "origin", "total_files", "total_bytes", "from_cache", ... }
ws new feature-x --format json # { "branch", "path", "base_branch", "created", "snap" }
ws merge --format json # { "merged", "branch", "target", "commits", "deleted" }
ws sync --format json # { "action", "branch", "target", "strategy" }
ws clean --format json # { "dry_run", "cleaned": [...], "skipped_dirty": [...] }
ws rm . --format json # { "action": "removed", "branch", "returned_to" }
ws config list --format json # { "path", "keys": [ { "key", "kind", "value", "description" } ] }Every object carries a top-level schema_version (currently 1) so an agent can
gate on the shape it expects — see docs/json-output.md for the
full per-command roster and the versioning policy. mv, cd, exclude --list, and
config get emit JSON too.
Discipline: in JSON mode stdout carries only the single JSON object; all
progress, notices, and the update nag go to stderr (so ws ls --format json | jq
just works). Errors are emitted as { "error", "causes": [...] } on stderr with a
non-zero exit. ws new --format json prints the created worktree path on stdout —
an agent can read it directly instead of relying on shell integration. Human
output (the default, --format human) is unchanged.
Defaults to ~/.agent-workspace. Override via AGENT_WORKSPACE_DIR:
export AGENT_WORKSPACE_DIR=/data/agent-workspace[general]
merge_strategy = "squash" # squash | merge
sync_strategy = "rebase" # rebase | merge
copy_files = [".env", ".env.*"] # Gitignore-style patterns for files to copy
[hooks]
post_create = ["pnpm install"]
pre_merge = ["pnpm test", "pnpm lint"]
post_merge = []
copy_filesconstraints — patterns are gitignore-style and must stay inside the repo: leading/(absolute paths) and..traversal are rejected. Symlinks are not followed.Hook trust boundary — hooks run via
sh -c(orcmd /Con Windows) with no sandboxing or timeout. Treat.workspace.tomllike any shell script: only run repos whose hooks you wouldbashdirectly.Hook CWD —
pre_mergeandpost_mergealways run with the worktree root as the working directory.post_createruns in the new worktree.
Repo/worktree config lives in .workspace.toml — a local, per-machine file that ws keeps out of git by adding it to the repo's local exclude file (.git/info/exclude), so there's nothing to commit and your working tree stays clean (existing repos with a committed .agent-workspace.toml keep working: it's read as a fallback when no .workspace.toml is present). It layers in three tiers, later overriding earlier:
- global —
~/.agent-workspace/config.toml - repo-level —
.workspace.tomlat the main repo root - worktree-level —
.workspace.tomlat an individual worktree's root (overrides the repo-level file for commands run inside that worktree)
trunk is project-only; other fields are merged (copy_files / copy.exclude append, hooks replace per-phase when set, the rest are overrides). ws config and ws exclude write the repo-level file; worktree-level files are hand-authored.
[general]
trunk = "main" # Trunk branch (auto-detected if omitted)
vcs = "git" # Force a backend for this repo: "git" | "jj" (default: auto-detect)
merge_strategy = "merge" # Override global merge strategy
sync_strategy = "merge" # Override global sync strategy
copy_files = ["*.secret.*"] # Appended to global copy_files
[hooks]
post_create = ["pnpm install"] # Replaces global hooks if set
[copy]
# Gitignore-style patterns the Copy-on-Write path SKIPS when cloning the source
# repo into a new worktree. Anchor with a leading `/` for top-level only.
exclude = ["/target", "/node_modules", "**/*.iso"]
[create]
use_cow = true # Copy-on-Write worktree creation (default: true when supported)
[ui]
open_in_new_tab = true # Open ws new / ws cd in a new terminal tab (default: true)
[workspace]
alias = "myrepo" # Override the workspace directory name (default: repo basename)
use_path_hash = false # Append a `-<hash>` suffix to disambiguate same-named reposws speaks both git and Jujutsu. The backend is resolved once per invocation,
in this order:
--vcs <auto|git|jj>global flag[general] vcsin the repo-level.workspace.toml[general] vcsin the global config- auto-detection from
.git//.jj/presence
Colocated repos (both .git/ and .jj/) default to jj — you installed jj
for a reason, so ws respects it. Force git with --vcs=git or vcs = "git".
A handful of git-shaped operations have no clean jj analogue and return a clear
Error::Unsupported with a hint rather than guessing:
| Operation | jj guidance |
|---|---|
ws mv |
use ws rm + ws new |
ws sync --abort / --continue |
jj records conflicts in commits — resolve the files and re-run |
See AGENTS.md → "VCS backend compatibility" for the full
semantic-delta table (no staging area, bookmarks vs branches, atomic operations).
A child process can't change its parent shell's directory, so the commands that
move you (ws cd, ws new, ws rm, ws mv, ws merge -d, ws clean) can't
cd you directly. Instead the binary writes the destination to a temp file and
a tiny shell wrapper (installed by ws setup) reads it and performs the cd.
Consequences worth knowing:
- Run
ws setuponce per shell. Without it, thecd-changing commands still do their work but can't move your shell — andwsrefuses to delete or move the current worktree without integration (it would strand you in a deleted directory).cdto the main repo first, or install the wrapper. - The wrapper is bracketed by
# === agent-workspace BEGIN/END ===markers in your rc file;ws uninstallremoves exactly that block and nothing else. - On terminals with tab support,
ws new/ws cdopen a new tab instead, and the originating shell stays exactly where it was.
ws cddoesn't change my directory — shell integration isn't active in this shell. Runws setup, then open a new shell (orsourceyour rc file).- "Refusing to remove/move the current worktree without shell integration" —
same cause. Run
ws setup, orws cdback to the main repo and retry. ws newopened a new tab I didn't want — pass--no-tab, or set[ui] open_in_new_tab = falsein your config.- Creation isn't using Copy-on-Write — the source repo and
$AGENT_WORKSPACE_DIRmust be on the same reflink-capable volume.wsprobes with a real sentinel reflink and falls back to a plain copy silently otherwise. Force a plain copy with--no-cow. - Merge says "Base branch '…' no longer exists" — the worktree's base branch
was deleted. Pick an explicit target:
ws merge --into <branch>.
~/.agent-workspace/
├── bin/ # ws binary (shell installer)
├── config.toml # Global config
├── install_channel # 'npm' or 'shell'
└── {project}-{hash}/ # one dir per source repo
├── {branch_name}.toml # Worktree metadata
├── {branch_name}/ # Worktree directory
└── ...
v0.13.6 layout change — earlier versions added an extra
workspaces/directory between the base and the per-project dirs (~/.agent-workspace/workspaces/{project}-{hash}/{branch}/). That intermediate level is gone now. Existingworkspaces/directories from pre-0.13.6 installs are NOT migrated automatically — the worktrees inside have absolute paths baked into git's gitlink / jj's workspace registration that would break on relocation.wsprints a one-line migration nudge on startup whenever it spots a non-empty legacyworkspaces/dir. Runws rm <branch>from inside each old worktree to clean up, orrm -rf ~/.agent-workspace/workspacesif you don't need them.
MIT © Anton Zhelezniakou. Fork of nekocode/agent-worktree.
