Skip to content
Go back

When the Tool Doesn't Know About the Vault

TL;DR


The fourth post in this series showed op read in code — a subprocess call that reaches into the vault at runtime and returns a secret. The pattern works. But it has a cost: the code knows about 1Password. You’ve baked a vault dependency into the tool.

There’s a cleaner approach, and it changes what “credential-free tool” actually means.

The distinction

op read is code that fetches from the vault:

api_key = subprocess.check_output(
    ["op", "read", "op://Personal/Some-API/credential"]
).decode().strip()

The function knows the vault exists. It imports subprocess. It constructs the op:// reference. If 1Password isn’t installed, the code breaks.

op run is different. It wraps a process at launch, resolves op:// refs in the environment before the process starts, and the process reads a normal env var:

api_key = os.environ["API_KEY"]

No subprocess. No op:// reference in the code. No vault dependency at all. From the tool’s perspective, it received a credential in the environment. Where that credential came from is not its problem.

What this looks like in a real config

Three places in a working agent setup where op run replaces in-code credential fetching:

1. ~/.claude/settings.json

{
  "env": {
    "GITLAB_TOKEN": "op://Personal/GitLab-token/credential"
  }
}

Claude Code reads GITLAB_TOKEN as a normal env var. No code in Claude Code knows about 1Password. The op:// reference sits in the config file; op run resolves it when the session starts.

2. An MCP server you didn’t write

Before, wrapper code fetched its own credentials:

# wrapper.py — knew about the vault
import subprocess

def _resolve_secret(ref: str) -> str:
    return subprocess.check_output(["op", "read", ref]).decode().strip()

api_key = _resolve_secret(os.environ.get("OP_SECRET_REF"))

After — wrap the server launch with op run and put references in the env block:

"some-mcp-server": {
  "command": "/opt/homebrew/bin/op",
  "args": ["run", "--", "/path/to/mcp-server-binary"],
  "env": {
    "API_KEY": "op://Personal/Some-API/credential"
  }
}

The server reads os.environ["API_KEY"]. It doesn’t know what populated it. You can delete the wrapper’s _resolve_secret() entirely.

3. The agent launch alias

alias claude='op run -- claude'

This wraps the entire Claude Code process. All op:// refs in settings.json env vars are resolved before the session starts. One Touch ID prompt at launch covers every credential for the entire session.

The three op tools and when to use each

ToolWhat it doesWhen to use
op runWraps a subprocess, resolves op:// refs in env at launchMCP servers, CLIs, long-running tools, any process you launch via config
op readPrints a single secret to stdoutScripts that need a value inline; code that actively manages credentials
op injectResolves op:// refs in a template fileOne-time config generation — writes plaintext, delete the output after

The practical cut: op read is for code that knows it’s talking to a vault. op run is for tools that shouldn’t have to know.

This matters most at the MCP server layer. A server you install, configure, and run — but didn’t write — has no vault dependency and never should. op run in the command wrapping that server means it gets credentials without you forking the code or adding vault support to something someone else maintains.

CI and deploy scripts

GitHub Actions can load AWS credentials the same way — store the full op:// path in a repository secret, let 1password/load-secrets-action resolve it at workflow runtime. The workflow YAML references the secret name, not the vault item ID. Same pattern as local op run: the tool sees env vars, not vault paths.

For local deploy scripts, wrap aws calls behind op run or export AWS_PROFILE from your own environment — the vault stays out of the script body.

The open question

The op run pattern works cleanly for processes you launch via config. It doesn’t work for credentials that need to be rotated mid-session, or for tools that acquire tokens themselves and need the underlying secret only once to do that. For those cases, the in-code op read pattern is still the right call.

What I haven’t resolved: whether there’s a clean way to handle credential rotation for a long-running process that started with op run. If the token expires and the process needs to re-acquire it, the op:// reference was already resolved at launch — it doesn’t re-resolve. The options are either re-launching the process (which op run handles cleanly since the env gets fresh values) or having the code fall back to op read for the renewal path. Neither is fully clean. The right answer probably depends on whether the credential is the secret or the derived token.

So what

The op run pattern is the correct default for anything you configure but don’t write. An MCP server config, a CLI alias, a CI workflow, a long-running tool launched via JSON — wrap the command with op run --, put op:// references in the env block, and the tool reads a normal env var. The vault is transparent to it.

op read belongs in code you’re writing that actively manages its own credential lifecycle. Everything else is a configuration problem, and op run solves it without touching the tool.

The vault stays invisible to the tool. That’s the point.


This is the sixth post in a series on 1Password as infrastructure. Start from the first post.


Share this post on:


Previous Post
How This Site Is Built
Next Post
The Same Pattern, From Solo Builder to Enterprise Scale