[BUG] Linked worktree loads a project-scope plugin from the first same-repository row in installed_plugins.json, not the worktree's own row

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

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 projectPath equals 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 project appends 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.git plus git worktree add checkouts, 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 projectPath equals 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 project from 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 list marks 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.

  1. 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
``

  1. In wt-a, run claude plugin install <plugin>@<marketplace> --scope project while the marketplace serves version X.
  2. Publish version Y and run claude plugin marketplace update <marketplace>.
  3. In wt-b, run claude plugin install <plugin>@<marketplace> --scope project.
  4. Open ~/.claude/plugins/installed_plugins.json. The plugin has two rows: wt-a at X first, wt-b at Y second.
  5. Start a session in wt-b and invoke a skill of the plugin. The base directory is the X cache.
  6. Delete the wt-a row 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.

View original on GitHub ↗