Org-managed plugins have no way to inject secrets into MCP servers

Resolved 💬 3 comments Opened Mar 30, 2026 by augustolj Closed Apr 2, 2026

Problem

Org-managed plugins that ship MCP servers requiring API tokens (e.g., Meta Ads, Google Ads) have no viable mechanism to provide secrets in the Cowork cloud sandbox environment.

This is a blocker for teams building internal tooling plugins that connect to authenticated APIs.

What we tried

1. userConfig with sensitive: true

Defined secrets in plugin.json manifest:

{
  "user_config": {
    "api_token": {
      "type": "string",
      "sensitive": true,
      "required": true
    }
  }
}

Referenced in .mcp.json as ${user_config.api_token}.

Result: For org-managed plugins, the "Customize" button is greyed out with "managed by organization". End users cannot input values. Admins have no way to set them either.

2. ${ENV_VAR} in plugin .mcp.json

Referenced environment variables directly:

{
  "env": {
    "META_ACCESS_TOKEN": "${META_ACCESS_TOKEN}"
  }
}

Result: Vars are passed as literal strings, not expanded. This was reported in #9427 (closed), but still does not work for org-managed plugin .mcp.json files in our testing (v2.1.87).

3. SessionStart hook fetching from a vault

Added a hook to fetch secrets at session start:

{
  "hooks": {
    "SessionStart": [{
      "command": "gh api repos/org/vault/contents/secrets.env --jq '.content' | base64 -d > $CLAUDE_PLUGIN_DATA/.env"
    }]
  }
}

Result: Cowork cloud sandbox has no gh CLI, curl access is restricted, and CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA are both empty. The hook cannot fetch or write anything useful.

4. Wrapper script as MCP command

Pointed the MCP server command to a shell script that fetches secrets before launching:

{
  "command": "bash",
  "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/start-with-secrets.sh"]
}

Result: Same sandbox limitations — no network access to vaults, no credentials bootstrap.

Current workaround

A scheduled GitHub Action fetches tokens from a private vault repo and commits them directly into the plugin's .mcp.json:

# .github/workflows/refresh-secrets.yml
- name: Fetch secrets from vault
  run: |
    TOKEN=$(gh api repos/org/vault/contents/ads.env --jq '.content' | base64 -d | grep META_ACCESS_TOKEN | cut -d= -f2-)
    # Write token directly into .mcp.json
    cat > plugins/whizz-meta/.mcp.json << EOF
    { "mcpServers": { "meta-ads": { "env": { "META_ACCESS_TOKEN": "$TOKEN" } } } }
    EOF
- name: Commit if changed
  run: git add -A && git diff --staged --quiet || git commit -m "Refresh secrets" && git push

Cowork auto-syncs from the repo, so the MCP server picks up the new token. This works, but secrets live in git history (even if the repo is private).

Proposal

An org-level secrets mechanism, similar to GitHub Actions secrets:

  1. Admin defines secrets in the org plugin settings at claude.ai (e.g., META_ACCESS_TOKEN)
  2. Plugin manifest declares required secrets:

``json
{
"secrets": {
"META_ACCESS_TOKEN": {
"description": "Long-lived Meta API token",
"required": true
}
}
}
``

  1. Secrets are injected as env vars into the MCP server process at runtime — never written to disk or committed to git

This would also unblock:

  • Plugins connecting to any token-based API (Google Ads, TikTok Ads, LinkedIn Ads, etc.)
  • Plugins that need database credentials, internal API keys, etc.
  • Teams that can't use OAuth-based connectors because their APIs don't support it

Alternatives considered

  • OAuth connectors (like Slack/Asana) — ideal but requires the upstream API to have an MCP server with OAuth support. Most advertising APIs don't.
  • headersHelper script — requires network access from the sandbox to a vault, which isn't available.

Environment

  • Claude Code v2.1.87
  • Claude Team plan
  • Plugin installed as org-managed in Cowork (cloud)

Related issues

  • #9427 — env var expansion in plugin .mcp.json (closed)
  • #25964 — secret vault for plugins (closed as stale)
  • #17719 — shell command substitution in env values (closed)

View original on GitHub ↗

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