[BUG] Plugin install does not seed userConfig.*.default into settings.json — surfaces as skill !-context bash failing "Contains expansion"

Resolved 💬 3 comments Opened Apr 28, 2026 by AaronValentino Closed May 31, 2026

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 default field 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.*.default values to settings.json automatically. 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 in settings.json but a default is declared in plugin.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

  1. Create a plugin with a userConfig field declaring a default:

``json
"userConfig": {
"my_flag": {
"title": "My Flag",
"description": "Test",
"type": "boolean",
"default": false,
"sensitive": false
}
}
``

  1. 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}"

```

  1. Install the plugin via claude plugin install .... Do not open /plugin > Configure options.
  2. Inspect settings.jsonmy_flag is not present. The declared default was never written.
  3. Invoke /my-skill → fails immediately with Shell 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 declared default gets written to settings.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 materialize userConfig values into settings.json, neither by prompting required values nor by seeding declared defaults. This bug describes the default-field facet and a downstream skill !-context failure mode that #39455 does not cover.
  • #46477 (open) — [FEATURE] Support default field in plugin userConfig entries. The default schema 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 from settings.json.
  • #52623 (open, docs) — userConfig substitution 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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗