[BUG] Plugin userConfig values written to global settings.json even when plugin is installed with project scope
Claude Code version: 2.1.210
Description
When a plugin declares a userConfig block in plugin.json and is installed/enabled with project scope, the answers to the userConfig prompts are still written to the global ~/.claude/settings.json under pluginConfigs, instead of the project-local .claude/settings.json.
Steps to reproduce
- Author a plugin with a
userConfigblock in.claude-plugin/plugin.json, e.g.:
``json``
"userConfig": {
"local_repo_path": { "type": "directory", "required": true },
"base_branch": { "type": "string", "default": "main" }
}
- Install/enable the plugin scoped to a specific project (e.g. via the plugin UI, choosing "project" scope).
- Confirm the install was recorded as project-scoped —
~/.claude/plugins/installed_plugins.jsonshows:
``json``
{ "scope": "project", "projectPath": "C:\\...\\my-project", ... }
- Answer the
userConfigprompts (e.g.local_repo_path: ../my-repo). - Inspect where the values landed:
- Project's
./.claude/settings.json→ only containsenabledPlugins, nopluginConfigsentry. - Global
~/.claude/settings.json→ contains the new entry:
``json``
"pluginConfigs": {
"my-plugin@my-marketplace": {
"options": { "local_repo_path": "../my-repo", "base_branch": "main" }
}
}
Expected behavior
Since the plugin was explicitly installed with project scope, its userConfig answers should be stored alongside that scope (e.g. in the project's .claude/settings.json), not merged into the global user settings.
Actual behavior
pluginConfigs is always written to ~/.claude/settings.json, regardless of install scope. There appears to be no project-scoped storage location for userConfig answers at all.
Impact
Several userConfig fields are inherently project-relative by design (e.g. a type: "directory" field pointing at a sibling repo checkout, or a branch name). Because the values are forced into global scope:
- Using the same plugin in two different projects with different config values overwrites the config for both — last write wins.
- A project-relative path value (e.g.
../sibling-repo) becomes meaningless/wrong as soon as you're working in a different project.
This makes userConfig effectively unusable for any plugin meant to be installed per-project with per-project settings.
Related issues (none appear to be an exact duplicate of this specific scope mismatch):
- #11461 (closed) – general per-project plugin config feature request
- #62209 – feature request for
pluginDefaultScope - #46477 – feature request for
defaultfield inuserConfig - #39827 – userConfig prompt not shown during install
3 Comments
This is a scope-boundary bug, not just a config placement bug.
If a plugin is installed with project scope, any generated or requested
userConfigshould be attributable to that scope all the way through persistence. A project-scoped install writing into global settings turns a local trust decision into ambient configuration for unrelated workspaces.A regression test I would want:
userConfigThe durable event should record scope, target settings file, plugin id, and whether the write was project, global, or inherited. If a global write is intentional fallback, it should be explicit in UI/CLI and in audit output.
From the brainAI agent-receipts angle, this is the same core problem as agent identity and config provenance: a verifier needs to know which principal and scope authorized a setting, not just that a setting exists.
Boundary: architecture/test feedback only. I have not reproduced this path locally or inspected Claude Code internals.
Follow-up finding, related to but distinct from the storage-location issue described above.
Repro (Claude Code CLI 2.1.210, Windows):
userConfig.local_repo_path(type: "directory",required), referenced in one of its agents' instructions as${user_config.local_repo_path}.pluginConfigsentry for this plugin from the global~/.claude/settings.json, and set only a project-local.claude/settings.json→pluginConfigs["my-plugin@my-marketplace"].options.local_repo_path, pointing at a valid, existing directory. Ran/reload-plugins, then invoked the agent fresh.${user_config.local_repo_path}— no value was injected at all, even though a valid value existed in project scope andinstalled_plugins.jsoncorrectly recorded the install as"scope": "project"for this exact project path.pluginConfigsentry for the same key in the global settings (a different directory value than the project-local one — both directories exist on disk), left the project-local entry untouched. Ran/reload-pluginsagain, invoked the agent fresh.${user_config.local_repo_path}was now substituted, but with the global value. The project-local value was never consulted.So this isn't only about where
userConfiganswers get written during install (the original report above) — the placeholder-substitution step itself appears to read exclusively from~/.claude/settings.json, never from the project-local.claude/settings.json, regardless of how the project-local value got there (installer or manual edit). When no global entry exists for the key, the placeholder is left raw/unsubstituted in the agent's live instructions rather than falling back to project scope or failing with a clear error.This makes per-project
userConfigvalues effectively non-functional today, not just inconveniently stored in the wrong scope.Thanks for the detailed report. I reproduced the behavior on 2.1.233: installing a plugin with
--scope projectand answering itsuserConfigprompts records the install as project-scoped and writesenabledPluginsto the project's.claude/settings.json, but the option values land inpluginConfigsin the user-level~/.claude/settings.json.This is currently intended rather than a bug.
pluginConfigsvalues are substituted into plugin hook, MCP, LSP, and monitor commands, so since 2.1.207 they are only written to and read from user settings, the--settingsflag, and managed settings — never from a project's.claude/settings.jsonor.claude/settings.local.json, because a cloned repository could otherwise supply values that end up in commands. That also explains your follow-up: a project-localpluginConfigsentry is ignored, and the placeholder stays unsubstituted when there is no user-level value. Docs: https://code.claude.com/docs/en/settings#pluginconfigs and https://code.claude.com/docs/en/plugins-reference#user-configurationThat said, we agree the current model is limiting for project-scoped installs whose values are inherently per-project (sibling repo paths, branch names). We're leaving this open as a usability improvement — e.g. per-project option values stored outside the workspace so they can't be repo-supplied. In the meantime, a
--settingsfile passed at launch is a way to give a project its own values.🤖 Generated with Claude Code