Use promo code BETATEST1 for full access
seekrit
← all posts

Managing secrets in LangGraph agents

Frameworks

A LangGraph agent gets its credentials the way any Python process does: from the environment, through the provider client under ChatOpenAI. That means the simplest setup is one command with no code changes —

seekrit run -- langgraph dev

— and it also means every credential the process holds is one os.environ read away from anything running in it. There are four shapes, in increasing order of how little the agent holds. Pick by what the key can do, not by what's convenient.

Why the environment stopped being a safe place

Two LangChain-specific reasons, on top of the general one (an agent can be talked into repeating what it can read):

  • CVE-2025-68664 (CVSS 9.3): langchain-core's load() defaulted secrets_from_env=True, so a crafted serialized payload could name any environment variable and read it back out. Patched in 1.2.5 / 0.3.81 — and a reminder that "in the environment" means "in reach of the next deserialization bug."
  • LangSmith is a second credential. LANGSMITH_API_KEY sits in the same environment as your provider keys and gets forgotten in exactly the way a main key doesn't.

Shape 1 — wrap the process

seekrit run -- python agent.py
seekrit run -- langgraph dev

Every granted secret arrives as an environment variable. Delete the .env afterwards — a file that still exists is a file an agent can still read.

Shape 2 — resolve in code

import seekrit

seekrit.Client().into_env()   # before constructing the model

Or skip os.environ entirely and pass the key explicitly:

secrets = seekrit.Client().resolve()
model = ChatOpenAI(model="gpt-5.6-terra", api_key=secrets["OPENAI_API_KEY"])

Shape 3 — the process never holds the key

Point the model at the proxy and pass a placeholder. The graph — including any code-generating node in it — holds a string that names a credential without being one:

model = ChatOpenAI(
    model="gpt-5.6-terra",
    base_url="http://127.0.0.1:8080/openai/v1",
    api_key="{{seekrit:OPENAI_API_KEY}}",
)

The proxy substitutes the real key on the way out, against a default-deny allowlist that also bounds methods and paths. No sidecar? The same substitution runs in-process as an httpx transport (seekrit.transport.SeekritTransport) — weaker, since it shares the process, but it unlocks shape 4.

Shape 4 — scope a key to one tool call

The keys that deserve the most care aren't the model's — a provider key buys tokens; a Stripe key bought by a tool call does something irreversible. LangChain 1.x middleware makes "which credentials may this tool use" a line of configuration:

from seekrit.langchain import SeekritCredentials

agent = create_agent(
    model=model,
    tools=[refund, search],
    middleware=[
        SeekritCredentials(
            model=["OPENAI_API_KEY"],
            tools={"refund": ["STRIPE_SECRET_KEY"]},
        ),
    ],
)

refund may substitute the Stripe key and nothing else; an unlisted tool gets an empty allowlist, so a prompt-injected search call can't reach a payment credential. The middleware's scope hook also resolves per-tenant off runtime.context, so one agent process serves many tenants without holding two tenants' keys at once.

Picking a shape

The credentialShape
Model key, local dev1 — wrap the process
Model key, your own deployment2, or 3 if the graph runs generated code
Anything a tool spends (Stripe, GitHub, a database)3 with methods/paths, or 4

The full walkthrough — including the LangSmith egress rule you'll need if the forward proxy denies unmatched hosts, and the LangGraph Platform case where seekrit syncs to the platform instead — is in the LangGraph guide. Setup is three commands, and the values are encrypted client-side before they're stored: the service holding them can't read them.