fix: scope plugin enablement per project so multi-org developers get only their current repo's MCP + skills (#20)#21
Conversation
…only their current repo's MCP + skills Closes #20 Pre-#20 versions wrote enablement to user-scope ~/.claude/settings.json. That meant every Claude Code session in every repo loaded *every* installed org's MCP server and skill catalogue. A developer with access to celliq and talentrix saw both MCPs connect and both skill sets appear in every session, regardless of which repo they were actually in. Move enablement to <projectDir>/.claude/settings.local.json. Plugin installation (marketplace, plugin files, installed_plugins.json) stays global — only the *enable* toggle is per-project. After this commit: cd ~/code/celliq && claude → only aictrl-celliq MCP + skills load cd ~/code/talentrix && claude → only aictrl-talentrix MCP + skills load Telemetry, credentials, and per-project routing in the slash-command hook are unchanged — they already key off .aictrl.json. Migration: on install we clean this org's user-scope enablement entry out of ~/.claude/settings.json (preserving every other entry so other orgs and non-aictrl plugins survive). The next install in another org's repo migrates that org's entry. The new settings.local.json is auto-added to the project .gitignore since it carries per-developer enablement state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Now let me read the reference files and build the manifests.Now let me read the patches and build the manifests.Now I have everything. Total changed lines S = 338, so N = 1 bug hunter shard. I'll dispatch all 3 subagents (Security, Consistency, Bug Hunter 1/1) in parallel.All 3 subagents have returned. Let me merge and deduplicate the findings, then record the review. Subagent results:
After deduplication by Review: PR #21 — Scope plugin enablement per project1 MAJOR | 3 MINOR | 0 blockers | 0 nits Data Integrity[MAJOR]
// current
delete (enabledPlugins as Record<string, unknown>)[pluginDirName];
await writeFile(userSettingsFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8');Fix: Write to a temp file first, then rename: import { rename } from 'fs/promises';
// ...
const tmp = userSettingsFile + '.tmp';
await writeFile(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
await rename(tmp, userSettingsFile);Correctness[MINOR]
// current
const PROJECT_SETTINGS_RELPATH = join('.claude', 'settings.local.json');Fix: Use a plain forward-slash string literal — identical result on POSIX, safe on Windows: const PROJECT_SETTINGS_RELPATH = '.claude/settings.local.json';Consistency[MINOR]
Fix: Either move Performance / Housekeeping[MINOR]
Fix: Acceptable as-is (cost is one stat + read of a small file). If desired, add a brief comment clarifying this is intentional per-run cleanup, or gate behind |
… migration intent comment Addresses 3 of 4 aictrl-dev review findings: - [MAJOR] extract writeJsonAtomic helper (tmp + rename) and use it for both user-scope settings.json (high-blast-radius user-owned file) and the new project-scope settings.local.json (also carries user state like theme). Original file content stays intact until the new content is durable, so a SIGKILL/power-loss mid-write cannot truncate the file. - [MINOR] use a forward-slash literal for PROJECT_SETTINGS_RELPATH so the value is correct in both the .gitignore (which only matches POSIX separators) and Node fs operations (Node accepts forward slashes on Windows too). Avoids a Windows-only bug where the gitignore entry would silently fail to match the actual file. - [MINOR] comment clarifying that removeUserScopeEnablement runs every install intentionally — self-healing if a stale entry returns via backup restore or manual edit. The 4th finding (gitignore-responsibility-differs-across-writers) is deferred to #22 — the principled fix harmonises in the opposite direction the reviewer suggested (move cursor's gitignore call into installCursor) which is broader scope than #21. 2 new regression tests; suite 116/116 (was 114). Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review response — PR #21All 4 findings verified TRUE. 3 fixed in-PR, 1 deferred to a follow-up issue (it would regress test coverage to fix here). Issues addressed (pushed to this PR)
2 regression tests added in
The existing Suite: 116/116 pass (was 114). Review claims verified false (no change needed)None — all 4 findings were verified true. Not addressed here
|
Summary
Closes #20.
Pre-#20, the installer wrote
enabledPlugins["aictrl-<org>@aictrl"] = trueto user-scope~/.claude/settings.json. A developer with access to two aictrl orgs (e.g.celliqandtalentrix) rannpx @aictrl/pluginin each repo and ended up with both plugins enabled globally — every Claude Code session in every project connected both MCP servers and loaded both skill catalogues, regardless of which repo they were actually in.After this PR
installClaudePluginwrites enablement to project-scope<projectDir>/.claude/settings.local.jsoninstead. Plugin installation (marketplace dir, plugin files,installed_plugins.json) stays global — only the enable toggle is per-project.Telemetry, credentials, and the slash-command hook are unchanged — they already resolve the active org from
.aictrl.jsonwalking up the CWD.Migration for users on a pre-#20 install
On every run,
installClaudePluginnow:settings.local.json.~/.claude/settings.jsonif present (preserves every other entry — other orgs' aictrl plugins,claude-plugins-officialplugins, etc.). The next install in another org's repo migrates that org's entry too..claude/settings.local.jsonto project.gitignore(it's per-developer state, shouldn't be committed).API change
ClaudePluginOptionsswaps:cli.tsnow passesprojectDir(already computed at the top ofmain()) and renames the user-settings arg.Repro (from #20)
npx @aictrl/plugin --org celliq --api-key ...in~/code/celliq.npx @aictrl/plugin --org talentrix --api-key ...in~/code/talentrix.cd ~/code/celliq && claudeconnects bothaictrl-celliqandaictrl-talentrixMCP servers; both skill sets visible.aictrl-celliqloads.Test plan
5 new regression tests in
test/writers/claude.test.ts(suite total: 114, up from 109):does not write any enablement entry to user-scope settings.jsonremoves legacy user-scope enablement on upgrade and preserves unrelated entriesdoes not touch unrelated aictrl-<other-org>@aictrl entries in user settings— celliq install must not clobber a pre-existing talentrix entryadds .claude/settings.local.json to project .gitignoremulti-org: two project dirs each get only their own org enabled— end-to-end multi-org scenario asserting both settings.local.json files land correctly and the global marketplace still carries both plugin entriesThe existing
enables plugin in [project-scope] settings.local.jsonandpreserves existing entries in project settings.local.json when mergingtests were rewritten to target the new file. The end-to-end consistency test now reads enablement from the project file.npm test— 114/114 passnpm run build— clean tsc🤖 Generated with Claude Code