[BUG] Linked worktree loads a project-scope plugin from the first same-repository row in installed_plugins.json, not the worktree's own row
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
A session in a linked git worktree loads a project-scope plugin from the first installed_plugins.json row of any worktree of the same repository. The worktree's own row does not take precedence. The loaded version is whatever the earliest-registered sibling worktree had at its last install, and nothing reports the selection.
I read the resolution rule from the 2.1.263 binary. Two functions decide it.
The applicability filter accepts a row when its scope is user or managed, when its projectPath equals the original cwd, or when the canonical git root of its projectPath equals the canonical git root of the cwd. The canonical root follows .git -> gitdir -> commondir. When the common directory is named .git, the canonical root is its parent, the main checkout. Otherwise the canonical root is the common directory itself.
The picker filters the plugin's rows with that function, walks the survivors in array order, and returns the first whose installPath exists on disk.
Three consequences follow.
- Exact match loses. A row whose
projectPathequals the cwd wins only when no earlier same-repository row exists. - Project-scope install from a worktree has no visible effect.
claude plugin install <plugin> --scope projectappends a row for the worktree at the end of the array. The earlier row keeps winning, and the command prints no warning. - A bare-repository worktree pool has no main checkout. With
repo.gitplusgit worktree addcheckouts, the canonical root is the bare directory, so every worktree is a sibling. The winner is the worktree registered first.
In my repository, 15 rows of one plugin pointed at worktrees of one bare pool. Row 1, registered 2026-07-24 at version 0.85.0, won from every worktree. The row for the worktree I worked in said 0.92.6. The marketplace had 0.92.8. Eight claude plugin install --scope project runs from six different worktrees between 2026-08-17 and 2026-09-05 each added or updated their own row and changed nothing a session loaded.
This is the same shape as #45997, which reported the loader taking array index 0. The exact-cwd filter that report proposed is now in the binary. The worktree extension widened the filter to every same-repository row, and the pick among them is array order again. #77546 reports the same symptom, one row apparently current yet an older cache loaded, and the maintainer could not reproduce it with rows for one project path. Rows for two worktrees of one repository reproduce it every time.
What Should Happen?
- A row whose
projectPathequals the cwd takes precedence over same-repository rows. - Among same-repository rows the winner is deterministic and documented, for example the row with the newest
lastUpdated, or the main worktree's row for a non-bare repository. claude plugin install --scope projectfrom a worktree updates the row a session in that worktree loads, or warns that an earlier row for the same repository shadows the new one.claude plugin listmarks the row that applies to the cwd. Today it prints every row of a plugin with no indication of which one loads.
Error Messages/Logs
No error. The skill invocation exposes the loaded version through its base directory:
Base directory for this skill: ~/.claude/plugins/cache/outcomeeng/spec-tree/0.85.0/skills/pickup
claude plugin list from the affected worktree, filtered to the plugin, before cleanup:
❯ spec-tree@outcomeeng
Version: 0.92.2
Scope: project
Status: ✔ enabled
❯ spec-tree@outcomeeng
Version: 0.85.0
Scope: project
Status: ✔ enabled
❯ spec-tree@outcomeeng
Version: 0.85.0
Scope: project
Status: ✔ enabled
...
Steps to Reproduce
The plugin needs a per-version marker in a skill so the loaded version is visible. Version X is older than version Y.
- Create a bare repository with two worktrees:
````
git clone --bare <url> pool.git
git -C pool.git worktree add ../wt-a main
git -C pool.git worktree add ../wt-b main
- In
wt-a, runclaude plugin install <plugin>@<marketplace> --scope projectwhile the marketplace serves version X. - Publish version Y and run
claude plugin marketplace update <marketplace>. - In
wt-b, runclaude plugin install <plugin>@<marketplace> --scope project. - Open
~/.claude/plugins/installed_plugins.json. The plugin has two rows:wt-aat X first,wt-bat Y second. - Start a session in
wt-band invoke a skill of the plugin. The base directory is the X cache. - Delete the
wt-arow by hand and run/reload-plugins. Invoke the skill again. The base directory is now the Y cache.
The same steps with an ordinary clone and git worktree add ../wt-b from it reproduce the bug when wt-a is the main checkout: a later install in wt-b never loads.
Claude Model
Other
Is this a regression?
I don't know
Last Working Version
No response
Claude Code Version
2.1.263 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
claude doctor:
Claude Code doctor
Running: native (2.1.263)
Commit: 37ae3f38d765
Platform: darwin-arm64
Path: ~/.local/share/claude/versions/2.1.263
Config install method: native
Search: OK (bundled)
Auto-updates: enabled
Auto-update channel: latest
No installation issues found.
Terminal: Ghostty (TERM=xterm-ghostty), zsh.
Cache directory of the plugin while the wrong version loaded, ~/.claude/plugins/cache/outcomeeng/spec-tree/:
0.85.0 0.85.2 0.86.0 0.86.1 0.87.1 0.88.0 0.88.10 0.88.3 0.88.7 0.88.8 0.88.9 0.89.0 0.89.1 0.90.0 0.90.1 0.91.0 0.92.1 0.92.2 0.92.3 0.92.4 0.92.5 0.92.6 0.92.7
The two rows that decided the outcome, from installed_plugins.json before cleanup. Row 1 won; row 47 was the cwd's own row:
{
"scope": "project",
"projectPath": "~/Code/outcomeeng/plugins/plugins",
"installPath": "~/.claude/plugins/cache/outcomeeng/spec-tree/0.85.0",
"version": "0.85.0",
"installedAt": "2026-07-24T21:31:46.862Z",
"lastUpdated": "2026-07-24T21:31:46.862Z",
"gitCommitSha": "e9d8b8a3d8090d7f23ffdfe2413ff2ce42bb4c62"
}
{
"scope": "project",
"projectPath": "~/Code/outcomeeng/plugins/worktrees/go-plugin",
"installPath": "~/.claude/plugins/cache/outcomeeng/spec-tree/0.92.6",
"version": "0.92.6",
"installedAt": "2026-09-05T11:47:38.402Z",
"lastUpdated": "2026-09-05T11:47:38.402Z",
"gitCommitSha": "07a71f88aab9d1ac73b884bb4fca590c28a1d43d"
}
Worktree layout: ~/Code/outcomeeng/plugins/plugins.git is bare. Every checkout's .git file reads gitdir: ~/Code/outcomeeng/plugins/plugins.git/worktrees/<name>, and each commondir reads ../.., so the canonical root of every checkout is the bare directory.
Workaround: run the project-scope install from the checkout whose row comes first in the array, or delete the earlier same-repository rows by hand, then /reload-plugins.
The worktrees documentation says plugins installed at project scope from the main checkout also load in worktrees of the same repository, from v2.1.200. The implemented rule is broader: any registered worktree of the same repository, first in file order. For an ordinary repository that is the main checkout only when the main checkout was registered first.