[BUG] Plugin install does not seed userConfig.*.default into settings.json — surfaces as skill !-context bash failing "Contains expansion"
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported (closest matches are #39455 for the broader install-time prompt gap and #46477 for the
defaultfield schema; neither describes the install-time materialization for declared defaults nor the downstream skill!-context failure documented here) - [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.121)
---
What's Wrong?
Root cause: when a plugin is installed, the install flow does not materialize any userConfig.*.default values into settings.json. The keys simply don't exist in settings.json until the user manually opens /plugin > Configure options and clicks Save (even with no edits). Declared default values are effectively documentation that does nothing until the user takes a manual action.
Surfacing symptom: any skill that references those values via ${user_config.KEY} in a SKILL.md !-context bash command fails the shell command permission check with "Contains expansion" — because the substitution finds no value, leaves the literal ${user_config.KEY} in the command, and the permission engine rejects the unresolved expansion.
Shell command permission check failed for pattern "...${user_config.KEY}...": Contains expansion
This is the default-field facet of the broader install-flow gap also described in #39455 (which focuses on required values not being prompted). Both stem from the same underlying problem: the install path does not write any userConfig values into settings.json — neither prompted nor defaulted.
This is distinct from #51573 (closed, fixed in v2.1.119) — that bug was about MCP substitution when a key exists with a blank value. Here the key does not exist at all.
---
What Should Happen?
Two acceptable resolutions, in order of preference:
- (a, preferred — fixes the root cause) When a plugin is installed (first time, not on every enable/re-enable), write any
userConfig.*.defaultvalues tosettings.jsonautomatically. Doing this at install time (not enable time) avoids clobbering values the user customized before disabling the plugin. This also dovetails with #39455 — installs that do not auto-prompt would still seed declared defaults so plugin authors get sensible behavior out of the box. - (b, fallback — masks the symptom) At substitution time, if
${user_config.KEY}has no value insettings.jsonbut adefaultis declared inplugin.json, fall back to the declared default. This bypasses the install-time gap without fixing it.
(a) is the right structural fix. (b) is a defense-in-depth that would still help if (a) is harder to land cleanly.
---
Error Messages/Logs
Error: Shell command permission check failed for pattern "!<plugin-cache-path>/scripts/check.sh "${user_config.my_flag}"": Contains expansion
---
Steps to Reproduce
- Create a plugin with a
userConfigfield declaring adefault:
``json``
"userConfig": {
"my_flag": {
"title": "My Flag",
"description": "Test",
"type": "boolean",
"default": false,
"sensitive": false
}
}
- Create a skill that references it in
!-context bash:
```markdown
---
name: my-skill
description: My skill.
---
## Context
- Flag value: !
${CLAUDE_PLUGIN_ROOT}/scripts/check.sh "${user_config.my_flag}"
```
- Install the plugin via
claude plugin install .... Do not open/plugin > Configure options. - Inspect
settings.json—my_flagis not present. The declareddefaultwas never written. - Invoke
/my-skill→ fails immediately withShell command permission check failed ... Contains expansion.
Workarounds (each makes the substitution work by manually triggering what install should have done):
- (i) Open
/plugin > Configure options, hit Save without changing anything → the declareddefaultgets written tosettings.json. Subsequent skill invocations succeed. - (ii) Set the value to anything (then optionally clear it back to default) via Configure options → same effect, value appears in
settings.json.
These workarounds prove the substitution mechanism itself works. The bug is specifically that the install path skips materializing declared defaults.
---
Claude Model
Not relevant — install-time / loader-time bug, no model invocation involved.
---
Is this a regression?
I don't know. Plugin install behavior around userConfig.default is undocumented (see #52623), so it is unclear if there was ever a version where defaults were auto-seeded.
---
Last Working Version
N/A.
---
Claude Code Version
2.1.121
---
Platform
Anthropic API
---
Operating System
macOS
---
Terminal
VS Code integrated terminal
---
Additional Information
Related issues:
- #39455 (open, has-repro, 16 comments) —
[BUG] Plugin userConfig values not prompted on enable. Same root-cause family as this bug — the install/enable path does not materializeuserConfigvalues intosettings.json, neither by prompting required values nor by seeding declared defaults. This bug describes thedefault-field facet and a downstream skill!-context failure mode that #39455 does not cover. - #46477 (open) —
[FEATURE] Support default field in plugin userConfig entries. Thedefaultschema field exists; this bug is about the install-time materialization step that is missing. - #51573 (closed, fixed v2.1.119) — MCP server failing on blank
${user_config.*}. Different mechanism — that fix handled blank values; this bug is about the value being entirely absent fromsettings.json. - #52623 (open, docs) —
userConfigsubstitution behavior with optional/blank values is undocumented. - #39048 (closed as duplicate) — related
!-context permission failure for${CLAUDE_SKILL_DIR}substitution.
Suggested fix: auto-seed settings.json with declared userConfig.*.default values at plugin install time (option a above). This makes default actually behave like a default and removes the "open Configure options + Save to nothing" friction. Combined with a fix for #39455 (prompting for required values), the install flow would correctly handle both facets.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗