---
title: "When the Tool Doesn't Know About the Vault"
description: "op run wraps any process and resolves vault references as normal env vars — so the tool reads credentials without knowing 1Password exists."
canonical_url: "https://artificialcuriositylabs.ai/posts/1password-op-run-tool-transparent/"
md_url: "https://artificialcuriositylabs.ai/posts/1password-op-run-tool-transparent.md"
published_at: "2026-05-18T00:00:00.000Z"
tags:
  - "ai-native"
  - "infrastructure"
  - "patterns"
  - "build-log"
---

## TL;DR

- `op run` wraps any process at launch and resolves `op://` vault references into normal env vars — the tool reads `os.environ["API_KEY"]` and never knows 1Password exists.
- `op read` belongs in code you wrote that actively manages its own credential lifecycle; `op run` belongs 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 run` for anything you configure but don't write.

---

The [fourth post in this series](https://artificialcuriositylabs.ai/posts/1password-op-run-tool-transparent/1password-trust-anchor-auth-chain) 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:

```python
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:

```python
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`**

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

```python
# 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:

```json
"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**

```bash
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`](https://github.com/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](https://artificialcuriositylabs.ai/posts/1password-op-run-tool-transparent/1password-trust-anchor-auth-chain) 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 an eight-post series on 1Password as infrastructure. The [seventh](https://artificialcuriositylabs.ai/posts/1password-op-run-tool-transparent/1password-scope-the-vault-not-just-the-token) puts `op run` and scoped service accounts through their paces on a box that runs agents continuously, including the part where the boundary pushed back. The [eighth](https://artificialcuriositylabs.ai/posts/1password-op-run-tool-transparent/your-agent-doesnt-need-the-secret-just-the-result) covers why resolving the secret into the environment — what `op run` does — is still the floor, not the ceiling. [Start from the first post](https://artificialcuriositylabs.ai/posts/1password-op-run-tool-transparent/1password-builder-stack).*
