Sponsored Content

DEV Community

Cover image for 1Password's secret masking silently corrupts MCP servers
Wiktor Małyska
Wiktor Małyska

Posted on

1Password's secret masking silently corrupts MCP servers

I added 1Password support to an MCP secrets runner and the first thing I had to do was turn one of its security features off. Here is why that was the right call.

What op run does

1Password's CLI resolves op:// references and hands the values to a child process:

op run --env-file=./mcp.env -- npx -y @stripe/mcp@latest
Enter fullscreen mode Exit fullscreen mode

It also does something thoughtful on top. It watches the child's output and rewrites anything matching a resolved secret to <concealed by 1Password>. If your program accidentally logs a token, or you are screen sharing, the secret does not leave the machine.

For a normal CLI this is a good default. I would keep it on.

What an MCP server is

An MCP server on the stdio transport speaks JSON-RPC over stdout. Not log lines. A framed protocol stream, where the client parses every byte:

{"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
Enter fullscreen mode Exit fullscreen mode

stdout is not a place for human-readable output. It is a wire.

The collision

Put those two together. Masking rewrites bytes in a stream that a parser is reading with exact expectations. If a secret value ever appears inside a frame, the frame changes length, and the parse fails.

What makes this genuinely nasty is where it fails. The MCP client reports a protocol error. Nothing in that error mentions 1Password, masking, or secrets. You would go and read your server's serialisation code, which is fine, for a long time.

So the provider passes --no-masking by default, and the README says why in as many words. There is an opt-in flag to turn masking back on for servers that do not use stdio.

The general shape

What I find worth writing down is that nothing here is a bug. Not in 1Password, not in MCP, not in my code.

A security feature assumes it may rewrite a child's output. A wire protocol assumes it owns stdout exclusively. Both assumptions are reasonable in isolation. The failure lives in the space between them, and it is silent, because neither component is in a position to notice the other exists.

Every integration you write has a few of these. They do not show up in either project's issue tracker, because from each side nothing is wrong. They show up in yours.

The practical takeaway: when you wrap a process that speaks a protocol on stdout, audit everything in the chain for the assumption that stdout is for humans. Log prefixes, colour codes, progress bars, and yes, secret masking.

https://www.npmjs.com/package/mcp-secrets-runner

Top comments (0)