Project-scoped Claude Code plugin skills load globally, leaking into unrelated sessions (plugin counterpart of #457)

Open
#1,126 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust
Domain
cli

Research direction

Start in crates/jcode-base/src/skill.rs, reading installed_plugin_paths and the load_global/load_project_overlay paths, then compare the session-scoped overlay introduced for #457. Verify the three described cases: the owning workspace and nested directories, an unrelated workspace, and the global pass. Done means local plugins load only for their project tree, user-scoped and legacy unscoped installs remain global, and symlinked workspaces compare correctly.

Written by the indexing model from the issue text.

Description

autonomous: no bug priority: medium triage: needs-decision triage: reproducible

Summary

Claude Code lets you install a plugin either for the user (global) or for a single project ("local" scope). jcode loads both kinds into the shared global skill registry, so a plugin installed for one repo contributes its skills to every session's prompt.

Concretely: on my machine figma and notion are installed for a single work repo, and their ~10 skill descriptions show up in the prompt of every session in every other project, where they can never be relevant.

This is the plugin-install counterpart of #457, which fixed the same leak for ./.agents/skills project-local directories. That fix introduced the session-scoped overlay; plugin installs never got routed through it.

Environment

  • macOS 26.4.1, jcode master (v0.81.4)
  • Claude Code plugin manifest at ~/.claude/plugins/installed_plugins.json

Repro

  1. In Claude Code, install a plugin with local (project) scope in repo A — e.g. figma.

  2. Confirm ~/.claude/plugins/installed_plugins.json records it with "scope": "local" and a projectPath pointing at repo A:

    "figma@claude-plugins-official": [
      {
        "scope": "local",
        "projectPath": "/Users/me/work/repo-a",
        "installPath": "/Users/me/.claude/plugins/cache/claude-plugins-official/figma/2.2.68"
      }
    ]
    
  3. Open a jcode session in an unrelated repo B.

  4. The figma-* skills are listed as available.

Expected: they appear only in sessions whose workspace is repo A (or a directory nested inside it).

Cause

crates/jcode-base/src/skill.rs, installed_plugin_paths reads only installPath and discards the rest of the install record:

for install in installs {
    if let Some(path) = install.get("installPath").and_then(|p| p.as_str()) {
        let path = PathBuf::from(path);
        if path.is_dir() {
            paths.push(path);
        }
    }
}

Every returned path is then scanned during load_global, so scope and projectPath never influence which registry a plugin's skills land in.

Impact

Mostly prompt pollution, and it scales with how many project-scoped plugins the user has installed across all their repos: every one of them is described in every session, in every project. It also blurs the guarantee #457 established, that project-local skills belong to one workspace.

Suggested fix

Read scope and projectPath alongside installPath and split the load into two passes:

  • global pass (load_global) takes only unscoped/user-scoped installs;
  • workspace pass (load_project_overlay) takes only installs whose projectPath is the session's workspace root or a parent of it, so nested directories inside the project still match.

Two details worth calling out:

  • An install can be "scope": "user" and still record the projectPath it happened to be installed from. Scope should win, otherwise a user-scoped plugin would silently become project-only.
  • The legacy unscoped layouts (repos/, and the bare cache/ fallback used when the manifest is missing) carry no scope information, so they belong to the global pass only. Letting the overlay re-scan them would undo the isolation and make every overlay load do a full plugin tree walk.

Symlinked workspaces need canonical-path comparison for the containment check.

I have a patch with tests for the three cases (owning workspace incl. nested dirs, unrelated workspace, global pass) and can open a PR.


Investigated and written with agent assistance.
Agent: jcode v0.64.160-dev (claude-opus-5)

Dominant language
Rust
Stars
19.9k
Forks
2.3k
Avg merge
2d 7h
Merged PRs (30d)
30

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from 1jehuang/jcode

All issues in 1jehuang/jcode

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.