# One History, Many Agents: Using jj workspaces for parallel coding agents

> Source: <https://minikin.me/blog/jj-workspaces-for-parallel-agents/>
> Published: 2026-10-22 09:25:05+00:00

# One History, Many Agents

With five coding agents, jj workspaces let me check on each one, rebase them all onto main and recover what one destroyed, without stopping any of them.

This is `jj log` in the main checkout of a service while three coding agents work on it, each in its own directory:

`fix-retry@` is one agent’s working copy. It has no description because the agent has not committed anything, and it is still a commit in my history. From my own directory, without interrupting the agent, I can see what it has changed so far:

I run several Claude Code agents on one repo at once, each in its own terminal split. They need separate directories, or two of them edit the same file, and git has handed out directories since 2015 with `git worktree add`. With one agent, a worktree is all I need. With five, most of my time goes into four things that a worktree does not help with: checking on agents while they work, keeping them on the latest `main` as their work lands, recovering what an agent broke, and handing one agent’s work to the next. Every one of them goes through the agent’s commits and its branch, and agents commit when their instructions say so, not when I need them to. [jj](https://docs.jj-vcs.dev/latest/) [workspaces](https://docs.jj-vcs.dev/latest/working-copy/#workspaces) turn each agent’s files into a commit in one shared history, and all four become commands I run from my own checkout while the agents keep working. Every command and output here was run on jj 0.46 and git 2.56.

## Four moments in a day with parallel agents[#](#four-moments-in-a-day-with-parallel-agents)

| Moment | With git worktrees | With jj workspaces | 
|---|---|---|
| I want to see how an agent is doing | `cd` into its worktree, or ask it to commit | `jj diff -r fix-retry@` from my checkout | 
| One agent’s change lands, the rest fall behind | `git rebase` inside each worktree, stopping at the first conflict | one `jj rebase` for all of them, conflicts left to each agent | 
| An agent throws away its own edits | uncommitted files are gone | every snapshot is in `jj evolog` ,`jj restore` brings it back | 
| A second agent picks up a finished change | `git worktree add --detach` gets the last commit only | `jj workspace add -r fix-retry@` includes every recorded edit | 

## A workspace is still a worktree[#](#a-workspace-is-still-a-worktree)

jj in a [colocated repo](https://docs.jj-vcs.dev/latest/git-compatibility/#colocated-jujutsugit-workspaces), one where `.jj/` sits next to `.git/`, registers each new workspace as a git worktree:

Inside it, `git status`, the IDE, `gh` and pre-commit hooks behave as in any worktree, so moving to workspaces costs no git tooling. What changes is where the agents’ work lives.

## Checking on an agent without stopping it[#](#checking-on-an-agent-without-stopping-it)

An agent that took a wrong turn ten minutes in is cheap to correct. One that took it an hour ago has built on it. So I look at each agent’s work while it runs, not only when it says it is done. The `jj diff` at the top of this post is that look.

With worktrees, an agent’s edits do not exist outside its directory until it commits. `git log` in my checkout shows nothing, `git diff main fix-retry` shows only what was committed, and the rest I can only read by going into the agent’s worktree and running `git status` there. Asking the agent to commit interrupts its task and leaves a half-done commit on its branch.

jj has no staging area and no commit step between a file and the history. Before any jj command runs in a workspace, jj [records the directory’s files](https://docs.jj-vcs.dev/latest/working-copy/) into its working-copy change, `@`. An agent runs `jj st` or `jj diff` every few steps, so its edits are in the commit within seconds, and the log at the top of this post is current without the agent committing anything. An edit made after the agent’s last jj command is not there yet, and `jj st` in its directory closes that gap.

## When one agent lands, the others fall behind[#](#when-one-agent-lands-the-others-fall-behind)

Every change that reaches `main` leaves every other agent working on an old base, and the longer one goes without a rebase, the bigger its conflicts get.

With worktrees, the rebase cannot happen from my checkout, because a branch can be checked out in one worktree only:

So it happens inside each worktree, one at a time. A conflict stops the rebase halfway, and the worktree sits in the middle of it, with the agent’s task on hold, until someone resolves it.

With workspaces, the whole round runs from my checkout. `fix-retry` is done, so I move `main` to its change and push it:

The other two agents are now one change behind, and one command moves both:

The rebase finished even though `add-metrics` changed the same line of the README as `fix-retry`. jj [stores the conflict in the commit](https://docs.jj-vcs.dev/latest/conflicts/) instead of stopping, so `add-tracing` is on the new `main` with nothing to do, and `add-metrics` is on it too, with the conflict recorded in its two commits for its agent to resolve.

Each workspace whose change I rewrote from outside is now stale, and the next jj command in it says so:

`jj workspace update-stale` in that directory checks out the rebased change, conflict markers included. I do the whole round while the agents wait for input: `jj st` in each workspace first, so recent edits move with the commit, then the rebase, then `update-stale` in each, so no agent sees the error.

## An agent’s mistake stays its own[#](#an-agents-mistake-stays-its-own)

I do not watch every command an agent runs. Sooner or later one runs `git reset --hard` or `git checkout .` to get out of a mess. With worktrees, git’s reflog covers commits, and `git fsck --lost-found` finds files that were once `git add` ed. Files the agent never added are gone.

With jj, every snapshot of the agent’s working copy is still in the repo, and [`jj evolog`](https://docs.jj-vcs.dev/latest/cli-reference/#jj-evolog) lists them:

The 10:22 snapshot has the work, and restoring from it brings the files back:

Recovery covers what jj recorded, so edits made after the agent’s last jj command are lost the same way as in git.

Undo stays inside one workspace too. Every jj command is an operation in the [operation log](https://docs.jj-vcs.dev/latest/operation-log/), and `jj undo` reverts the last one. With several agents writing to one repo, the last operation is usually some other agent’s, and jj refuses that case:

An agent’s `jj undo` reverts its own mistake or does nothing. To take back one specific thing an agent did, `jj op revert <id>` reverts that operation alone.

There is no shared stash either. With worktrees, `git stash` is one list for the whole repo, so an agent that runs `git stash pop` can apply another agent’s edits to its own files. An agent in a jj workspace that wants its edits out of the way runs `jj new @-`: it continues on a fresh change from the same parent, and its edits stay in the old change, where only that change holds them.

## Handing work to the next agent[#](#handing-work-to-the-next-agent)

Agents work well in a chain: one writes the change, a second reviews it or writes the tests, a third fixes what the review found. The second agent needs the first one’s work as the first agent left it, including edits it never committed.

With worktrees, the first agent’s branch is locked to its directory. `git worktree add --detach ../review fix-retry` gives the reviewer the branch’s last commit, without the uncommitted edits.

With jj, the reviewer starts on top of the first agent’s working copy:

Its directory has every edit the first agent had made by its last jj command. This works best on a change the first agent has finished. If the first agent keeps working, each of its jj commands rewrites `fix-retry@`, jj rebases the reviewer’s change on top, and the reviewer’s workspace goes stale until it runs `jj workspace update-stale`.

## Rules for agents that learned git[#](#rules-for-agents-that-learned-git)

Models know git much better than jj, and out of habit an agent reaches for `git commit`, `git checkout` and `git stash`. In a colocated repo read-only git is harmless and the writes are not, so the global `CLAUDE.md` that every agent of mine loads says to use jj for every write, to keep `git status`, `git diff`, `git log` and `git blame` for reading, to pass `-m` to every jj command that takes a message and never open an editor, and never to push.

Rules in a prompt get skipped now and then, so the ones that matter are also enforced, the way I argued for in the [cargo-crap post](https://minikin.me/blog/cargo-crap/). `JJ_EDITOR=false` makes a missing `-m` fail at once instead of leaving the agent waiting on an editor it cannot see. A Claude Code `PreToolUse` hook blocks `git push` and `jj git push` in a workspace, and a `pre-push` git hook in each workspace catches a `git push` from a script the hook never sees.

## What it does not do[#](#what-it-does-not-do)

- It does not copy ignored files. A new workspace has no `target/` ,`.env` or`node_modules` , so the agent’s first`cargo build` or`cargo test` compiles every dependency from scratch, and tests that read`.env` fail until the file is copied in. Pointing every workspace at one`CARGO_TARGET_DIR` saves the rebuild, but then the agents wait on each other for cargo’s lock on that directory. Removing a workspace deletes all of these files.
- It does not record edits on its own. An agent’s latest edits reach the shared history, and become recoverable, at its next jj command.
- It does not keep a rewritten workspace current. A change rewritten from outside leaves its workspace stale until `jj workspace update-stale` runs there.
- It does not make a workspace a git worktree in an older jj, or in a repo that is not colocated. Git tools then see a directory with no repo in it.

## Try it[#](#try-it)

In an existing git repo:

Start an agent in the new directory and run `jj log` from your own. `jj diff -r fix-retry@` shows its work, and `jj workspace remove fix-retry` ends it when the change has landed. The agent’s changes stay in `jj log` after the directory is gone.

## How I use it[#](#how-i-use-it)

My [dotfiles](https://github.com/minikin/dotfiles) wrap these commands and fill the gaps above. `jwc fix-retry` creates the workspace in `<repo>-ws/` next to the repo, copies the untracked `.env*` files from the main checkout, runs the project’s `just setup` or `mise run setup` and starts Claude Code in it. `jsync` fetches once and takes the workspaces one at a time: `jj st` in the workspace, a rebase onto trunk, `update-stale` straight after.

From the main checkout, `jwst` prints one row per workspace: how far it is behind trunk, whether it was pushed and whether it has conflicts. It runs jj only against the main checkout, never inside an agent’s directory, so looking never snapshots a running agent’s files. The repo’s [parallel agents guide](https://github.com/minikin/dotfiles/blob/main/docs/guides/parallel-agents.md) has the full workflow.
