Skip to content
david winter

Keeping Claude Code MCP credentials out of git with 1Password

I commit parts of my Claude Code setup to git so it follows me between machines and stays version controlled. The trouble is that MCP servers often need credentials, an API token here, a bearer token there, and those absolutely cannot go into a file that’s tracked in a repo. I already keep everything in 1Password and use its CLI daily, so the answer was to have Claude Code pull the tokens from there at the moment it needs them, rather than store them anywhere on disk.

The result is a .mcp.json I’m happy to commit. It describes every server, but holds no secrets. Each one that needs a credential resolves it at launch through a helper command that reads from 1Password, so the token only ever exists in memory while the server is running.

How the credential gets injected

Claude Code supports a headersHelper field on an HTTP MCP server. Instead of a static header value, you give it a command, and Claude Code runs that command when it starts the server and merges the output into the request headers. The command has to print a single JSON object mapping header names to values.

That’s the hook. The command reads the secret from 1Password with op read and the op://vault/item/field reference format, so the reference (which is safe to commit) lives in the config, and the actual value is fetched on demand.

Note: you need the 1Password desktop app installed, with CLI integration enabled under Settings > Developer > “Integrate with 1Password CLI”. I’m frequently between a mixture of WSL, Linux and macOS, but on WSL, the CLI is the Windows binary invoked as op.exe; on macOS or Linux it’s just op. In future, I’ll create an alias within my Fish shell so that op is available on all platforms!

Adding a server the right way

The important detail is to add servers with claude mcp add-json, not the plain claude mcp add. The plain command’s --header flag only stores static values, which would defeat the whole point. The JSON variant takes a raw blob, so it’s the only path that can carry the dynamic headersHelper field.

Store the credential in 1Password first, note its op://vault/item/field reference, then feed the template to add-json:

claude mcp add-json --scope project <server-name> '{
    "type": "http",
    "url": "<mcp-endpoint-url>",
    "headersHelper": "printf '"'"'{\"<Header-Name>\": \"%s\"}'"'"' \"$(op.exe read '"'"'op://<Vault>/<Item>/<field>'"'"')\""
  }'

Remember, change op.exe to just op on macOS or Linux.

Restart Claude Code afterwards and confirm the server connects. The quoting looks fierce, but you’re only ever changing the header name, the URL, and the op:// reference.

A single secret

Most servers need one bearer token in an Authorization header. Here’s GitHub, pinned to read-only tools with the /readonly on the URL:

claude mcp add-json --scope project github '{
    "type": "http",
    "url": "https://api.githubcopilot.com/mcp/readonly",
    "headersHelper": "printf '"'"'{\"Authorization\": \"Bearer %s\"}'"'"' \"$(op.exe read '"'"'op://Employee/GitHub token/credential'"'"')\""
  }'

More than one secret

When a server needs two credentials, send them as two headers. Extend the printf format string with another %s and add one op.exe read per placeholder, in order. Datadog wants an API key and an application key:

claude mcp add-json --scope project datadog '{
    "type": "http",
    "url": "https://mcp.datadoghq.eu/api/unstable/mcp-server/mcp",
    "headersHelper": "printf '"'"'{\"DD_API_KEY\": \"%s\", \"DD_APPLICATION_KEY\": \"%s\"}'"'"' \"$(op.exe read '"'"'op://Employee/Datadog API key token/credential'"'"')\" \"$(op.exe read '"'"'op://Employee/Datadog MCP token/credential'"'"')\""
  }'

With this in place my Claude Code config is fully committable. The tokens stay in 1Password, unlocked by the desktop app I’ve already authenticated, and they’re pulled into memory only when a server starts. Nothing sensitive now ever touches the repo.