A Claude Code skill that isolates parallel subagent work in Jujutsu (jj) repositories using jj workspaces — the jj-native alternative to git worktree.
git worktree doesn't work inside jj-managed repos (secondary worktrees have no .jj/, so jj commands fail). jj workspace is the correct primitive, but the ergonomics around multi-agent workflows (safe placement, base revision checks, sealed-commit conventions, safe teardown) require enough boilerplate that a skill + helper scripts make it worth packaging.
jj≥ 0.23 installed and onPATH- A jj-managed repository (
.jj/directory present, orjj rootsucceeds)
Install directly from GitHub (recommended):
# Project-local (skill available only in this repo)
git clone --depth=1 https://github.com/pkrusche/jj-parallel-agents-skill .claude/skills/jj-workspace-isolation
# Global (skill available in every repo)
git clone --depth=1 https://github.com/pkrusche/jj-parallel-agents-skill ~/.claude/skills/jj-workspace-isolationOr copy from a local clone:
# Project-local
install -D SKILL.md .claude/skills/jj-workspace-isolation/SKILL.md && install -Dt .claude/skills/jj-workspace-isolation/scripts/ scripts/*.sh
# Global
install -D SKILL.md ~/.claude/skills/jj-workspace-isolation/SKILL.md && install -Dt ~/.claude/skills/jj-workspace-isolation/scripts/ scripts/*.shClaude Code loads SKILL.md automatically; no further configuration is needed.
Some tooling uses .agents/ as the config directory instead of .claude/. Create a symlink so both names resolve to the same place:
ln -s .claude .agentsAdd .agents to your .gitignore (the symlink itself should not be committed — it's a local convenience):
echo '.agents' >> .gitignoreWhen you git clone into a directory that is already tracked by jj, jj will not snapshot .claude/ (or .agents/) provided those paths are listed in your .gitignore. Add them before cloning so jj never picks them up:
echo '/.claude/' >> .gitignore
echo '/.agents/' >> .gitignore # if using the symlink
jj describe -m "ignore agent config dirs" && jj new
git clone --depth=1 https://github.com/pkrusche/jj-parallel-agents-skill .claude/skills/jj-workspace-isolationIf you cloned first and .claude/ is already tracked, untrack it:
jj file untrack .claude/
echo '/.claude/' >> .gitignoreWhen invoked — either explicitly or when Claude detects a jj repo and a parallel-agent task — it:
- Creates one isolated
jjworkspace per subagent viascripts/spawn-workspace.sh. - Instructs each subagent to work only inside its workspace and to seal its result with
jj describe -m "..." && jj new. - Integrates finished work back onto the main branch using
jj new/jj rebase. - Cleans up workspaces via
scripts/cleanup-workspaces.sh, refusing to discard unsealed work without-f.
Both scripts must be run from the main (default) workspace.
Usage: spawn-workspace.sh [-r REVSET] [-d WS_PARENT_DIR] NAME...
Creates one jj workspace per NAME under <repo-root>/../<repo-name>-ws/ (sibling of the repo, so workspaces are never snapshotted). Prints a machine-readable WORKSPACE name=... path=... base=... line per workspace on stdout.
# Create two workspaces based on main
scripts/spawn-workspace.sh -r main agent-auth agent-searchUsage: cleanup-workspaces.sh [-f] [-d WS_PARENT_DIR] NAME...
cleanup-workspaces.sh [-f] [-d WS_PARENT_DIR] -p PREFIX
Forgets each workspace and removes its directory. Refuses to remove workspaces with unsealed work unless -f is passed. Never removes the default workspace.
scripts/cleanup-workspaces.sh agent-auth agent-search
scripts/cleanup-workspaces.sh -p agent- # all workspaces with this prefix- Sibling placement — workspaces live at
<repo-root>/../<repo-name>-ws/<name>by default, keeping them outside the working copy and away from jj's snapshot. - Explicit base revision — always pass
-r <revset>in local-only repos; without a remotemain/master/trunkbookmark,trunk()falls back toroot()and the scripts reject this. - Sealed commits — agents must end with
jj describe -m "..." && jj newso their result is a stable, addressable commit (<name>@-). - No
gitin secondary workspaces — secondary workspaces have only.jj/, no.git/; git-dependent tooling won't work inside them.
The evals/ directory contains a harness that runs the skill against a live Claude agent and checks the result deterministically — no LLM judge.
| Case | Trigger | Gated |
|---|---|---|
parallel-todo-merge |
Implicit — skill must auto-invoke from context | No (monitoring) |
parallel-todo-merge-explicit |
Explicit /jj-workspace-isolation in prompt |
Yes (CI gate) |
Each case seeds a jj repo with a TODO.md listing three parallel file-creation tasks, then runs the agent and verifies:
- Skill was discovered at session start (
system/initslash_commands) spawn-workspace.shorjj workspace addwas called (from a hook-based command log)git worktreewas never called (hard-denied at the hook level, exits 2)- All three result files exist in the working copy
- Agent workspaces were cleaned up (only
defaultremains injj workspace list) - Working copy is clean (
@ & empty())
# Requires an authenticated claude CLI (ANTHROPIC_API_KEY or OAuth session)
bash evals/run_e2e.sh --trials 1 --case parallel-todo-merge-explicit
# Full 3-trial run of both cases
bash evals/run_e2e.sh
# Options
bash evals/run_e2e.sh --agent claude --trials 3 --case CASE_ID --report out.jsonExits 0 if all gated cases have at least one passing trial; exits 1 otherwise. Run artifacts (transcripts, grading results, cost per trial) are written to evals/.work/<timestamp>/ and gitignored.
evals/
cases.json — test case definitions (prompts, assertions, gate flag)
fixtures/todo-repo/ — seed content copied into a fresh jj repo per trial
hooks/settings.json — PreToolUse hook: bash command logger + git-worktree deny
runners/
common.sh — shared setup (jj repo init, skill install)
run_claude.sh — drives claude -p with stream-json transcript capture
verify.py — deterministic grader (no external dependencies)
run_e2e.sh — top-level runner: case × trial loop, pass@k/pass^k report
MIT