TL;DR
op runwraps any process at launch and resolvesop://vault references into normal env vars — the tool readsos.environ["API_KEY"]and never knows 1Password exists.op readbelongs in code you wrote that actively manages its own credential lifecycle;op runbelongs everywhere else.- MCP servers you didn’t write get credentials without forking their code — wrap the launch command, put references in the env block, done.
- One shell alias (
alias claude='op run -- claude') covers every credential for an entire Claude Code session from a single Touch ID prompt. - Default to
op runfor anything you configure but don’t write.
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
| Tool | What it does | When to use |
|---|---|---|
op run | Wraps a subprocess, resolves op:// refs in env at launch | MCP servers, CLIs, long-running tools, any process you launch via config |
op read | Prints a single secret to stdout | Scripts that need a value inline; code that actively manages credentials |
op inject | Resolves op:// refs in a template file | One-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.