Plaintext tokens in your MCP config: how to move them out

Where each coding agent keeps its MCP server settings, how to find tokens written in them, how zn-gate init --wrap-mcp treats them (it does not remove them), and how to move the values out and undo each step.

View .md

An MCP server often needs a token, and the quickest way to give it one is to paste it into the server's env or headers in the agent's config file. The token then sits in a plain text file in your home folder or in your project. This page lists where each agent keeps that file, shows how to find tokens in it, explains what zn-gate init --wrap-mcp does and does not do with them, and shows how to move the values out and how to undo each step. For the wider picture of secrets and coding agents, see the checklist guide.

Where each agent keeps its MCP servers

Agent User file Project file Rewritten by init --wrap-mcp
Claude Code ~/.claude.json .mcp.json in the project root ~/.claude.json
Claude Desktop claude_desktop_config.json (macOS ~/Library/Application Support/Claude/, Windows %APPDATA%\Claude\, Linux ~/.config/Claude/) none that file
Cursor ~/.cursor/mcp.json .cursor/mcp.json ~/.cursor/mcp.json
Codex CLI ~/.codex/config.toml (or $CODEX_HOME/config.toml) .codex/config.toml (trusted projects only) the user file
OpenCode ~/.config/opencode/opencode.json (or opencode.jsonc) opencode.json in the project the user file
Antigravity ~/.gemini/config/mcp_config.json .agents/mcp_config.json the user files (zn-gate also looks for ~/.gemini/antigravity/mcp_config.json)

~/.claude.json also holds servers set up for a single project, under projects. init --wrap-mcp wraps only the top-level mcpServers of that file, so the per-project entries stay unwrapped; audit does check them.

The paths for Claude Code, Cursor, Codex CLI and OpenCode come from each agent's own documentation; Claude Desktop and Antigravity follow zn-gate's documentation and the Antigravity page. init --wrap-mcp does not touch project files: a token in a project file stays where it is. For VS Code, Windsurf, Cline, Zed, JetBrains and Continue, see Editors.

Find the tokens

npx -y zn-gate reach
npx -y zn-gate audit --include-home

reach needs zn-gate 1.12.0 or later. Both are read only. They report a secret value written in an agent config (rule ZNA011) and never print the value. Run audit from a project folder to check its project files too; --include-home adds the files in your home folder from the table above (except Claude Desktop's and Antigravity's). Both use zn-gate's own detection, so a token in a format it does not know can be missed: read your config files yourself as well.

What zn-gate init --wrap-mcp does with a token

It does not remove it. For each local (stdio) server it changes only the start command: the original command becomes the argument of npx -y zn-gate shield --. Everything else in the entry, including env, stays as it was. The shield then checks tool descriptions, call arguments and tool output, and removes secrets it recognizes from the output before your agent reads it (details). That protects what the agent sees from the server. It does not protect the file.

Other facts about the command:

  • Remote (URL) servers are left as they are, so a token in a headers entry stays.
  • Before it writes a file, it copies it next to the original as <file>.bak.<timestamp>. That copy is the file as it was, tokens included.
  • OpenCode files with comments and Codex entries with multi-line args are not rewritten; init reports them as "wrap manually".
npx -y zn-gate init --dry-run    # preview, changes nothing
npx -y zn-gate init --shadow     # wrap, log only
npx -y zn-gate init --wrap-mcp   # wrap and enforce
npx -y zn-gate init --revert     # restore the latest backup of each file

Move the value out

Put the token in an environment variable and make the config refer to it by name. What each agent's documentation says:

  • Claude Code, in .mcp.json: ${VAR} (and ${VAR:-default}) is expanded in command, args, env, url and headers. For a remote server's url and headers, Claude Code's documentation adds a limit for credential variables; read it before relying on this there.
  • Cursor, in mcp.json: ${env:NAME} is resolved in command, args, env, url and headers.
  • Codex CLI, in config.toml: env_vars forwards variables from Codex's environment to a stdio server, bearer_token_env_var names the variable that holds a bearer token, and env_http_headers maps header names to variable names. The file stores only names.
  • OpenCode: {env:VARIABLE_NAME} is substituted in config files, and its MCP documentation shows it in headers.
  • Claude Desktop, Antigravity and the other editors: we have not verified a way to refer to an environment variable in their MCP files, so this page does not give one.

Example for Cursor, with an invented server and variable (EXAMPLE_API_TOKEN):

{
  "mcpServers": {
    "example": {
      "command": "npx",
      "args": ["-y", "example-mcp-server"],
      "env": { "EXAMPLE_API_TOKEN": "${env:EXAMPLE_API_TOKEN}" }
    }
  }
}

The variable has to exist in the environment of the agent process: set it where you start the agent (your shell profile, or your operating system's way of setting variables), restart the agent, and check that the server connects. Keep in mind that a shell profile is itself a plain text file that the agent can read, so putting the token there only moves it to another file. Better: use your operating system's keychain or a secrets manager to supply the variable when you start the agent, or at least keep the token in a file outside any folder the agent opens.

A safe order

  1. Run npx -y zn-gate reach and note which files hold a token.
  2. Replace each token with an environment variable reference as above and restart the agent.
  3. Rotate every token that was ever in the file, if that file was committed, backed up, shared or pushed, if an agent could read it, or if the token appeared in a transcript. A value you removed from the file is still valid until you revoke it at the provider.
  4. Only then run npx -y zn-gate init --dry-run, then --shadow, then --wrap-mcp. The backup it writes holds the file without the tokens.
  5. If you wrapped first: delete the .bak.<timestamp> files next to your config files once the tokens are out, because they still hold the old values.
  6. Run npx -y zn-gate reach again.

What zn env shadow is for, and what it is not

zn env shadow .env moves the values of a .env file into zn-gate's local vault and leaves markers in the file (Scrubber). It reads KEY=value lines. MCP config files are JSON or TOML, and zn-gate's documentation does not describe env shadow for them, so do not point it at one.

How to undo each step

  • Wrap: npx -y zn-gate init --revert copies the latest .bak.<timestamp> back over each config file. Anything you changed in the file after wrapping is lost, and if the backup holds tokens, they come back. After step 2 above, undo by hand instead: in each entry, put the original command and arguments back in place of npx -y zn-gate shield --.
  • Environment variable references: put the value back in the file only if you must, and never in a file you commit.
  • env shadow: zn env unshadow FILE --backup NAME, with the name that env shadow printed.

Limits

  • Detection finds known formats; a token with no known format can be missed.
  • Wrapping checks what a server returns and what the agent sends it. It does not stop another program from reading the config file.
  • A project file such as .mcp.json is outside init --wrap-mcp.
  • A token you moved to an environment variable is visible to any program started with that environment.

Frequently asked questions

Does zn-gate init --wrap-mcp remove tokens from my MCP config? No. It changes the start command of local servers and leaves env and headers as they were.

Does the backup hold my tokens? Yes, if they were in the file when you wrapped. Delete the .bak.<timestamp> files after you move the values out.

Can I use zn env shadow on mcp.json? It is documented for .env files only.

How do I check that nothing is left? Run npx -y zn-gate reach and npx -y zn-gate audit --include-home, and read the files yourself.

Sources