claude-code - 💡(How to fix) Fix [BUG] Plugin sensitive userConfig not persisted to keychain/credentials.json/settings.json — values lost on Claude Code restart

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

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

Root Cause

  1. Create a minimal plugin with this plugin.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}" }
        }
      }
    }
  2. Install plugin in Claude Code
  3. /plugin → Installed → test-sensitive → Configure options → enter hello-world → Save
  4. /mcp → confirm echo connected (works — value used for spawn)
  5. Quit Claude Code fully (/exit)
  6. Inspect storage:
    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",{}))'  # {}
  7. Restart Claude Code → MCP server fails because ${user_config.api_key} substitutes to empty

Fix Action

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.

Code Example

{
     "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}" }
       }
     }
   }

---

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",{}))'  # {}
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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.json under pluginConfigs[<plugin>].options
  • ❌ Not in ~/.claude.json top-level pluginSecrets (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?

Per Plugins reference docs:

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

  1. Create a minimal plugin with this plugin.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}" }
        }
      }
    }
  2. Install plugin in Claude Code
  3. /plugin → Installed → test-sensitive → Configure options → enter hello-world → Save
  4. /mcp → confirm echo connected (works — value used for spawn)
  5. Quit Claude Code fully (/exit)
  6. Inspect storage:
    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",{}))'  # {}
  7. 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 options manually, but that workaround itself is broken for sensitive: 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)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING