Worktree isolation guard covers git but not jj — jj commands in an isolated session silently read and write the main checkout
Summary
Worktree isolation enforces itself against git but not against jj (Jujutsu). In a colocated jj+git repository, an isolated session's jj commands are not blocked, and they silently operate on the main checkout instead of the worktree — reading state that isn't there and committing work that isn't theirs.
The isolation guard exists to stop exactly this. For jj it does not fire.
Why the worktree resolves to the parent repo
--worktree, EnterWorktree, isolation: "worktree", and background-session isolation all create a plain git worktree at .claude/worktrees/<name> inside the repository. That worktree has a .git file but no .jj/ of its own. jj resolves its repository by walking up the directory tree, finds the parent's .jj/, and operates on the parent workspace.
Reproduction
Claude Code 2.1.226, jj 0.43.0, git 2.54.0, Linux (WSL2).
mkdir repo && cd repo
git init -q . && git commit -q --allow-empty -m init
jj git init --colocate # colocated is the default for jj git init
jj describe -m "chore: base"
echo 'MAIN-CHECKOUT-WORK' > main_only.txt # uncommitted work in the MAIN checkout
claude --worktree isoprobe -p \
"Run these two commands and report their raw output: (1) jj --no-pager status (2) git status --short" \
--allowedTools Bash
Observed, from inside the isolated session:
(1) jj --no-pager status
Working copy changes:
A ../../../main_only.txt
Working copy (@) : xqysklpn f355b737 chore: base
Parent commit (@-): vmruzmnq 84a5f4b5 main worktree-isoprobe | init
(2) git status --short
(no output)
git correctly reports the worktree as clean. jj reports the main checkout's file — note the ../../../ path, which is jj describing a file outside the session's worktree. The command was not blocked.
Impact
Three consequences, all observed on jj 0.43.0:
- Reads are wrong. In a worktree with modified and untracked files,
jj statusprintsThe working copy has no changes. - Writes hit the main checkout. With an unrelated dirty edit in the main checkout,
jj describe -m "..."from the isolated session committed that edit under the session's message, andjj newadvanced the main checkout's working copy. The worktree's own edits were never touched. ExitWorktreebecomes a data-loss path.ExitWorktreecorrectly refuses to remove a worktree with uncommitted files unlessdiscard_changes: true. That check reads real git state and is right. But an agent that trusts jj's "no changes" over the tool's refusal will setdiscard_changes: trueto get past what looks like a spurious block — and the work is gone, with no jj operation-log entry to recover from, because jj never recorded it.
Point 3 is the reason this is a data-safety issue rather than a cosmetic one: the guard that would have saved the work is overridden on the strength of the unguarded tool's wrong answer.
Worth noting that --worktree and EnterWorktree are opt-in, but background-session isolation is not: a background session in a colocated jj repo creates .claude/worktrees/<name> on its own, so a user who never asked for a worktree lands in this state. I verified that in the same setup — the worktree appears at the session's first file edit.
Why the existing escape hatches don't cover it
WorktreeCreatehook: documented for non-git VCS, and it works — I verified a hook runningjj workspace addplaces a non-colocated jj repo's session in a real jj workspace outside the repo. But a colocated repo has a real.git/at the root, so Claude Code takes the git path and the hook never fires. Verified in both directions. Sincejj git initis colocated by default, this is the common case.- Repairing the worktree in place: not possible.
jj git init --colocate→Cannot create a colocated jj repo inside a Git worktree.jj workspace add <that path>→Destination path exists and is not an empty directory. worktree.bgIsolation: "none"does work, and is the one usable mitigation today — but only for background sessions, and it is not listed on the worktrees page (I found it in the changelog). A/B with that key as the only variable, same task and flags:
| | worktree created | file written to |
| --- | --- | --- |
| without the key | .claude/worktrees/marker-file | the worktree |
| bgIsolation: "none" | none | the main checkout |
It does not cover --worktree, EnterWorktree, or isolation: "worktree" subagents.
Suggested directions
Any one of these would close it; listed cheapest first.
- Extend the guard to jj. It already blocks
git -C,--git-dir,GIT_DIR/GIT_WORK_TREE, andcd-then-git. The jj equivalent is a single check: in an isolated session, blockjjwhenjj rootdoes not resolve to the worktree root. - Warn or refuse at creation time. When the repository has a
.jj/at its root, creating a git worktree inside it produces a directory in which jj is actively misleading. Saying so at creation is cheaper than detecting it afterwards. - Let
WorktreeCreatetake precedence when.jj/is present, so a colocated repo can opt intojj workspace addthe same way a non-colocated one already can.
Related
- #74775 — jj detection in environment metadata. Adjacent but distinct: that one is about the model choosing the right command; this one is about the enforcement layer not covering the command once chosen.