[BUG] Plugin sensitive userConfig not persisted to keychain/credentials.json/settings.json — values lost on Claude Code restart
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When a plugin declares userConfig with sensitive: true and the user enters the value via /plugin → Installed → <plugin> → Configure options → Save, the value is silently not persisted anywhere:
- ❌ Not in macOS Keychain (no new entry created)
- ❌ Not in
~/.claude/.credentials.json(file does not exist) - ❌ Not in
~/.claude/settings.jsonunderpluginConfigs[<plugin>].options - ❌ Not in
~/.claude.jsontop-levelpluginSecrets(key not even initialized)
The value works for the current session only (in-memory env substitution into the MCP server spawn succeeds), then is lost on Claude Code restart. The user must re-enter the API key on every cold start.
No error is shown to the user. Debug logs (--debug --debug-file ...) show no save-related log lines (saveMcpServerUserConfig, Saved user config, saveConfigWithLock, secureStorage, [keychain] — all 0 occurrences).
What Should Happen?
Sensitive values go to the system keychain (or ~/.claude/.credentials.json where the keychain is unavailable).
The value entered in Configure options should be persisted to macOS Keychain and survive Claude Code restarts.
Reproduction Steps
- Create a minimal plugin with this
plugin.json:
``json``
{
"name": "test-sensitive",
"version": "0.1.0",
"description": "test",
"author": { "name": "x" },
"userConfig": {
"api_key": {
"type": "string",
"title": "API key",
"description": "test sensitive",
"sensitive": true
}
},
"mcpServers": {
"echo": {
"command": "sh",
"args": ["-c", "echo \"got=${API_KEY}\" >&2; sleep 99999"],
"env": { "API_KEY": "${user_config.api_key}" }
}
}
}
- Install plugin in Claude Code
/plugin → Installed → test-sensitive → Configure options→ enterhello-world→ Save/mcp→ confirmechoconnected (works — value used for spawn)- Quit Claude Code fully (
/exit) - Inspect storage:
``bash``
security find-generic-password -s "test-sensitive" 2>&1 # not found
ls ~/.claude/.credentials.json # ENOENT
python3 -c 'import json; d=json.load(open("~/.claude.json".replace("~", "$HOME"))); print(d.get("pluginSecrets",{}))' # {}
python3 -c 'import json; d=json.load(open("~/.claude/settings.json".replace("~", "$HOME"))); print(d.get("pluginConfigs",{}))' # {}
- Restart Claude Code → MCP server fails because
${user_config.api_key}substitutes to empty
Workaround
None for sensitive: true. For sensitive: false values, #39827 suggests manually editing settings.json under pluginConfigs, but that defeats the secure-storage promise.
I worked around in my plugin by replacing userConfig with a wrapper shell script that reads from macOS Keychain directly via security find-generic-password, requiring users to register the key manually with security add-generic-password before installing.
Environment
- Claude Code: 2.1.150 (native installer,
~/.local/share/claude/versions/2.1.150) - macOS: 25.4.0 (Darwin) on Apple Silicon
- Plugin source: GitHub-hosted private marketplace, installed via
/plugin marketplace add+/plugin install
Related Issues
- #39455 Plugin userConfig values not prompted on enable (open, macOS) — workaround =
Configure optionsmanually, but that workaround itself is broken forsensitive: true(this report) - #39827 userConfig prompt not shown during plugin install (closed not-planned, Windows)
- #40600 Cowork: Personal marketplace plugin installation lost after app restart (closed not-planned)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗