Worktrees
Agents working in parallel need isolation, and Git already ships the primitive: a worktree (git worktree) is a second checkout of your repository, in its own directory…
Agents working in parallel need isolation, and Git already ships the primitive: a worktree (git worktree) is a second checkout of your repository, in its own directory, on its own branch, sharing the same history. RimZ puts that primitive behind one flag. Add -w to any launch and the whole layout (agents, your editor, a shell) opens inside a fresh tree, seeded and ready to work.
rimz agents claude,codex -w feat-a # a pair, isolated on their own branch
rimz teams forge -w feat-b # a whole team, its own worktree
rimz agents codex --from-pr 42 # a tree checked out from a pull requestWhy a tree per task
Your repository normally has one working directory with one branch checked out, and every agent you start shares it. Two agents in one checkout overwrite each other's edits, trip each other's builds, and braid two tasks into one diff, with your own uncommitted work sitting in the blast radius.
A worktree gives each line of work its own directory and branch, backed by the same repository. Edits in one tree stay invisible to its siblings until you merge, and you keep as many trees side by side as you have tasks. Point one agent at a bug fix and another at a refactor and let both run. Agents that should collaborate, like a team, share one tree; nothing touches your main checkout.
Because each tree is a real branch, the natural unit of work is one worktree to one pull request. Launch a team into a tree, let it carry the change from plan to reviewed diff, open a PR from the branch, and reclaim the tree once the PR merges.
What RimZ adds to git worktree
git worktree hands you the tree and nothing else. Around it sits a lifecycle you run once per task, several times a day once agents do the work: create the tree, copy in the untracked files it needs (.env, local config), point your editor and a shell at the new directory, and tear tree and branch down once the work merges.
Claude Code solves part of this. claude --worktree opens the session in a fresh tree, minimal and genuinely useful, but Claude's alone. RimZ gives every agent it drives the same flag and runs the whole lifecycle behind it:
- The whole layout lands in the tree.
vim,codex+termroots your editor, the agent, and a shell in one checkout, and a team isolates as a unit. Everyone in the tree works on the same files, so you edit what the agent edits and run its build in the shared shell. - The tree opens ready to run. Two committed manifests seed every new tree with the local files and shared directories a fresh checkout does not carry (seed the tree).
- The tree is addressable. Its name is also a channel name, so
@coder#feat-areaches the coder in that tree, and the sidebar groups the tree's panes as one block. - Cleanup proves the work landed. RimZ reclaims a tree only once its content is verifiably on its base branch, and only a tree it created itself; worktrees from your own workflow keep running beside RimZ's, untouched (cleanup).
The wrapper stays thin: every step is plain Git, a file operation, or your own shell command (what -w does on your machine).
Open a worktree
-w (--worktree) puts the whole layout in a fresh RimZ-owned worktree. Pass a name to create or reuse a specific one, or a bare -w for a generated name like brisk-harbor. The layout grammar (, + /) is the same one Agents covers; -w only decides where it lands.
rimz agents claude,codex -w feat-a # two agents in one isolated tree
rimz agents planner,coder+reviewer -w feat-b # a whole layout, its own branch
rimz agents claude -w # bare -w: a generated name
rimz agents codex -w feat/login # branch feat/login, tree feat-loginGive the name without the channel's #: -w feat-a creates the tree feat-a and names its channel #feat-a. A # on the name never helps. Unquoted, your shell eats #feat-a as a comment, so RimZ sees a bare -w and generates a name; quoted, -w '#feat-a' is rejected as an invalid worktree name. If a launch lands in a generated tree you did not ask for, look for a # on the name you typed.
A / in the name goes into the branch and becomes - everywhere else: -w feat/login puts branch feat/login in a tree called feat-login, on channel #feat-login.
rimz worktree new <name> creates an empty tree, launching nothing into it, for work of your own or a tree you will fill with agents later. rimz worktree is the whole verb surface: new, list (ls), cd, path, merge, remove (rm), and sweep. Use new --json when a script needs the path and full base commit rather than a human receipt.
To work in the tree yourself, run rimz worktree cd <name>. It opens a new shell and prints where you entered and where exiting will return you. It requires a terminal; rimz worktree path <name> prints just the absolute path for scripts or your own shell's cd.
Already made the tree with git worktree add? If it is in the configured worktree directory and belongs to this repository, rimz agents <spec> -w <name> and rimz teams <team> -w <name> ask whether to enter it. Answer y to launch there; Enter or n prints Launch aborted; nothing changed. RimZ keeps your files and branch as they are, does not mark or seed the tree, and leaves cleanup to you. Confirmation needs a terminal; an unattended launch refuses instead of guessing.
Start from a pull request
--from-pr <number|url> fetches a pull request's head over your origin credentials and lands the layout in its worktree, named pr-<N> when creating one by default. Reach for it to address review comments or chase a failing check: the agent works the PR's own branch, and on a forge RimZ can ask, its fixes push straight back to the PR.
If you started the same-repository PR's branch in a -w tree, --from-pr returns to that marked tree and records the PR number for later resume; when the tips differ, the PR checkout rules determine whether RimZ asks you to choose or fast-forwards an unattended launch. Teams and multi-agent layouts first reconcile the existing cohort, so a running team is reported without moving its branch or launching a duplicate.
rimz agents claude --from-pr 42 # review PR 42 in its own tree
rimz agents codex --from-pr 42 -w review-42 # name the tree yourself
rimz agents resume --from-pr 42 # come back to that PR's tree laterThe fetch works on any host. RimZ knows each one's PR ref, refs/pull/<N>/head on GitHub, Gitea, and Forgejo and refs/merge-requests/<N>/head on GitLab, and a PR URL must name the same host and repository as your origin.
Whether the tree can push back is the part that varies, because it depends on an authenticated forge CLI that can say who owns the PR's head branch. RimZ asks gh when origin is on github.com, and tea when the origin hostname contains gitea, forgejo, or codeberg. The CLI reports whether the head branch lives in the origin repository or in a fork, and RimZ configures pushes to whichever it is, so your fixes go straight back to the PR.
Everywhere else you get a review-only checkout: the exact PR head on a local branch, with no push destination configured, and a review : line on the create report saying why. That covers GitLab, a self-hosted forge whose hostname carries none of those names, and a host whose CLI is simply not installed. A CLI that is installed but cannot answer, usually because you are not logged in, refuses the checkout instead of quietly downgrading it; log in, or pass --branch <name> to ask for review-only on purpose. The full table of checkout shapes and refusals is in pull request checkouts.
Seed the tree
A tracked checkout alone rarely runs. git worktree add checks out tracked files at the base ref and nothing else, so the .env an agent needs and the fixture cache its tests read never follow it. Two committed, optional files at the repository root tell RimZ what else every new tree carries:
.worktreeincludelists globs for files to copy in:.env, local config, credentials the tests need.*stays inside a path component and**crosses directories, so.env*matches the root only and**/*.keyrecurses..worktreelinklists directories to symlink-share rather than copy: heavy machine-local data whose contents are intentionally branch-independent, such as downloaded model or fixture caches. Sharing them keeps a new tree cheap instead of duplicating gigabytes. Leave build output directories out of it. A shared one is written by every tree at once, so branches overwrite each other's artifacts; share the compiler's work through a content-addressed cache instead, the way RimZ's own contributors usesccacheand keeptarget/local (contributor cache setup). To put each tree's owntarget/on another disk, prepare it with a hook.
Both files take one entry per line and ignore blank lines and # comments. Because both are committed, every teammate's worktrees seed the same way, and every create reports what it brought in:
$ rimz worktree new feat-a
created feat-a
path : ~/code/query-engine-worktrees/feat-a
branch : feat-a, from main at c175274
seeded : 2 file(s) from .worktreeinclude
linked : 1 dir(s) from .worktreelink
next : rimz agents <spec> -w feat-a or rimz worktree cd feat-aCover every .worktreeinclude path in .gitignore. A copied file that Git can see makes the tree permanently dirty, and a dirty tree is one RimZ refuses to reclaim. Linked directories need no such care: RimZ writes each one into the new tree's .git/info/exclude as it links it.
Neither file runs a command. Sources are confined to the project root, absolute patterns and patterns reaching out with .. are skipped, and a pattern that matches nothing prints a warning and is skipped while the launch continues. The exact copy, symlink, and safety rules are in the worktree internals.
Prepare the tree with a hook
Copying files and sharing directories covers static setup. If your build wrapper instead needs a separate artifact directory on another disk for each tree, it needs a command: create that directory and link target/ to it before any build starts. A worktree hook runs your setup script once for each new tree, whether you create it directly, launch agents or a team into it, or check out a pull request.
Put the commands in your per-machine ~/.rimz/config.toml:
[agents.worktree.hooks]
created = "~/bin/rimz-init-target"
removed = "~/bin/rimz-drop-target"Both commands are optional; neither runs by default. The script paths above are yours to supply. For an artifact directory, have the setup script choose a destination unique to the repository and worktree, then create and link it. Ignore target/ in Git so the hook does not leave every tree dirty. The removal script undoes that setup using the same destination.
If the machine's config.toml cannot be read or parsed, rimz worktree new and launches using -w or --from-pr refuse rather than silently skipping setup. Correct the reported file before retrying; rimz config get shows the strict config error. Listing and removing trees remain available with defaults and a warning.
The lifecycle runs in this order:
- RimZ creates and marks the tree, then finishes the copies and links from the seed files.
- It runs
createdassh -c <command>with the new tree as its working directory, and waits before opening any launch panes. The command gets no stdin; stdout and stderr are captured, not streamed. A ten-minute timeout kills the hook's process group. A background process must detach stdout and stderr (for example,command >/dev/null 2>&1 &); otherwise the hook counts as running until those streams close, and reaching the timeout fails creation and rolls it back. - A successful hook lets creation continue.
rimz worktree newaddshook : worktree.created ranto its report; launches print nothing extra. Successful hook output is discarded. - A nonzero exit, failure to start, or timeout fails creation and attempts to remove the tree and its branch; a local branch that existed before the tree, such as an adopted pull request branch, is kept. The error includes bounded tails of stdout and stderr and the config key to fix. If removal also fails, the error says
rollback failed: <error>instead oftree removed; remove the leftover tree before retrying, since a named launch reuses it without rerunning the hook. Interrupting RimZ with SIGINT or SIGTERM whilecreatedruns can also leave a marked tree; remove it before retrying for the same reason. - Whenever RimZ successfully removes a tree, it runs
removedfrom the repository root, after the directory is gone and before deleting any branch. That includes rollback after a failedcreated, explicit removal, sweep, garbage collection, and automatic cleanup. It uses the same shell, capture, and timeout rules. Failure only logs a warning and removal continues; detached cleanup may have no visible output. A dry-run sweep fires nothing.
A successful setup command adds the hook line:
$ rimz worktree new feat-a
created feat-a
path : ~/code/query-engine-worktrees/feat-a
branch : feat-a, from main at 1946c74
hook : worktree.created ran
next : rimz agents <spec> -w feat-a or rimz worktree cd feat-aA setup command that prints init-out to stdout, prints init-err to stderr, and exits with status 3 reports:
$ rimz worktree new feat-b
error: worktree.created hook failed: exit status: 3
stdout:
init-out
stderr:
init-err
tree removed; fix agents.worktree.hooks.createdThe same names are also loop signals, separate from hook execution.
Both scripts receive these environment variables, overriding any inherited values with the tree being created or removed:
| Variable | Value |
|---|---|
RIMZ_HOOK_EVENT | worktree.created or worktree.removed. |
RIMZ_WORKTREE_NAME | The dashed tree and channel name, such as feat-login. |
RIMZ_WORKTREE_PATH | The tree's path; for removed, the directory that just went away. |
RIMZ_WORKTREE_BRANCH | The tree's branch, such as feat/login. |
RIMZ_WORKTREE_REPO_ROOT | The repository root the tree was created from. |
Reusing or resuming an existing tree never runs created. Trees made before you configured the hook have not run it either: initialize them yourself or remove and recreate them. These commands are machine-local, not part of either seed file or project config (worktree hook security).
What -w does on your machine
-w feat-a runs five steps, each plain Git, a file operation, or your configured shell command:
- Add the tree.
git worktree add --no-track -b feat-a ../<repo>-worktrees/feat-a <base>. The new branch does not track its base. The directory template and the base ref are the two knobs below. - Mark it. Write
rimz-worktree.jsoninto the tree's Git admin directory, at.git/worktrees/feat-a/rimz-worktree.jsonunder the main repository, recording the name, branch, and the base branch and commit that cleanup later measures against. That file is the whole of RimZ's claim on the tree: the checkout itself stays free of RimZ metadata, and every verb that can delete something reads the marker first and does nothing without it. - Seed it. Copy the
.worktreeincludematches, symlink the.worktreelinkdirectories, and register each link in the tree's.git/info/exclude. - Prepare it. Run the optional
createdhook and wait for success. - Open the layout. Every pane starts with its working directory in the tree, on a channel named after it.
Two per-machine keys under [agents.worktree] in config.toml tune the first step:
rimz config set agents.worktree.dir "../{repo}-worktrees" # where sibling trees land
rimz config set agents.worktree.base fresh # branch from origin/HEAD, not local HEADUnset, base gives a team the remote default branch fetched at launch; non-team launches and rimz worktree new keep local HEAD. Set it to head, fresh, or any Git ref to choose explicitly. Both keys read the main repository even when you launch from one of its linked worktrees, so head means the branch checked out in your main clone, not the one under your cursor, and dir puts the new tree beside that clone. rimz worktree new --base <ref> --branch <name> overrides the base and the branch name for one tree; the directory always comes from dir. Both fields are written up in configuration → worktrees.
Team launches fetch trunk before branching, warning and falling back if the fetch fails. For an explicitly selected local base, the stale-base warning tells you when to fast-forward that base or choose fresh (--base origin/main for a single worktree new).
If the repository you are standing in differs from the active room's, RimZ prints both paths and asks before creating anything, and warns that this room will not list, remove, or garbage-collect a tree made under the other root. A non-interactive caller states the choice with --root <git-root> instead of answering.
Cleanup, once work lands
RimZ reclaims a tree only after proving its work landed, so a merged feature cleans itself up and unmerged work is never lost. "Landed" is measured against the base branch the marker recorded, falling back to the trunk once that base has itself merged away, and it recognizes merge, squash, and rebase alike.
What happens when the last agent in a tree exits depends on how it exits, and the three cases differ enough to be worth knowing:
- The pane closes, or you kill it. RimZ checks the tree. A clean one whose work has landed goes, branch and all, and says so on stderr:
rimz: removed clean worktree <path>. A dirty or unproven one raises aChoose (keep/remove/shell) [keep]:prompt if you are there to answer it, and is kept if you are not. - An unattended run finishes. Same check, same outcomes. This is the path a scripted
-prun or a scheduled loop turn takes, and it is why a fleet that works while you sleep does not leave trees behind. - You quit the agent normally. Nothing is reclaimed. The pane prints
rimz: worktree <path> kept; `rimz worktree sweep` reclaims it once its work lands, then drops to a shell inside the tree so you can commit, push, or look around.
rimz worktree list is the status read, and bare rimz worktree does the same. STATE says landed, unlanded, or unknown, appending · dirty for uncommitted changes. The summary counts what is removable and what is occupied. Agent handles are the live holders that sweeping uses, not ended sessions; a pane with no agent reads pane.
$ rimz worktree list
2 worktrees in ~/code/query-engine-worktrees · 1 removable (rimz worktree sweep) · 1 in use
WORKTREE BRANCH STATE AGENTS
auth auth unlanded · dirty @coder#auth
feat-a feat-a landed -When you are ready to land local work, rimz worktree merge auth names its destination and asks before moving anything. For a local base, the destination is the branch that list measures against, including master or a stacked feature branch, falling back to the trunk once the base is superseded. With no agent or pane left in the worktree and both checkouts clean, RimZ rebases the work onto that branch and advances it by fast-forward in whichever checkout holds it. A conflict aborts the rebase and leaves the destination unchanged rather than creating a merge commit. The destination must be checked out; a snapshot commit is not a branch you can land on. Scripts use --yes; without a terminal and without that flag, RimZ refuses.
For a remote-tracking base such as the origin/main recorded by --base fresh, RimZ lands on the local branch that tracks it, or the trunk's local branch if none tracks it. With neither, it refuses. The prompt and receipt name that local branch, for example main. list keeps measuring against origin/main, so the tree still reads unlanded after a local landing until you push the destination branch.
$ rimz worktree merge auth --yes
landed auth on main
branch : auth
head : f0c10e649367adc7a6a82bbbe07549e5ea1f805b
the worktree stays at ~/code/query-engine-worktrees/auth; `rimz worktree remove auth` reclaims itMerging leaves the tree in place. rimz worktree sweep is the routine that takes it away: it removes every marked tree that is clean, proven landed, and unoccupied, reports what it reclaimed and why it kept the rest, and previews with --dry-run. rimz gc runs the same sweep alongside the other maintenance areas. A sweep never forces, so a tree you are still working in survives it.
$ rimz worktree sweep
sweep — removed 1 · 203 B · 1 kept
kept, uncommitted changes (1)
feat-a
removed (1) from ~/code/query-engine-worktrees
authrimz worktree remove <name> reclaims one tree on demand, and refuses twice before it does:
$ rimz worktree remove auth
error: worktree `auth` is in use by @coder#auth; use --force to remove it
$ rimz worktree remove feat-a
removed feat-aThe first refusal is for an agent or an open pane still working in the tree, naming what holds it. A stale record left by a crashed agent does not count, so a crash never leaves you unable to reclaim the tree. Once the tree is empty, uncommitted changes refuse with the file count and a commit-or-stash fix. Unlanded work refuses with the commit count, comparison ref, and rimz worktree merge NAME hint; unknown landing asks you to land the work yourself. Sweeping shows every keep reason, grouped under the first; use sweep --json or list --json for reason ids and full holders.
Removal deletes the branch with the tree when Git considers it merged or RimZ proves its work landed. Otherwise the branch stays and the report adds branch kept: its work is not proven landed on TARGET, so the commits are still reachable by name. --force skips the refusals and deletes the branch regardless, printing its full tip and a git branch BRANCH OID restore command when the tip could be read; when a live agent is in the tree it warns first, then removes anyway.
rimz worktree cd auth opens your user shell rooted in that checkout, for the times you want to look before you decide. Exit the shell to return to the directory where you started.
The full landed-content proof is in the worktree internals, and every refusal each verb can print is in the worktree CLI reference.
See also
- Agents: launch agents by name and compose the layout that lands in the worktree.
- Teams: a named team is the common unit to isolate on its own branch.
- Messaging: a worktree's name is its channel; reach agents there by handle.
- Worktree CLI reference: the complete
rimz worktreeandrimz gcsurface. - Worktree internals: the ownership marker, file seeding, and the landed-content proof.
Web
A room is a plain Zellij or tmux session, so ssh dev and tmux attach reach it from any machine that has your key and a terminal you trust. That covers most days.
Messaging
To hand work to an agent that is already running, you switch to its pane and type, or you script the same keystrokes: tmux send-keys -t %3 'rebase on main first' Enter.