[BUG] Plugin userConfig values written to global settings.json even when plugin is installed with project scope

Status Open
Reported on v2.1.210
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Jul 15, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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

  1. Author a plugin with a userConfig block in .claude-plugin/plugin.json, e.g.:

``json
"userConfig": {
"local_repo_path": { "type": "directory", "required": true },
"base_branch": { "type": "string", "default": "main" }
}
``

  1. Install/enable the plugin scoped to a specific project (e.g. via the plugin UI, choosing "project" scope).
  2. Confirm the install was recorded as project-scoped — ~/.claude/plugins/installed_plugins.json shows:

``json
{ "scope": "project", "projectPath": "C:\\...\\my-project", ... }
``

  1. Answer the userConfig prompts (e.g. local_repo_path: ../my-repo).
  2. Inspect where the values landed:
  • Project's ./.claude/settings.json → only contains enabledPlugins, no pluginConfigs entry.
  • 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 default field in userConfig
  • #39827 – userConfig prompt not shown during install

View original on GitHub ↗

3 Comments

0xbrainkid · 1 month ago

This is a scope-boundary bug, not just a config placement bug.

If a plugin is installed with project scope, any generated or requested userConfig should 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:

  1. install plugin at project scope with userConfig
  2. assert config is written only under project/local settings, not global settings
  3. open a second project where plugin is not installed and assert no config is visible or active
  4. uninstall or disable the project plugin and assert only that project's config is removed
  5. repeat for explicit global install and verify the global path is used only there

The 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.

arnevonappen · 1 month ago

Follow-up finding, related to but distinct from the storage-location issue described above.

Repro (Claude Code CLI 2.1.210, Windows):

  1. A plugin declares userConfig.local_repo_path (type: "directory", required), referenced in one of its agents' instructions as ${user_config.local_repo_path}.
  2. Removed the pluginConfigs entry for this plugin from the global ~/.claude/settings.json, and set only a project-local .claude/settings.jsonpluginConfigs["my-plugin@my-marketplace"].options.local_repo_path, pointing at a valid, existing directory. Ran /reload-plugins, then invoked the agent fresh.
  • Result: the agent's live instructions contained the literal, unsubstituted string ${user_config.local_repo_path} — no value was injected at all, even though a valid value existed in project scope and installed_plugins.json correctly recorded the install as "scope": "project" for this exact project path.
  1. Restored a pluginConfigs entry 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-plugins again, invoked the agent fresh.
  • Result: ${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 userConfig answers 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 userConfig values effectively non-functional today, not just inconveniently stored in the wrong scope.

bcherny collaborator · 14 days ago

Thanks for the detailed report. I reproduced the behavior on 2.1.233: installing a plugin with --scope project and answering its userConfig prompts records the install as project-scoped and writes enabledPlugins to the project's .claude/settings.json, but the option values land in pluginConfigs in the user-level ~/.claude/settings.json.

This is currently intended rather than a bug. pluginConfigs values 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 --settings flag, and managed settings — never from a project's .claude/settings.json or .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-local pluginConfigs entry 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-configuration

That 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 --settings file passed at launch is a way to give a project its own values.

🤖 Generated with Claude Code