Skip to content

Secrets management — 1Password/doppler + scoped tokens + quarterly audit

Q22 · Operations & hygiene How do you manage API keys / secrets in MCP and hooks?

Max-score answer: “Secret manager (1Password CLI, Doppler) + scoped tokens, quarterly audit.”

Why it matters: Agents that can read your filesystem can read your secrets. Narrow what they can find.

The threat model around secrets fundamentally changed the moment LLM agents got generic shell, filesystem, and HTTP access. In 2025, the worst case for a leaked .env was a careless git push to a public repo. In 2026, the worst case is an agent that reads your filesystem on every turn, ingests every plaintext OPENAI_API_KEY=sk-… it can find, and then quotes it back in a stack trace, a debug log, or — increasingly — a prompt-injected response to a poisoned web page or MCP tool output. GitGuardian’s State of Secrets Sprawl report logged 28.6 million new secrets leaked into public GitHub commits across 2025, a 34% year-over-year jump, and the report’s authors explicitly attribute the acceleration to agent workflows that copy local context into shareable transcripts, GitHub issues, and Discord screenshots without redaction. Researchers at API Stronghold separately found over 21,000 publicly exposed agent runtimes (OpenClaw, AutoGPT-style stacks, langgraph deployments) leaking API keys, OAuth tokens, and full conversation histories on the open internet through trivial misconfigurations — the agent equivalent of S3 buckets in 2017.

The mechanism is mundane and unavoidable on any modern stack. Your agent has filesystem read. It reads .env. Anything in .env is now in its working context, and from there it can end up in: a transcript you share with a colleague, a Sentry breadcrumb, a console.log debugging session, a Slack paste, a stack trace the model includes in its self-analysis, a tool call response sent to a third-party MCP server you don’t fully trust, or — the new 2026 hazard — a prompt-injection payload embedded in a web page or PDF that asks the agent to exfiltrate environment variables. None of those paths require malice, none of them require the agent to be jailbroken, and none of them are blocked by your model provider. The only durable defense is to never have the plaintext secret on disk in the first place, and to make sure any token the agent can fetch is scoped narrowly enough that compromising it is non-catastrophic.

That’s why Q22 sits in the Operations & hygiene section and why the max-score answer demands three concrete things together — a secret manager, scoped tokens, and a quarterly audit. Each one alone is partial. A secret manager without scoped tokens still gives the agent a master key when it runs. Scoped tokens without a manager still leak in plaintext through .env. Both without an audit decay into “we set it up in Q1 and never looked again” within two quarters, which is functionally the same as not having it.

A max-score Q22 setup has no production secret in plaintext anywhere on your machine outside the secret manager’s encrypted vault. Not in ~/.zshrc, not in .env, not in .env.local, not in .envrc, not in a Notes.app scratchpad, not in a claude/settings.json. The secret manager (1Password CLI, Doppler, or an equivalent like HashiCorp Vault or AWS Secrets Manager) is the only source of truth, and every process that needs a secret — your dev server, your MCP server, your Claude Code session, a Codex CLI background agent, a GitHub Actions runner — pulls it through the manager at the moment it starts. The plaintext exists only in the process’s environment for the lifetime of that process, never on disk.

Every token the agent can reach is scoped to the minimum required permission and lifetime. Your GitHub token is a fine-grained PAT scoped to one repo and one set of permissions (e.g. contents:write + pull-requests:write on developer-toolkit), not a classic PAT with repo scope across your entire account. Your Cloudflare token is a scoped API token for one zone with one set of resource permissions, not the global API key. Your OpenAI / Anthropic key is a project-scoped key with a monthly hard cap, not a workspace-admin key. Each agent or MCP server gets its own token — not a shared one — so that if a transcript leaks, you know exactly which token to rotate and the blast radius is small.

You run a quarterly audit that’s actually on a calendar. Every three months you open the secret manager, list every active secret, and answer two questions per entry: “is this still in use?” and “is the scope still right?” You rotate anything older than 90 days that doesn’t have a strong reason to persist. You review each scoped token’s last-used date and revoke anything dormant for >30 days. You search your repos and dotfiles for accidental plaintext that crept in between audits (gitleaks detect, trufflehog filesystem ., or op inject --dry-run on every config file). You log the audit somewhere (a secrets-audit-YYYY-Q*.md in your personal repo, a calendar event with the checklist) so next quarter’s audit has the history to compare against.

Anything less — “I have a 1Password vault but my MCP servers still read .env”, “I rotate keys when something feels off”, “I’ll audit when I have time” — is a 1- or 2-point answer.

1Password CLI (op run --env-file) injection

Section titled “1Password CLI (op run --env-file) injection”

The 1Password CLI (op) shipped first-class secret references in 2023 and they’re now the most-used pattern in 2026 for agent + MCP workflows. Instead of a plaintext .env, you keep an .env file with op:// references and run your command through op run:

Terminal window
# .env (committable — no secrets in it)
OPENAI_API_KEY=op://Personal/openai/credential
ANTHROPIC_API_KEY=op://Personal/anthropic/credential
GITHUB_TOKEN=op://Work/github-developertoolkit/token
CLOUDFLARE_API_TOKEN=op://Work/cloudflare-dt-prod/token
# Run anything — npm, MCP server, claude code — through op run
op run --env-file=.env -- npm run dev
op run --env-file=.env -- claude
op run --env-file=.env -- npx @modelcontextprotocol/server-github

op run resolves the references at process start, injects the plaintext into the child process’s environment only, and never writes them to disk. The child sees normal environment variables; the model running inside the child sees the same; but the file on disk is safe to commit, share in a screenshot, or hand to a teammate. As of 2026 there’s also an open request — already implemented in nightly builds — to let Claude Code resolve op:// references directly inside settings.json’s env block, removing the need for wrapper scripts entirely. The 1Password developer docs publish a secret references guide that covers nested fields, sections, and how to reference TOTP codes, SSH keys, and connect tokens with the same syntax.

For MCP servers specifically the convention is to wrap the launch command in your MCP client config:

{
"mcpServers": {
"github": {
"command": "op",
"args": ["run", "--env-file=/Users/you/.config/mcp/github.env", "--",
"npx", "-y", "@modelcontextprotocol/server-github"]
}
}
}

The MCP server gets GITHUB_TOKEN in its environment when it starts, the model never sees the value, and the only on-disk artifact is op:// references — which leak nothing if the file is exposed.

Doppler is the strongest centralized alternative for teams that want secrets out of personal vaults and into a shared, audited control plane. The Doppler CLI follows the same “wrap the command” pattern but pulls from a hosted, role-based-access project:

Terminal window
doppler run --project developer-toolkit --config dev -- npm run dev
doppler run --project ai-tools --config dev -- npx @modelcontextprotocol/server-github
doppler run --project developer-toolkit --config dev -- claude

Doppler’s selling point versus 1Password is centralization: secrets live in one project per app, with environments (dev, staging, prod), role-based access for teammates, and a full audit log of who fetched what and when. The Doppler blog published an LLM security writeup in 2026 that explicitly addresses secret leakage across agents and prompts — worth reading before you commit to a pattern, because it makes the case for never letting the model see the secret value at all, only the reference. Doppler also has first-class Cloudflare Workers integration: doppler secrets download --no-file --format env | wrangler secret put … is the canonical way to keep wrangler secrets in sync with the Doppler source of truth instead of wrangler secret put from memory.

The trade-off is operational overhead: Doppler is a hosted dependency, it costs money above the free tier, and your dev workflow now has an extra failure mode (Doppler down → agent can’t start). For a solo developer with a 1Password subscription already, op run is usually the right pick. For a team of 3+ where rotation discipline matters more than personal convenience, Doppler wins.

Cloud-native (Vault, AWS Secrets Manager, Cloudflare Secrets Store)

Section titled “Cloud-native (Vault, AWS Secrets Manager, Cloudflare Secrets Store)”

For server-side secrets — anything an MCP server or background agent fetches in production — the canonical 2026 pattern is to keep secrets in the cloud manager native to your platform and never sync the plaintext to local disk. On Cloudflare Workers (this repo’s runtime), the path is wrangler secret put POLAR_ACCESS_TOKEN followed by reading env.POLAR_ACCESS_TOKEN in your code; the plaintext exists only inside the encrypted Cloudflare store and the worker runtime — your local machine never holds it. On AWS, Secrets Manager plus IAM scoped to the specific secret ARN is the equivalent. On HashiCorp Vault, dynamic database credentials with TTLs let you grant an agent a 30-minute-lived DB token that auto-revokes — which is the gold standard for any agent that needs read/write database access.

The fast.io writeup “AI Agent Secrets Management: Best Practices for 2026” makes the case that the real defense for agentic workloads is dynamic, short-lived credentials (Vault style), not just better static-secret hygiene. The reasoning: an agent that gets a fresh 15-minute token from Vault at the start of each task can leak that token freely into transcripts because by the time anyone reads the transcript, the token is dead. This is a stronger guarantee than “the token in your vault is encrypted”, because it removes the attack window entirely.

Scoped tokens (GitHub fine-grained PAT, Cloudflare scoped API tokens)

Section titled “Scoped tokens (GitHub fine-grained PAT, Cloudflare scoped API tokens)”

The other half of the answer is that the secret itself should be small. A leaked fine-grained GitHub PAT scoped to contents:write on one repo is mildly bad; a leaked classic PAT with full repo scope across your whole account is catastrophic. The 2026 default for every token in an agent’s hands should be: one specific resource, one specific permission, one specific expiry.

  • GitHub: fine-grained PATs (Settings → Developer settings → Personal access tokens → Fine-grained tokens). Scope to the exact repo(s) the agent or MCP server touches, pick the minimum permissions (contents, pull-requests, issues as needed), and set an expiry of 30 or 90 days, not “no expiration”. GitHub App tokens are even better for team setups because they’re auto-rotated.
  • Cloudflare: scoped API tokens, not the global API key. Each token gets specific account/zone permissions (Workers Scripts: Edit, Account Settings: Read, etc.) and is restricted to the resources it actually needs. The global API key should not exist on any developer machine in 2026 — it’s a relic of the 2018 API.
  • OpenAI / Anthropic: project-scoped API keys with monthly usage limits. Each project (e.g. developer-toolkit-prod, cli-tools-personal) gets its own key, so spend is attributable per agent setup and rotation is local.
  • Stripe / Polar / Resend / PostHog / Sentry: restricted keys wherever the provider supports them. Stripe’s restricted keys let you scope to read-only customers or write-only payment_intents. Resend offers per-domain API keys. PostHog has personal API tokens that can be scoped to specific projects.

The pattern across all of them: assume the token will leak. Then design so that when it does, the damage is one repo, one zone, one project, or one bounded action.

The audit is what keeps the system from rotting. Without it, scoped tokens accumulate, dormant ones stay enabled, and “temporary” elevated-permission tokens become permanent. The 2026 standard is to put a recurring 90-day calendar event titled “Secrets audit — Q*” and run a fixed checklist:

  1. List everything. op item list --vault Personal and op item list --vault Work (or doppler secrets --project … --config …). Compare against last quarter’s list. Investigate every new entry that doesn’t have an obvious owner.
  2. Check last-used. For every API token, check the provider’s dashboard for last-used timestamp (GitHub, Cloudflare, OpenAI, Anthropic, Stripe — they all expose this). Anything dormant for >30 days: revoke. You can always re-create.
  3. Rotate the long-lived. Anything older than 90 days with no good reason: rotate. Update the secret manager entry, restart the dependent processes, and revoke the old token at the provider.
  4. Re-check scope. For each surviving token, re-verify the scope matches current usage. If a token still has repo scope but the agent only touches one repo, downgrade to fine-grained.
  5. Scan for plaintext drift. Run gitleaks detect --source . --no-banner across every repo on your machine and trufflehog filesystem ~/ --no-update --only-verified (or equivalent) to catch plaintext that snuck in. Fix what’s found, then commit a .gitleaks.toml allowlist for known-safe references (op://… strings, example values in docs).
  6. Log the audit. Append a one-paragraph summary to ~/personal/secrets-audit-YYYY-Q*.md: what changed, what got rotated, what got revoked, what surprised you. Next quarter’s audit reads this file first.

The first audit will take 2–3 hours. Each subsequent one drops to under 30 minutes because the bulk of the work is rotation + revocation, not setup.

Step-by-step: rolling out a secrets workflow

Section titled “Step-by-step: rolling out a secrets workflow”
  1. Pick a manager and commit to it for 90 days. If you already have 1Password and you’re solo or two-person: op run is the path of least resistance. If you’re a team of 3+ or you want centralized audit logs out of the box: Doppler. Don’t run both — the dual-source-of-truth problem will burn you. Install the CLI (brew install 1password-cli or brew install dopplerhq/cli/doppler) and verify auth before touching any existing .env.

  2. Inventory every plaintext secret on your machine. Run gitleaks detect --source ~ --no-banner --no-git (or trufflehog filesystem ~/ --no-update) to surface every plaintext key in your home directory. Expect 30–100 hits the first time. Open the report, ignore false positives, and put every real key into the manager — naming convention: <service>/<environment>/<purpose> (e.g. openai/personal/cli, github/work/developertoolkit-pat).

  3. Replace .env files with reference files. For every project’s .env, replace plaintext values with op:// references (1Password) or remove them entirely and switch to doppler run (Doppler). The new .env is committable — verify by running cat .env and confirming you’d be happy to paste it in a screenshot. Update .gitignore to keep the old .env.local form ignored as a backstop, but the new ref-based file should be safe to track.

  4. Wrap every long-running process in the manager. Your dev server: op run --env-file=.env -- npm run dev. Your Claude Code session: op run --env-file=.env -- claude (or the Doppler equivalent). Every MCP server in your client config: prepend the manager command. The principle is one place injects secrets; everywhere else reads them from the environment without knowing where they came from.

  5. Downgrade every token to scoped. Open each provider’s dashboard. GitHub: convert classic PATs to fine-grained PATs scoped to specific repos. Cloudflare: replace the global API key with per-purpose scoped tokens. OpenAI / Anthropic: split a single workspace key into project-scoped keys with monthly caps. As you create each new scoped token, store it in the manager, revoke the old one immediately, and update wherever it’s referenced.

  6. Give each MCP server and agent its own token. Don’t share GITHUB_TOKEN between Claude Code, Codex CLI, and the @modelcontextprotocol/server-github MCP. Create three fine-grained PATs with the same scope but different names — that way if one transcript leaks, you rotate one token and you know exactly which agent surfaced it.

  7. Add gitleaks to your pre-commit hook. brew install gitleaks and add gitleaks detect --staged --no-banner to your repo’s pre-commit hook (or .husky/pre-commit). This is your safety net for the inevitable day you forget and paste a real key into a config file by accident. CI should run the same check on push.

  8. Put a quarterly audit on the calendar. Recurring 90-day event, 60 minutes, titled “Secrets audit — Q*”. The first run is the one you’ll resist; do it anyway. Walk through the six-step checklist above. After the first one you’ll find your habits self-correct because you don’t want to spend an hour next quarter rotating things you could have rotated quickly.

  • .env checked into the repo. Still the #1 leak vector in 2026 according to GitGuardian. Run git log -p -- '.env' across every repo you’ve ever touched and rotate anything you find — Git history is forever, so “I removed it later” doesn’t help. The fix is structural: switch to ref-based .env files and put a gitleaks hook on pre-commit so future drift is caught at the source.
  • Hardcoded keys in source files. A literal const apiKey = "sk-…" in src/lib/openai.ts is much worse than .env because it propagates through every clone, every CI artifact, and every search index the agent ever reads. If your agent surfaces it once in a transcript, assume it’s compromised. Rotate, then move to env-only access.
  • Broad-scope tokens. A classic GitHub PAT with full repo scope is a master key for every private repo you can see — including ones you don’t own. Cloudflare’s global API key is the same. OpenAI workspace keys with no spend cap let a runaway agent burn your whole credit card balance in an afternoon. Downgrade every one of these to its scoped equivalent. The 2018-era token model isn’t fit for 2026 agent workloads.
  • One token shared across agents. When the same GITHUB_TOKEN is used by Claude Code, Codex CLI, a GitHub Actions runner, an MCP server, and a Slack bot, a leak forces you to rotate everywhere at once and you can’t tell which agent leaked. One token per agent per environment is non-negotiable for a max-score setup.
  • Secrets manager as a copy-paste tool. Putting secrets in 1Password but still copying them into .env defeats the entire point. The plaintext is back on disk, the agent can still read it. The manager must be the runtime source, not a clipboard.
  • No expiry. “No expiration” on a PAT is a habit from 2018 and it should be illegal in your workflow. Default to 90 days, even for keys you think you’ll always need. The rotation forces the audit.
  • Skipping the audit because “nothing’s changed”. Nothing visible has changed; under the surface, six tokens are now older than 180 days, two are dormant, one provider added new permission scopes you haven’t picked up. The audit’s value compounds when you run it. Skipping it for one quarter doesn’t double next quarter’s work — it triples it.
  • Trusting MCP server output blindly. A third-party MCP server you don’t fully audit can call process.env and exfiltrate everything in your environment. Treat MCP servers like browser extensions: minimum count, minimum permissions, prefer first-party or well-reviewed ones, give each its own scoped token, and never run them with your master .env injected.
  • You can name the secret manager you use without thinking (1Password CLI, Doppler, Vault, AWS Secrets Manager). It’s installed, you’re signed in, and it’s been working for >30 days.
  • Running grep -r "sk-" ~ 2>/dev/null (or any equivalent provider prefix) returns zero plaintext keys — only op:// references or empty .env files.
  • Every .env file in your repos is safe to commit. The actual secrets live in the manager.
  • Every MCP server in your client config launches via op run or doppler run — no plaintext keys in any MCP config JSON.
  • Your GitHub PATs are all fine-grained, each scoped to specific repos with explicit expiry dates ≤90 days. No classic PATs with repo scope exist on your account.
  • Your Cloudflare account has no token labeled “Global API Key” attached to any active workflow.
  • OpenAI and Anthropic keys are project-scoped with monthly spend caps. You know what each project costs per month within ±20%.
  • Each agent / MCP server / CI job uses its own token, not a shared one.
  • gitleaks runs on every pre-commit and on every CI run, with an allowlist for known-safe op:// references.
  • You have a recurring 90-day calendar event for “Secrets audit”, you ran it last quarter, and you have a secrets-audit-YYYY-Q*.md to prove it.
  • When you read your last week of agent transcripts, you don’t find a single plaintext credential quoted anywhere.