Worktree isolation guard covers git but not jj — jj commands in an isolated session silently read and write the main checkout

Status Open
Reported on v2.1.226
Maintainer reply None cached
Activity 0 comments · opened Aug 8, 2026

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:

  1. Reads are wrong. In a worktree with modified and untracked files, jj status prints The working copy has no changes.
  2. 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, and jj new advanced the main checkout's working copy. The worktree's own edits were never touched.
  3. ExitWorktree becomes a data-loss path. ExitWorktree correctly refuses to remove a worktree with uncommitted files unless discard_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 set discard_changes: true to 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

  • WorktreeCreate hook: documented for non-git VCS, and it works — I verified a hook running jj workspace add places 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. Since jj git init is colocated by default, this is the common case.
  • Repairing the worktree in place: not possible. jj git init --colocateCannot 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.

  1. Extend the guard to jj. It already blocks git -C, --git-dir, GIT_DIR/GIT_WORK_TREE, and cd-then-git. The jj equivalent is a single check: in an isolated session, block jj when jj root does not resolve to the worktree root.
  2. 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.
  3. Let WorktreeCreate take precedence when .jj/ is present, so a colocated repo can opt into jj workspace add the 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.

View original on GitHub ↗