Skip to content

EnterWorktree is unsafe in co-located jj repos: jj inside the worktree drives the main workspace #134

Description

@aguil

Summary

Claude Code's EnterWorktree tool creates a git worktree at .claude/worktrees/<name>, inside the repository working tree. jj resolves its workspace by walking up from the current directory, so from inside that worktree it finds the main repo's .jj/ and operates on the default workspace. An agent that believes it is isolated is in fact driving the operator's working copy.

AGENTS.md lines 18-20 tell agents to prefer jj in a co-located repo. That rule is correct, and it is exactly what makes this collision likely: the agent follows the jj instruction inside a directory where jj does not mean what it appears to mean.

Root cause

Two tools disagree about what directory they are in, and only one of them is right.

EnterWorktree's own documentation states the location: "creates a new git worktree inside .claude/worktrees/". Git treats that directory as a separate checkout with its own HEAD. jj has no knowledge of git worktrees, finds .jj/ by upward search, and answers every question about the parent repo.

Evidence

Reproduced in a disposable repo, not inferred from the code.

git init -q -b main . && echo hello > a.md && git add a.md && git commit -qm init
jj git init --colocate
git worktree add .claude/worktrees/test -b testbranch

From inside .claude/worktrees/test, the two tools report different repositories:

$ git rev-parse --show-toplevel
.../wt-repro/.claude/worktrees/test
$ git branch --show-current
testbranch

$ jj workspace root
.../wt-repro                     # the MAIN repo, not the worktree

Running a single jj command from inside the worktree moves the parent's working copy:

main workspace @ BEFORE:   lkslwqpw

$ cd .claude/worktrees/test && jj new -m "isolated-work"
Working copy  (@) now at: utsrsqvp 3c575574 (empty) isolated-work

main workspace @ AFTER:    utsrsqvp  isolated-work
$ jj workspace list
default: utsrsqvp 3c575574 (empty) isolated-work

One workspace exists. The "isolated" commit landed on it. Any of jj new, jj describe, jj commit, jj bookmark set, jj abandon has the same reach.

Secondary effect: git worktree add -b testbranch creates a git branch that jj imports on its next command, so testbranch appears as a bookmark on the main repo's commit and pollutes the bookmark namespace.

What is not a problem

Recording this so nobody re-investigates it. jj does not snapshot the nested worktree's files into the main working copy. jj st stays clean both after git worktree add and after creating a new file inside the worktree. The hazard is command routing, not tree duplication. I assumed the opposite before testing and was wrong.

How it surfaced

A background agent working a merge hand-off on aguil/notes PR #2. Its fork reminder instructed it to isolate with EnterWorktree. It recognised the co-location hazard, used jj workspace add in a sibling directory instead, and the parent session's @ survived intact. It got that right by reasoning, not because anything told it to. The next agent may not.

Proposed fix

1. Name the tool in AGENTS.md. The jj co-location paragraph currently says to prefer jj and avoid raw git that rewrites state. Extend it:

Do not use EnterWorktree in a co-located repo. It creates a git worktree inside .claude/worktrees/, where jj still resolves to the main workspace, so "isolated" jj commands move the operator's working copy. Isolate with jj workspace add ../<name> in a sibling directory, and jj workspace forget <name> plus removing the directory when finished.

2. Consider a rules module. dot_agents/rules/worktree-isolation.md, following the live-interaction-safety.md pattern already referenced from AGENTS.md, if the guidance grows past a paragraph.

3. Investigate a hook, which would be the stronger fix. Instruction-level guidance depends on the agent reading and obeying it. EnterWorktree's documented requirement is "Must be in a git repository, OR have WorktreeCreate/WorktreeRemove hooks configured in settings.json", and it says it delegates to those hooks "outside a git repository". Whether a configured hook takes precedence inside a git repo is not documented either way, and I have not tested it. If it does, a WorktreeCreate hook that shells out to jj workspace add when .jj/ is present fixes this mechanically rather than by asking nicely. Worth one experiment before writing prose. No worktree hooks are configured today; settings.json carries only a Stop hook.

Acceptance criteria

  • AGENTS.md names EnterWorktree as unsafe in a co-located repo and gives jj workspace add as the replacement, including the cleanup step.
  • Guidance states the sibling-directory requirement, since a workspace created inside the repo has the same defect.
  • The hook question is settled by test, and either implemented or recorded as not viable with the reason.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions