Skip to content
penard-monkeyPublic

About

A durable place for every work stream — git worktree + tmux session + AI pane per stream; branches flow through, the place stays.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

worktrees

A durable place for every work stream. Not a throwaway worktree per branch — a place you keep: ui-changes, prod-reviews, mcp-server. Each place is a git worktree + a tmux session running your AI CLI (claude, codex, opencode, …) — with --spare, a second pane installs deps and gives you a shell — and it lives as long as the work does: an afternoon, or weeks of async iteration until it's right.

Branches flow through places. The place keeps the expensive parts (directory, AI CLI history, node_modules, the running session); a branch is the unit of work currently on it. Ship a branch, switch the place to the next one, keep working. The place is named after its first branch unless --name says otherwise; switch moves it to the next branch without touching anything expensive.

worktrees new feat/checkout          # worktree + branch + tmux (AI | deps+shell)
worktrees ls                         # what places exist, what's on them
worktrees switch feat/checkout-v2    # same place, next branch (run inside it)
worktrees open feat-checkout         # reattach later
worktrees rm feat-checkout           # tear the place down when it's done

Works in any git repo. No config required.

Desktop app

Same engine, a native window. The macOS app (Tauri) links the identical Rust core in-process — no daemon, no subprocess — so everything the CLI knows, the app shows live: every place grouped by how active it is, ahead/behind and dirty state per branch, and a tmux-attached terminal for each session right in the window. Create a place, drop into its session, switch branches, tear it down — without leaving the app.

Creating a place and opening its session in the worktrees desktop app

New → list → open — driven headlessly against the built-in mock harness. The real app embeds a live tmux terminal running your AI CLI.

Every place, grouped by how active it is, with a home panel to resume where you left off:

worktrees desktop app — workspace overview

Open a place to see its branch state, notes, and embedded terminal:

worktrees desktop app — a place with its embedded terminal

Install it alongside the CLI on macOS (WORKTREES_INSTALL_APP=1, or answer the installer prompt; from a clone, make install-app) — see Install.

Install

Fresh machine (installs the latest release to ~/.local/bin):

curl -fsSL https://raw.githubusercontent.com/penard-monkey/worktrees/main/install.sh | bash

Teams should pin a release for reproducibility. Setting the version is the form that works for every tag:

curl -fsSL https://raw.githubusercontent.com/penard-monkey/worktrees/v0.29.0/install.sh | WORKTREES_INSTALL_VERSION=v0.29.0 bash

For tags after v0.29.0 the tagged script installs its own release on its own, so …/vX.Y.Z/install.sh | bash is enough. The app can also install any release, including an older one, from Settings → Updates.

The installer fetches the prebuilt binary for your platform (macOS/Linux, x86_64/arm64); with no match, or WORKTREES_INSTALL_FROM_SOURCE=1, it builds from source with cargo.

On macOS it also offers the desktop app (worktrees.app → /Applications, checksum-verified; the unsigned bundle's quarantine attr is stripped on your explicit opt-in). Non-interactive runs skip the prompt — opt in/out explicitly:

WORKTREES_INSTALL_APP=1 \
  curl -fsSL https://raw.githubusercontent.com/penard-monkey/worktrees/main/install.sh | bash

From a clone, make install-app builds the app locally and installs it to /Applications (no signing or quarantine involved).

Or clone and build (make install compiles the release binary and symlinks it — git pull && make install upgrades):

git clone https://github.com/penard-monkey/worktrees && cd worktrees && make install

Re-running the installer upgrades. install.sh --uninstall removes the binary (your repos' worktrees and tmux sessions are untouched).

Updating

Re-running the installer is the updater — it resolves the latest release, prints the old → new version, verifies the checksum, and replaces the binary in place:

curl -fsSL https://raw.githubusercontent.com/penard-monkey/worktrees/main/install.sh | bash
worktrees --version   # confirm

Roll back (or hold) a version by pinning the release tag:

WORKTREES_INSTALL_VERSION=v0.1.0 \
  curl -fsSL https://raw.githubusercontent.com/penard-monkey/worktrees/main/install.sh | bash

The same re-run updates the desktop app when you opt in (WORKTREES_INSTALL_APP=1 or answer the prompt); quit + reopen the app to pick up the new version.

From a clone instead: git pull && make install (note: make install symlinks the clone's release build — later cargo builds in that clone update it too. For a frozen copy, install -m 755 target/release/worktrees ~/.local/bin/worktrees). App from a clone: git pull && make install-app.

Updates never touch your repos' state: worktrees under .worktrees/, the declared store (.worktrees.places.json, schema-versioned), tmux sessions, and ~/.config/worktrees/config all survive binary swaps. Running sessions keep running — the CLI attaches to tmux, it doesn't own it.

Requires: git ≥ 2.23. tmux ≥ 1.9 recommended (new degrades to --no-tmux without it; open needs it). Prebuilt binaries for macOS + Linux (x86_64/arm64); building from source needs a Rust toolchain.

Commands

worktrees new <branch> [base]         create a worktree + tmux (one AI pane; --spare adds a shell)
worktrees new <branch> --name <topic> ...place named independently of the branch
worktrees co  <branch>                checkout a REMOTE branch (fetch if needed)
worktrees switch [<worktree>] <branch> [base]   move a worktree to another branch
worktrees open <name>                 reopen a worktree's tmux session
worktrees close <name> [name...]      end the tmux session (worktree stays; also: main)
worktrees ls [--json]                 list worktrees + their state (--json: machine-readable)
worktrees rm <name> [name...]         tear one (or more) down
worktrees                             (no args) → ls

new/co/switch are do-what-I-mean: reuse an existing worktree, check out an existing local or remote branch (fetching it first), or create a new branch off [base] (default main). origin/feat/x is accepted and normalized to feat/x. If a branch already lives in a differently-named worktree (after a switch), new/open find and reuse that place instead of failing.

Flags:

Command Flags
new/co/open -r/--resume (append the AI resume flag) · --ai <cmd> (AI pane command for this run) · --model <m> (the agent's model for this launch) · --force (launch despite an advisory refusal — see below) · --spare (add a spare shell as pane 1; for new it runs the detected install there — without it the install is printed as a then: hint) · --no-spare (accepted, and the default: single pane)
new/co --no-install · --no-tmux · --no-attach · --no-fetch · --name <topic> · --brief <text> (write the agent's task to .planning/brief.md and launch the selected agent on it)
switch --force (despite uncommitted changes) · --no-fetch · -y
rm --branch (delete the branch too) · --force · -y/--yes

An agent is not started on a plan window that is nearly spent. Before a session is created, the harness asks its own provider how much of the current window is left and refuses the LAUNCH when the provider's own grade says warning (Codex: 80%) — naming the window, the percentage and when it resets. The worktree, the branch and the brief are created either way: only the agent waits, so nothing has to be re-typed once the window rolls over. --force launches anyway (the app: Launch anyway; MCP: force: true), and worktrees open <slug> --force is the retry it prints.

It is yours to tune, in Settings → Behavior → Plan limits or in ~/.config/worktrees/config.toml (a repo's .worktrees.toml cannot set it):

[quota]
gate = false            # never refuse a launch on usage
weekly_warn_pct = 90    # weekly windows refuse at >= 90%, whatever the provider grades

The percentage applies to weekly windows only; the 5-hour window keeps the provider's grade unless the gate is off. Both are read at launch time, so a change reaches the next launch everywhere, running agents' MCP servers included.

It fails open in every direction — no reading, an unreadable one, a provider with no allowance to report, or --no-tmux (no agent starts at all) and the launch simply proceeds. It is deliberately not a burst limiter: lanes bill after their first turns, so eight started at once are all admitted and the gate first speaks once the window is already spent. WORKTREES_USAGE_PROBE=off switches the gate off entirely (the app's usage meter keeps reading). The check costs a launch one reading of the provider's usage — usually cached, but on a network that hangs rather than fails it can hold a new for up to ~15s (Claude's GET) or ~13s (Codex's probe) before failing open; --force skips the reading altogether, and so does gate = false.

Guards you'll be glad exist: dirty worktrees refuse to switch/rm (override with --force); a stale unregistered dir under .worktrees/ is never treated as a worktree (git would silently operate on your main checkout); switch from inside worktree A targeting worktree B asks first; a typo'd worktree name can't silently mint a junk branch.

The tmux layout

Each worktree gets a session named <prefix>-<slug>: pane 0 launches your AI CLI through an interactive shell (so shell aliases resolve), and that is the whole session by default. --spare adds pane 1, which runs the detected package-manager install (pnpm/bun/yarn/npm, by lockfile) and drops to a shell; without it new prints the install command as a then: hint. Sessions are reused, never duplicated — open finds a session already living in the worktree even under a different name.

JSON output

worktrees ls --json (or WORKTREES_JSON=1 worktrees ls) emits a machine-readable snapshot instead of the table — for editors, scripts, and tooling. The human ls output is byte-for-byte unchanged. Shape (schema_version 1):

  • a wrapper {schema_version, repo, prefix, places_file, places:[…]};
  • the main checkout first (slug:"(main)", is_main:true), then each worktree in the same recency order as the table;
  • per place: slug, path, branch (null when detached, with detached:true), dirty, dirty_files, ahead, behind, upstream (the last three null when there's no upstream), created, created_epoch, last_commit_epoch, last_commit_subject, tmux_session:{name,up}, claude_session_present, install_cmd, and lifecycle_effective.

All state is derived live on every call — nothing is cached. stack and declared are reserved (null for now). No jq required to produce it.

Configuration

Precedence: flag > environment > user config > default. User config lives at ~/.config/worktrees/config (respects $XDG_CONFIG_HOME) — key = value lines, # comments. It is parsed as data, never executed.

What Flag Env Config key Default
AI pane command --ai <cmd> WORKTREES_AI_CMD ai_cmd claude
AI resume argument (-r appends it) — WORKTREES_AI_RESUME_ARG ai_resume_arg Claude: -r; Codex: exact session (override ignored)
Session/name prefix — WORKTREES_PREFIX prefix repo dir name
Codex permissions (ask, auto-review, full) — WORKTREES_CODEX_PERMISSIONS codex_permissions auto-review
# ~/.config/worktrees/config
ai_cmd = codex
ai_resume_arg = resume
  • ai_cmd = none (or --ai none) → pane 0 is a plain shell, no AI.

  • WORKTREES_CLAUDE_CMD is honored as a deprecated alias of WORKTREES_AI_CMD.

  • A repo can pin its prefix with a committed .worktree-prefix file (one line); the env var wins over it.

  • Pane 0 hands the command to your $SHELL -ic — aliases work; assumes a POSIX-ish (bash/zsh/sh) login shell.

  • The app's Settings → Codex → Permissions overrides env and config for launches the app makes; the CLI (and MCP create_worktree) use env/config. auto-review is --approve-for-me plus a sandbox that can also write the repo's git common dir (so git commit works in a worktree — that includes .git/hooks and .git/config) and reach the network.

  • Every Codex launch also passes -c project_doc_fallback_filenames=["CLAUDE.md"], so a CLAUDE.md-only project briefs Codex without a repo change. worktrees agent-setup checks the default branch and can make AGENTS.md the one instruction file (CLAUDE.md becomes an @AGENTS.md stub) on a local agent-instructions branch (fix --push also pushes it and opens the PR), and link your ~/.claude/skills into ~/.agents/skills where Codex looks.

  • worktrees rules apply writes the worktrees way of working into the repo, for every agent including ones worktrees did not start: a managed block at the end of AGENTS.md (places, lanes, the MCP tools, planning, finishing), the close-out skill, and a .claude/close-out.md that is the repo's to edit. Same local branch, same layout fix; rules status says whether it is current, and a re-apply updates the block in place without touching anything outside its markers. MCP: rules_status, apply_rules.

Examples: --ai claude, --ai "claude --model opus", --ai codex, --ai opencode, --ai none.

Claude and Codex can both be configured, but each worktree runs one provider session at a time. Switching providers closes the current managed tmux session before opening the other; each provider keeps its own saved conversation. Settings → Claude and Settings → Codex can both be configured. When both CLIs are available, the New worktree dialog asks which agent to start; it preselects Settings → Commands → Default agent. Entering an existing place keeps its running provider; the default is used when no agent is running. Changing the default does not stop a running session. The CLI keeps its ai_cmd default and accepts open <place> --ai claude|codex and close <place> --ai claude|codex. An unqualified close closes both managed agent sessions. Codex resumes the newest interactive conversation started in that exact place with codex resume <session-id>, excluding subagents and scripted runs. Without a matching conversation it starts fresh. The generic resume-argument override does not apply to Codex: its --last selector can include sibling worktrees, so Worktrees always names the session explicitly.

Worktrees-launched Codex sessions require ChatGPT account sign-in. Run codex login once and complete its browser flow; codex login status shows the active method. Worktrees uses Codex's saved credentials and does not ask for or store an OpenAI API key. If Codex is currently signed in with an API key, sign out with codex logout and sign back in with codex login before opening it from Worktrees.

Connect Worktrees tools separately in Settings → Claude and Settings → Codex, or run worktrees mcp --install --ai codex for Codex. The unqualified MCP setup command still targets Claude. The MCP create_worktree tool accepts an optional provider: "claude" | "codex"; without one it uses the project's AI command.

Agents in different places of one project can talk through the same server, whichever provider each one is. report posts a message to another place ((main) unless told otherwise; reply_to threads an answer), messages reads what was sent to your place, and wait blocks for up to two minutes until another place's agent goes idle or a message arrives, so an agent waits instead of polling. The sender is the place the agent is working in, never a name it passes; anything running as your user can still write the log, so that is the trust boundary. Messages live in the repository's shared git data, untracked, and expire after a week. With --mutations, send types one line into another place's Codex, labelled as coming from the sending place and never while that Codex is waiting on an approval; for a Claude place it answers with the session name to use with Claude's own SendMessage.

Compatibility notes

  • macOS: stock /bin/bash 3.2 is fully supported (CI runs the whole suite on it).
  • .worktrees/ is added to .git/info/exclude automatically — worktrees never show up as untracked files.
  • rm deletes the worktree and its tmux session; the branch survives unless you pass --branch.

Development

git clone --recurse-submodules https://github.com/penard-monkey/worktrees
make check            # shellcheck + bash-3.2 gates + bats suite
make test-real-tmux   # 3 integration smokes against real tmux

Tests are bats-core (vendored as submodules); the suite fakes tmux with a PATH shim so every pane command is assertable, and CI runs ubuntu + macos, the latter twice — once under stock bash 3.2.

Running the desktop app from source

The app is Tauri, so cargo is driven for you — there is no cargo run. Node must match .nvmrc (pnpm 11 refuses anything older), and dependencies are installed once per clone:

nvm use                      # or any node >= the version in .nvmrc
cd app && corepack pnpm install   # first time only
make dev-app                      # or, from app/: corepack pnpm tauri dev

Run corepack pnpm, from app/ — not a bare pnpm, and not pnpm --dir app from the root. From the root, corepack fixes its version from the current directory's package.json before pnpm reads the flag, and the root has none, so you get ERR_PNPM_BAD_PM_VERSION against the pnpm@11.5.2 pin. And a bare pnpm may not exist at all: nvm installs it per node version, and the version .nvmrc pins ships only corepack node npm npx, so nvm use can take pnpm off your PATH. corepack comes with node and is always there. The make targets do both for you.

That builds the app crate and serves the frontend on port 1420, with hot reload on the TypeScript/CSS side; a Rust change rebuilds and relaunches the window. Real git and tmux are used, so it acts on whatever projects you have registered.

To work on the UI alone — no Rust build, no tmux, fake backend — use the mock harness, which runs the real App.tsx against fixtures in a plain browser:

cd app && corepack pnpm dev:mock --port 1425    # any port but 1420

Both are development loops. To actually use a locally built app, see make install-app under Install — that produces the bundle and puts it in /Applications.

Scripts

Script What it does Touches
sandbox.sh Builds the current branch and hands you an isolated worktrees to test it in. --app launches the desktop app against it. a scratch repo, or one you name with --repo
record-readme.sh Regenerates the README's desktop media (docs/media/desktop-*.{gif,png}). nothing — mock harness
shoot-profiles.sh Regenerates the AI-profiles screenshots for docs/ai-profiles.html. nothing — mock harness
record-profiles.sh Regenerates that page's walkthrough clip (walkthrough.mp4). nothing — mock harness

The three media scripts drive the mock harness — the real App.tsx against fixtures, in a plain browser — so they are deterministic and cannot touch your projects. The .py file beside each is the Playwright driver; run the .sh, which starts the harness, waits for it, drives it, encodes, and cleans up.

sandbox.sh is the exception: it builds and runs the REAL binary, against a scratch repo by default and against whatever you pass to --repo if you ask.

They need Python Playwright and Chromium once — pip install playwright && playwright install chromium — plus ffmpeg for the two that record video.

Testing a branch without disturbing an app you already have open

eval "$(app/scripts/sandbox.sh)"    # CLI: isolated env + a scratch repo
app/scripts/sandbox.sh --app        # …or the desktop app against that sandbox
app/scripts/sandbox.sh --clean

This matters more than it sounds. tmux session names are <prefix>-<slug> derived from the repo, so a second build computes the same name and attaches to the session your open app is using — closing it in one kills it in the other. The sandbox takes a per-branch prefix (sbx-<branch>-<slug>), which both prevents that and is how you tell them apart:

tmux ls | grep sbx-      # sandboxes, one prefix per branch
tmux ls | grep -v sbx-   # yours, untouched

It also isolates the AI-profile store and, with --app, overrides the bundle identifier so the sandbox app gets its own ui-state.json instead of sharing yours.

License

MIT

About

A durable place for every work stream — git worktree + tmux session + AI pane per stream; branches flow through, the place stays.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages