[{"data":1,"prerenderedAt":35},["ShallowReactive",2],{"post-data-mcp-server-api-keys-claude-code":3},{"post":4,"relatedPosts":19},{"id":5,"title":6,"content":7,"hashtags":8,"coverImage":15,"createdAt":16,"seoTitle":17,"seoDescription":18},"C1FTTqGsgY7jeMF2S54V","Where to Store MCP Server API Keys in Claude Code Without Committing Them","\u003Ch1>Where to Store MCP Server API Keys in Claude Code Without Committing Them\u003C\u002Fh1>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> do not put the key itself in \u003Ccode>.mcp.json\u003C\u002Fcode>. If the server supports OAuth, sign in with \u003Ccode>\u002Fmcp\u003C\u002Fcode> and the file holds only a URL. Otherwise commit a \u003Ccode>.mcp.json\u003C\u002Fcode> that contains \u003Ccode>${VAR}\u003C\u002Fcode> references, and supply the values through your environment or a secret manager. The commands \u003Ccode>claude mcp add --env\u003C\u002Fcode>, \u003Ccode>--header\u003C\u002Fcode> and \u003Ccode>add-json\u003C\u002Fcode> write whatever you type to disk, and \u003Ccode>claude mcp get\u003C\u002Fcode> prints it back, so a literal key passed to them is a literal key in a file.\u003C\u002Fp>\n\u003Cp>This guide is based on the Claude Code documentation, opened on October 22, 2026, and on a test of version 2.1.307 on Linux that used \u003Ccode>claude mcp\u003C\u002Fcode> commands with fake keys. No model session was run, because the CLI was not logged in; servers were exercised through the health check that \u003Ccode>claude mcp list\u003C\u002Fcode> runs, so everything below is about configuration and what the CLI writes or prints. Observed results come from one run each and may differ on your version. The changelog records several changes to how \u003Ccode>claude mcp\u003C\u002Fcode> displays credentials since September, so check yours with \u003Ccode>claude --version\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2>Where does Claude Code keep MCP server settings?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> in one of three places, chosen by scope. Only the project scope is shared through git.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Scope\u003C\u002Fth>\n\u003Cth>Stored in\u003C\u002Fth>\n\u003Cth>Shared with the team\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>local (default)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>~\u002F.claude.json\u003C\u002Fcode>, under the entry for your project path\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>project\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>.mcp.json\u003C\u002Fcode> in the project root\u003C\u002Ftd>\n\u003Ctd>Yes, if you commit it\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>user\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>~\u002F.claude.json\u003C\u002Fcode>, top level\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>The docs recommend local scope for \"servers with credentials you don't want in version control\". Two details trip people up. MCP's local scope lives in \u003Ccode>~\u002F.claude.json\u003C\u002Fcode>, not in \u003Ccode>.claude\u002Fsettings.local.json\u003C\u002Fcode>. And precedence is by whole entry: local beats project beats user, and \"fields are not merged across scopes\". A local server with the same name as a project one replaces it entirely, including its \u003Ccode>env\u003C\u002Fcode> and \u003Ccode>headers\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>On managed machines there is also \u003Ccode>managed-mcp.json\u003C\u002Fcode> (for example \u003Ccode>\u002Fetc\u002Fclaude-code\u002F\u003C\u002Fcode> on Linux). The docs warn that any user on the machine can read it, so it should not hold keys in \u003Ccode>env\u003C\u002Fcode> blocks.\u003C\u002Fp>\n\u003Ch2>What do \u003Ccode>claude mcp add\u003C\u002Fcode> and \u003Ccode>claude mcp get\u003C\u002Fcode> do with a key?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> \u003Ccode>add\u003C\u002Fcode> writes the literal value to the scope's file, and \u003Ccode>get\u003C\u002Fcode> and \u003Ccode>list\u003C\u002Fcode> print literal values. Only \u003Ccode>${VAR}\u003C\u002Fcode> references stay masked.\u003C\u002Fp>\n\u003Cp>Observed on version 2.1.307, with values like \u003Ccode>sk-test-FAKE-123\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>claude mcp add name --transport stdio -e API_KEY=... -- command\u003C\u002Fcode>, the same command with \u003Ccode>--header \"Authorization: Bearer ...\"\u003C\u002Fcode> for a remote server, and \u003Ccode>claude mcp add-json\u003C\u002Fcode> stored the value in plaintext. With \u003Ccode>--scope project\u003C\u002Fcode>, it went into \u003Ccode>.mcp.json\u003C\u002Fcode>, which was created with mode 644, so it is readable by other users and ready for \u003Ccode>git add\u003C\u002Fcode>. \u003Ccode>~\u002F.claude.json\u003C\u002Fcode>, which holds user and local scope, was mode 600.\u003C\u002Fli>\n\u003Cli>After \u003Ccode>add --header\u003C\u002Fcode>, the command's own output showed \u003Ccode>\"Authorization\": \"[REDACTED]\"\u003C\u002Fcode>. The file still held the full value.\u003C\u002Fli>\n\u003Cli>\u003Ccode>claude mcp get\u003C\u002Fcode> printed header values, \u003Ccode>env\u003C\u002Fcode> values, \u003Ccode>args\u003C\u002Fcode> and URL query strings as stored. This included a project server that was still waiting for approval. \u003Ccode>claude mcp list\u003C\u002Fcode> printed the command line and URL, including \u003Ccode>--api-key ...\u003C\u002Fcode> and \u003Ccode>?key=...\u003C\u002Fcode> parts.\u003C\u002Fli>\n\u003Cli>A \u003Ccode>${VAR}\u003C\u002Fcode> reference was shown by name, not value, and the docs say the same.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The changelog for version 2.1.161 says credential headers and URL secrets are redacted in these commands, and later releases changed what is displayed. That did not match what was observed for \u003Ccode>get\u003C\u002Fcode> with these fake values, so the redaction may depend on the shape of the value. Treat \u003Ccode>get\u003C\u002Fcode> and \u003Ccode>list\u003C\u002Fcode> output as sensitive when an entry holds literals, and do not paste it into issues or chats.\u003C\u002Fp>\n\u003Ch2>Which method should you use?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> pick the first row that your server supports.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Method\u003C\u002Fth>\n\u003Cth>Use when\u003C\u002Fth>\n\u003Cth>What lands in the file\u003C\u002Fth>\n\u003Cth>Main caveat\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>OAuth (\u003Ccode>\u002Fmcp\u003C\u002Fcode> or \u003Ccode>claude mcp login &lt;name&gt;\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>The remote server supports OAuth\u003C\u002Ftd>\n\u003Ctd>Type and URL only\u003C\u002Ftd>\n\u003Ctd>The docs say tokens are \"stored securely\" but do not say where\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>${VAR}\u003C\u002Fcode> reference\u003C\u002Ftd>\n\u003Ctd>Any stdio server, or a remote server with a static token\u003C\u002Ftd>\n\u003Ctd>The variable name\u003C\u002Ftd>\n\u003Ctd>Values must come from your environment; unset variables are not an error\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>headersHelper\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Remote server that needs short-lived or computed headers\u003C\u002Ftd>\n\u003Ctd>The path of a script\u003C\u002Ftd>\n\u003Ctd>Runs for up to 10 seconds; project-scope helpers lose credential-looking variables\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Wrapper command\u003C\u002Ftd>\n\u003Ctd>The stdio server needs secrets from a manager\u003C\u002Ftd>\n\u003Ctd>The wrapper's command line\u003C\u002Ftd>\n\u003Ctd>Not covered by Claude Code docs; verify it in your setup\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Also, if a configured \u003Ccode>Authorization\u003C\u002Fcode> header is rejected, there is no OAuth fallback. The docs say the connection is reported as failed instead of falling back, and \u003Ccode>claude mcp list\u003C\u002Fcode> printed: \"OAuth fallback is disabled when headers.Authorization is set.\"\u003C\u002Fp>\n\u003Ch2>How do you write a \u003Ccode>.mcp.json\u003C\u002Fcode> that holds no keys?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> reference variables by name in \u003Ccode>command\u003C\u002Fcode>, \u003Ccode>args\u003C\u002Fcode>, \u003Ccode>env\u003C\u002Fcode>, \u003Ccode>url\u003C\u002Fcode> and \u003Ccode>headers\u003C\u002Fcode>. Claude Code expands \u003Ccode>${VAR}\u003C\u002Fcode> and \u003Ccode>${VAR:-default}\u003C\u002Fcode> in those fields.\u003C\u002Fp>\n\u003Cpre class=\"language-json\">\u003Ccode class=\"language-json\">{\n  \"mcpServers\": {\n    \"example-local\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"example-mcp-server\"],\n      \"env\": {\n        \"EXAMPLE_API_KEY\": \"${EXAMPLE_API_KEY}\"\n      }\n    },\n    \"example-remote\": {\n      \"type\": \"http\",\n      \"url\": \"https:\u002F\u002Fmcp.example.com\u002Fmcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer ${EXAMPLE_MCP_TOKEN}\"\n      }\n    }\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>example-mcp-server\u003C\u002Fcode> and \u003Ccode>mcp.example.com\u003C\u002Fcode> are placeholders. Edit the file by hand or add it with \u003Ccode>claude mcp add-json --scope project\u003C\u002Fcode>, which stored \u003Ccode>${...}\u003C\u002Fcode> references unchanged in testing. Do not use \u003Ccode>add -e KEY=...\u003C\u002Fcode> for this, because it takes the literal.\u003C\u002Fp>\n\u003Cp>Export the variables in the shell that starts Claude Code, or load them from a secret manager (see the last section). Three behaviors matter before you rely on this.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>An unset variable is not an error.\u003C\u002Fstrong> Per the docs, the config still loads and Claude Code \"uses the unexpanded \u003Ccode>${VAR}\u003C\u002Fcode> text as-is\". In testing, the stdio server received the string \u003Ccode>${UNSET_VAR_XYZ}\u003C\u002Fcode> as its key. The only hint is a \"Missing environment variables\" line in \u003Ccode>claude mcp list\u003C\u002Fcode> and a warning in \u003Ccode>\u002Fmcp\u003C\u002Fcode>; \u003Ccode>claude mcp get\u003C\u002Fcode> output looked the same whether the variable was set or not. A typo in a variable name therefore tends to show up as a rejected request, such as a 401, and not as a startup failure.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Credential-named variables read as empty in remote \u003Ccode>url\u003C\u002Fcode> and \u003Ccode>headers\u003C\u002Fcode>.\u003C\u002Fstrong> The docs: \"A covered name reads as empty whether or not you have set the variable\". Named examples are \u003Ccode>ANTHROPIC_API_KEY\u003C\u002Fcode>, \u003Ccode>ANTHROPIC_AUTH_TOKEN\u003C\u002Fcode>, \u003Ccode>AWS_BEARER_TOKEN_BEDROCK\u003C\u002Fcode>, \u003Ccode>HTTPS_PROXY\u003C\u002Fcode> and \u003Ccode>NPM_TOKEN\u003C\u002Fcode>; the full list is not published. In testing, \u003Ccode>Bearer ${ANTHROPIC_AUTH_TOKEN}\u003C\u002Fcode> arrived as \u003Ccode>Bearer \u003C\u002Fcode> with nothing after it, and a header built from \u003Ccode>NPM_TOKEN\u003C\u002Fcode> was empty, while a variable named \u003Ccode>MY_TOKEN_Z\u003C\u002Fcode> expanded normally. The docs' fix is to copy the value into a variable with your own name and reference that. This rule applies to remote URLs and headers. A stdio \u003Ccode>env\u003C\u002Fcode> value with an ordinary variable name expanded normally in the same test, and a covered name in stdio \u003Ccode>env\u003C\u002Fcode> was not tried.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>A local entry replaces a project one.\u003C\u002Fstrong> If you once ran \u003Ccode>claude mcp add\u003C\u002Fcode> with a literal key at local scope under the same server name, that entry wins over the committed file, and the literal stays in \u003Ccode>~\u002F.claude.json\u003C\u002Fcode>. Per the docs, Claude Code warns in \u003Ccode>claude mcp list\u003C\u002Fcode> and \u003Ccode>\u002Fmcp\u003C\u002Fcode> only when the two entries point to different endpoints. Check \u003Ccode>~\u002F.claude.json\u003C\u002Fcode> for a local entry with the same name as a committed server.\u003C\u002Fp>\n\u003Ch2>Which environment does a stdio MCP server inherit?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> by default, all of it. A stdio server started by Claude Code saw every fake secret exported in the parent shell during testing: \u003Ccode>ANTHROPIC_API_KEY\u003C\u002Fcode>, \u003Ccode>AWS_SECRET_ACCESS_KEY\u003C\u002Fcode>, \u003Ccode>GITHUB_TOKEN\u003C\u002Fcode>, \u003Ccode>DB_PASSWORD\u003C\u002Fcode> and \u003Ccode>NPM_TOKEN\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Two settings narrow this, and both are documented in the \u003Ca href=\"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fenv-vars\">environment variables reference\u003C\u002Fa> rather than on the MCP page:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>CLAUDE_CODE_MCP_ALLOWLIST_ENV=1\u003C\u002Fcode> starts stdio servers with \"only a safe baseline environment plus the server's configured \u003Ccode>env\u003C\u002Fcode>\". In testing, none of the fake secrets exported in the shell arrived, while the entry's own \u003Ccode>env\u003C\u002Fcode> (including expanded \u003Ccode>${VAR}\u003C\u002Fcode> values) did. As a result, a variable you want the server to have must now be listed in its \u003Ccode>env\u003C\u002Fcode> block.\u003C\u002Fli>\n\u003Cli>\u003Ccode>CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1\u003C\u002Fcode> removes variables that look like credentials from what Bash commands, hooks and stdio MCP servers receive. Detection uses the variable name and the value shape, so a secret whose name and value both look ordinary passes through, and the docs say GitHub tokens and proxy settings are kept. On Linux the option needs bubblewrap, so its effect on an MCP server was not observed here. Our guide to \u003Ca href=\"https:\u002F\u002Fennote.io\u002Fblog\u002Fkeep-env-secrets-away-from-claude-code\">keeping .env secrets away from Claude Code\u003C\u002Fa> covers it in more detail.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The \u003Ccode>env\u003C\u002Fcode> block in \u003Ccode>settings.json\u003C\u002Fcode> is also plain text. The settings reference says its values \"reach every subprocess Claude Code starts\", so it is not a place for keys either.\u003C\u002Fp>\n\u003Ch2>When should you use \u003Ccode>headersHelper\u003C\u002Fcode>?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> for a remote server whose token must be fetched or refreshed, because the script runs on every connection and nothing is cached.\u003C\u002Fp>\n\u003Cp>Add \u003Ccode>\"headersHelper\": \"\u002Fpath\u002Fto\u002Fscript\"\u003C\u002Fcode> to the server entry. The script prints a JSON object of string pairs to stdout. These headers override static headers with the same name. Observed behavior and documented limits:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>The helper has 10 seconds. A helper that slept for 15 seconds made \u003Ccode>claude mcp list\u003C\u002Fcode> take about 10.8 seconds and no helper header was sent.\u003C\u002Fli>\n\u003Cli>For project and local servers, the helper does not run until the workspace trust dialog has been accepted. Without that, \u003Ccode>claude mcp list\u003C\u002Fcode> reported \"headersHelper not run\". A user-scope helper ran from \u003Ccode>~\u002F.claude\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Helpers from project scope run without credential-looking variables (names containing TOKEN, SECRET, PASSWORD, KEY or AUTH per the docs). The script must read its secret from a file or a credential store, not from the environment. In testing, a project helper did not see a custom variable called \u003Ccode>MY_TOKEN_Z\u003C\u002Fcode>; a local-scope helper did.\u003C\u002Fli>\n\u003Cli>An \u003Ccode>Authorization\u003C\u002Fcode> header from the helper turns off the OAuth fallback, like a static one.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>What changes when a team shares the repository?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> committed servers need approval, but not in every mode, and a committed file is only as safe as what is inside it.\u003C\u002Fp>\n\u003Cp>In an interactive session, Claude Code asks before using project servers; \u003Ccode>claude mcp reset-project-choices\u003C\u002Fcode> resets the answers. The docs state the limit: \"Claude Code can't show that prompt: it loads project-scoped servers without asking.\" That covers \u003Ccode>claude -p\u003C\u002Fcode>, the Agent SDK and some cloud sessions. To opt out there, use \u003Ccode>disabledMcpjsonServers\u003C\u002Fcode>, \u003Ccode>--setting-sources\u003C\u002Fcode> or \u003Ccode>--strict-mcp-config\u003C\u002Fcode>. Approvals committed in \u003Ccode>.claude\u002Fsettings.json\u003C\u002Fcode> are ignored in a folder that is not yet trusted, so a cloned repository cannot approve its own servers.\u003C\u002Fp>\n\u003Cp>For an organization, \u003Ccode>allowedMcpServers\u003C\u002Fcode> and \u003Ccode>deniedMcpServers\u003C\u002Fcode> (deny wins) and \u003Ccode>allowManagedMcpServersOnly\u003C\u002Fcode> control which servers may run, and a permission rule such as \u003Ccode>mcp__&lt;server&gt;__*\u003C\u002Fcode> can deny a server's tools. These limit what runs. They do not protect a key that is already written in a file.\u003C\u002Fp>\n\u003Ch2>How do you check which keys are still on disk?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> search the places where literals end up, then confirm that references expand.\u003C\u002Fp>\n\u003Col>\n\u003Cli>In the repository: \u003Ccode>git grep -n -E 'sk-|Bearer [A-Za-z0-9]|key=|api-key' -- .mcp.json .claude\u003C\u002Fcode>. This searches tracked files in the working tree only, not history, and the pattern is an example: adjust it to your providers' key prefixes.\u003C\u002Fli>\n\u003Cli>In your home config: open \u003Ccode>~\u002F.claude.json\u003C\u002Fcode> and look at \u003Ccode>mcpServers\u003C\u002Fcode> and \u003Ccode>projects.&lt;your path&gt;.mcpServers\u003C\u002Fcode> for literal values in \u003Ccode>env\u003C\u002Fcode>, \u003Ccode>headers\u003C\u002Fcode>, \u003Ccode>args\u003C\u002Fcode> and \u003Ccode>url\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Run \u003Ccode>claude mcp list\u003C\u002Fcode>. A line \"Missing environment variables\" under the config diagnostics means a reference has no value in this shell.\u003C\u002Fli>\n\u003Cli>For a remote server, a 401 right after you changed a variable name is the symptom of an unset or blanked reference. Compare with the credential-name rule above.\u003C\u002Fli>\n\u003Cli>After each Claude Code upgrade, repeat steps 2 and 3, since display rules have been changing.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>What if a key is already committed?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> revoke it at the provider first. Deleting the line from \u003Ccode>.mcp.json\u003C\u002Fcode> does not change the commit history, and anyone who fetched the repository still has the key.\u003C\u002Fp>\n\u003Cp>After revoking, issue a new key, put it behind a \u003Ccode>${VAR}\u003C\u002Fcode> reference, and then clean the file and, if you choose, the history. The order of actions and the provider consoles are in our guide to \u003Ca href=\"https:\u002F\u002Fennote.io\u002Fblog\u002Fleaked-openai-or-anthropic-api-key-what-to-do-in-the-first-hour\">what to do when an OpenAI or Anthropic key leaks\u003C\u002Fa>. If a key was only printed by \u003Ccode>claude mcp get\u003C\u002Fcode> in a terminal or a shared screen, treat it as exposed to whoever saw it.\u003C\u002Fp>\n\u003Ch2>Where should the values come from?\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Short answer:\u003C\u002Fstrong> from a secret manager that puts them in the environment of the process that needs them, so no dotfile holds them.\u003C\u002Fp>\n\u003Cp>The MCP authorization specification says a stdio server \"SHOULD NOT follow this specification, and instead retrieve credentials from the environment\". Environment variables are the expected channel, and \u003Ccode>${VAR}\u003C\u002Fcode> references are how Claude Code hands them over. What remains is how the variables get into your shell without a plaintext \u003Ccode>.env\u003C\u002Fcode> or a shell profile line.\u003C\u002Fp>\n\u003Cp>One option is a CLI that runs a command with secrets injected. Ennote, the company behind this blog, makes one, and its CLI documents \u003Ccode>ennote secret get &lt;secret-name&gt; -- &lt;command&gt;\u003C\u002Fcode>, which injects a secret's keys as environment variables of the child process, and \u003Ccode>ENNOTE_TOKEN\u003C\u002Fcode> for machine identities in headless use. Wrapping a stdio server in it would look like \u003Ccode>claude mcp add example-local --transport stdio -- ennote secret get example-keys -- npx -y example-mcp-server\u003C\u002Fcode>. That combination was not run for this article. Two things to check if you try it: with \u003Ccode>CLAUDE_CODE_MCP_ALLOWLIST_ENV=1\u003C\u002Fcode> the wrapper itself needs its own credentials in the entry's \u003Ccode>env\u003C\u002Fcode>, which brings back a secret in a file, and the environment scrub may remove a variable named like a token. The Ennote docs do not mention MCP, so confirm both in your setup.\u003C\u002Fp>\n\u003Cp>For remote servers the same idea applies one level up: start Claude Code from a shell where the secret manager has already exported the variables your \u003Ccode>${VAR}\u003C\u002Fcode> references expect.\u003C\u002Fp>\n\u003Ch2>Sources\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>Claude Code docs, opened October 22, 2026: \u003Ca href=\"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fmcp\">MCP\u003C\u002Fa>, \u003Ca href=\"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fenv-vars\">Environment variables\u003C\u002Fa>, \u003Ca href=\"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fsettings-reference\">Settings reference\u003C\u002Fa>, \u003Ca href=\"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fmanaged-mcp\">Managed MCP\u003C\u002Fa>, \u003Ca href=\"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fchangelog\">Changelog\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>Model Context Protocol, \u003Ca href=\"https:\u002F\u002Fmodelcontextprotocol.io\u002Fspecification\u002F2025-11-25\u002Fbasic\u002Fauthorization\">authorization specification, 2025-11-25\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>Ennote: \u003Ca href=\"https:\u002F\u002Fdocs.ennote.io\u002Fcli\u002Foverview\">CLI overview\u003C\u002Fa>, \u003Ca href=\"https:\u002F\u002Fdocs.ennote.io\u002Fcli\u002Fquickstart\">CLI quickstart\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>Observed behavior: Claude Code CLI 2.1.307, Linux, fake values, one run per case, no model session\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Cp>\u003Cstrong>Keep keys out of dotfiles.\u003C\u002Fstrong> Ennote is a secret manager for passwords, API keys and environment variables, with role-based access and an audit log. \u003Ca href=\"https:\u002F\u002Fapp.ennote.io\u002F?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=mcp-server-api-keys-claude-code&amp;utm_content=footer\">Start for free\u003C\u002Fa> on the Developer plan.\u003C\u002Fp>",[9,10,11,12,13,14],"MCP","ClaudeCode","APIKeys","SecretManagement","DevSecOps","CyberSecurity","https:\u002F\u002Ffirebasestorage.googleapis.com\u002Fv0\u002Fb\u002Fblog-01-c712e.firebasestorage.app\u002Fo\u002Fblog-covers%2F1793122242811_Gemini_Generated_Image_wp95o4wp95o4wp95.jpeg?alt=media&token=43d78904-0e34-492b-927f-45e8ff52d992",1793122244638,"MCP Server API Keys in Claude Code: What to Keep Out of Git","Where Claude Code stores MCP API keys, what claude mcp add writes to disk, and how to use OAuth, env references or headersHelper. Checked on 2.1.307",[20,25,30],{"title":21,"slug":22,"coverImage":23,"createdAt":24},"The Evolution of Kubernetes Secret Delivery (And Why Polling is Dead)","the-evolution-of-kubernetes-secret-delivery-and-why-polling-is-dead","https:\u002F\u002Ffirebasestorage.googleapis.com\u002Fv0\u002Fb\u002Fblog-01-c712e.firebasestorage.app\u002Fo\u002Fblog-covers%2F1777523487463_diagramm%20(2).jpg?alt=media&token=5b01f974-cfe5-4890-b68f-5e178b422c6e",1777523490608,{"title":26,"slug":27,"coverImage":28,"createdAt":29},"The Identity-Driven Cryptography Behind Ennote's Zero-Persistence Vault","the-identity-driven-cryptography-behind-ennote-s-zero-persistence-vault","https:\u002F\u002Ffirebasestorage.googleapis.com\u002Fv0\u002Fb\u002Fblog-01-c712e.firebasestorage.app\u002Fo\u002Fblog-covers%2F1778396426354_og-image.png?alt=media&token=4bd31d6a-d525-443b-8879-5178a80b5335",1778396428194,{"title":31,"slug":32,"coverImage":33,"createdAt":34},"Announcing the Ennote Certified Partner Network: Eliminate the Overhead of Legacy Infrastructure","certified-partner-network-launch","https:\u002F\u002Ffirebasestorage.googleapis.com\u002Fv0\u002Fb\u002Fblog-01-c712e.firebasestorage.app\u002Fo\u002Fblog-covers%2F1780035988445_wmremove-transformed%20(5)%20(2).png?alt=media&token=fdd2726b-b636-4356-b22d-a40eaf67949d",1780035990138,1793125208303]