← All posts

How to Keep API Keys Out of an AI Coding Agent

A practical guide to preventing API keys from leaking into Claude Code, Cursor, and Cline — covering common failure modes, vault patterns, and incident response.

AI coding agents are productive precisely because they read everything: your source tree, your shell output, your chat history. That same property is what makes them dangerous around credentials. If an API key crosses an agent’s context window — even once — you have to assume it has been logged, synced, embedded, or echoed back into a completion somewhere you cannot fully audit.

This article walks through how keys actually leak in practice, the mental model that prevents most of those leaks, and the concrete workflow for keeping secrets out of the model entirely.

Why API Keys and AI Coding Agents Are a Dangerous Combination

A traditional secret leak has a small blast radius: a key ends up in a git commit, you rotate it, you move on. Agent-mediated leaks are messier. When a key enters a prompt or a file the agent reads, it can end up in:

  • Provider-side conversation logs (Anthropic, OpenAI, Cursor’s backend).
  • Local cloud-synced workspace state and shell transcripts.
  • Embeddings and vector indexes built over the project.
  • Future completions, where the model helpfully reproduces the value as an example.

Auth0’s security team frames the underlying issue plainly: hardcoding API keys into AI agents is a security risk, and manual token management is the wrong primitive for systems that hand credentials to non-deterministic consumers (Auth0). Delinea makes a parallel point about chat interfaces specifically — secrets pasted into prompts should be treated as compromised the moment they hit the wire (Delinea).

The Three Ways Keys Usually End Up Exposed

In practice, almost every leak I have seen with Claude Code, Cursor, or Cline traces back to one of three patterns:

  1. Hardcoded in source files the agent reads. A config.py or constants.ts with a literal key. The agent indexes it, summarizes it, and may quote it back when asked about configuration.
  2. Pasted directly into the chat prompt. “Here’s my Stripe test key, write a script that…” The key is now in the provider’s conversation log and any local transcript file.
  3. Stored in .env files inside the project root. Agents like Cursor and Cline read files in the working directory as part of their normal context-gathering. A .env at the root is functionally equivalent to a hardcoded constant from the model’s perspective — once it is read, the values can land in the agent’s context, prompts, and any provider-side logs. (For Claude Code specifically, we cover the right .env-replacement pattern in detail here.)

Delinea’s recommendation is to replace all three patterns with a controlled, audited retrieval mechanism rather than relying on developer discipline (Delinea).

The Right Mental Model: Name-Based Access, Not Value-Based Access

The single most useful shift is moving from value-based to name-based access.

  • Value-based access (the default today): the agent sees the literal string sk-live-abc123... and passes it to a tool. Anything in the agent’s context can leak.
  • Name-based access: the agent only ever knows a secret exists under the identifier STRIPE_SECRET_KEY. It asks the runtime to execute a command with that name injected. The value materializes inside a subprocess and is destroyed when the process exits.

The agent’s job is orchestration, not custody. Once you internalize that, every solution that follows is just plumbing.

How to Configure OpaqueVault So the Agent Never Sees the Value

OpaqueVault is built around the name-based access model. The workflow has four steps:

  1. Encrypt client-side. You run ov secret set STRIPE_SECRET_KEY and paste the value into a local TTY prompt. The CLI derives an encryption key from your master password, encrypts the value in memory, and only ever writes ciphertext to disk or sends ciphertext over the network.
  2. Store under a name. The encrypted blob is stored in the vault, indexed by the name you chose. The plaintext never leaves your machine in clear form.
  3. Hand the agent a reference. In your agent’s instructions or project config, you tell it: “To call Stripe, use ov run --secrets STRIPE_SECRET_KEY -- <your command>.” The agent learns the name, not the value.
  4. Subprocess injection at runtime. When the agent invokes that command, a local OpaqueVault process — not the agent — decrypts the secret in memory and passes it as an environment variable to the spawned subprocess. The agent process receives the subprocess’s stdout, stderr, and exit code; it does not receive the plaintext value through any vault API. An output interceptor also scans stdout/stderr for the literal value (and common encodings of it) before the result is returned to the agent.

This rests on a few specific guarantees worth naming. OpaqueVault is zero-knowledge by design: the remote server stores only ciphertext it cannot decrypt — the master password and the derived key encryption key (KEK) never leave your machine. Symmetric encryption uses AES-256-GCM, the KEK is derived from your master password with Argon2id (memory-hard, tuned to 64 MiB). Encrypted DEK envelopes that the server delivers to the client are additionally wrapped with a post-quantum hybrid KEM (X25519 + ML-KEM-768), so a future quantum adversary that records traffic today cannot decrypt it later. Every secret retrieval is recorded in an audit log keyed by HMAC-hashed references — you get attribution without exposing the secret names themselves to whoever can read the log.

One honest caveat: the interceptor is a hygiene check, not a security boundary. A subprocess you have chosen to inject secrets into is, by construction, trusted with those secrets — it can transform, encode, or exfiltrate them in ways no output filter can reliably catch. The architecture moves the decryption boundary below the agent, so the agent never has to be trusted with plaintext, but you still need to choose carefully what you hand secrets to.

What the Agent Experience Actually Looks Like

Developers reasonably worry that any vault layer adds friction that the agent will route around. In practice, the experience inside Claude Code, Cursor, or Cline looks like this:

  • The agent decides it needs to hit the Stripe API.
  • It runs ov run --secrets STRIPE_SECRET_KEY -- node scripts/refund.js.
  • The script executes successfully. The agent sees stdout and exit code.
  • The conversation contains the command and its output, but no key material.

There is no extra prompt round-trip, no copy-paste step, and no “please provide your API key” dead end. The agent treats secret names the way it already treats file paths — as opaque handles it can pass to tools.

Broader Secrets Hygiene Practices That Complement a Vault

A vault is necessary but not sufficient. The practices that pair with it:

  • Rotate keys on a schedule, not on incident. Short-lived credentials limit the damage of any leak that does occur. If your provider supports it, prefer scoped, time-bounded tokens over long-lived secrets.
  • Scope to least privilege. The key the agent uses for a refund script should not also be able to issue payouts. Separate names in the vault for separate scopes makes this enforceable.
  • Keep .env files out of agent working directories. If you must use .env for legacy tooling, place it outside the indexed project root and symlink only into the subprocess environment. Better: delete it and let the vault be the single source.
  • Use the vault’s audit log. Delinea specifically calls out audited retrieval as the differentiator between a vault and a glorified config file. You want a record of which secret name was requested, by which process, at what time. When something looks off, that log is how you find it.
  • Treat the agent as an untrusted client. The agent should never directly handle sensitive credentials, even when you trust the developer driving it. Name-based access enforces this at the vault layer; you do not have to rely on the developer remembering.

What to Do If a Key Has Already Been Exposed in Agent Context

If you realize a key has hit a prompt, a chat log, or an indexed file, treat it as compromised. The checklist:

  1. Revoke and rotate immediately. Do this before you investigate scope. The window between exposure and rotation is the entire risk surface.
  2. Audit provider logs for use of the exposed key. Most API providers (Stripe, OpenAI, AWS) expose recent request logs. Look for unfamiliar IPs, user agents, or call patterns.
  3. Check synced conversation history. Cursor, Claude Code, and Cline all have varying degrees of cloud sync. Delete or purge any transcripts containing the value. Assume provider-side copies persist regardless.
  4. Search embeddings and indexes. If you have a local vector store over your codebase, rebuild it after removing the file containing the key. Embeddings of secrets are still secrets.
  5. Migrate to a name-based workflow. Recurrence is the real failure. Move the rotated key into a vault, give the agent the name only, and remove the original storage location.

The pattern that distinguishes teams who get this right from teams who keep getting paged is unglamorous: they took the agent out of the custody chain entirely, then they stopped relying on memory. Name-based access, subprocess injection, and an audit log do most of the work. The agent stays productive. The keys stay yours.

Zero-knowledge secrets for AI agents

Keep credentials out of Claude's context window.

OpaqueVault encrypts secrets client-side and injects them into subprocesses — your AI agent never sees the plaintext value.

Get started free → ← More posts