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

Source: https://usezn.com/docs/integrations/mcp-config-tokens/

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](https://usezn.com/blog/gitguardian-2026-ai-coding-agents-leaked-secrets-10-minute-checklist/).

## 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](https://usezn.com/docs/integrations/antigravity/). `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](https://usezn.com/docs/integrations/editors/).

## Find the tokens

```bash
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](https://usezn.com/docs/zn-gate/shield/)). 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".

```bash
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`):

```json
{
  "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](https://usezn.com/docs/zn-gate/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

- zn-gate, protect MCP servers and `init --wrap-mcp`: [/docs/zn-gate/shield/](https://usezn.com/docs/zn-gate/shield/)
- zn-gate, what `reach` and `audit` check: [reach.md](https://github.com/usezn/zn/blob/main/docs/reach.md), [audit.md](https://github.com/usezn/zn/blob/main/docs/audit.md)
- Claude Code, MCP: <https://code.claude.com/docs/en/mcp>
- Cursor, MCP: <https://cursor.com/docs/context/mcp>
- Codex CLI, MCP: <https://learn.chatgpt.com/docs/extend/mcp?surface=cli>
- OpenCode, config and MCP servers: <https://opencode.ai/docs/config/>, <https://opencode.ai/docs/mcp-servers/>
