LangGraph
LangGraph agents get their credentials the way any Python process does — from the
environment, through the provider client underneath ChatOpenAI. So the
zero-code shape works as-is, and the proxy shape is two changed arguments.
Start here if seekrit is new: three
commands put the keys in
an environment, mint a token bound to it, and export SEEKRIT_TOKEN. A token
reads everything in its
environment, so
there is no per-key or per-framework setup to do before any of the below.
1. Wrap the process
seekrit run -- python agent.py
seekrit run -- langgraph dev
Every granted secret arrives as an environment variable in the child process:
OPENAI_API_KEY, ANTHROPIC_API_KEY, TAVILY_API_KEY, your database URL, all
of it. Delete the .env file afterwards — seekrit run overlays one if it's
there, and a file that still exists is a file an agent can still read.
2. Resolve in code
import seekrit
from langchain.agents import create_agent
seekrit.Client().into_env() # existing os.environ wins by default
agent = create_agent(
model="gpt-5.6-terra",
tools=[get_weather],
system_prompt="You are a helpful assistant",
)
result = agent.invoke({"messages": [{"role": "user", "content": "Weather in SF?"}]})
into_env() before you construct the model, since ChatOpenAI reads the key at
construction time. If you'd rather not touch os.environ:
from langchain_openai import ChatOpenAI
secrets = seekrit.Client().resolve()
model = ChatOpenAI(model="gpt-5.6-terra", api_key=secrets["OPENAI_API_KEY"])
3. Never hold the key
Point the model at the proxy and pass a placeholder:
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="gpt-5.6-terra",
base_url="http://127.0.0.1:8080/openai/v1",
api_key="{{seekrit:OPENAI_API_KEY}}",
)
# seekrit-proxy.toml
listen = "127.0.0.1:8080"
[[route]]
prefix = "/openai"
upstream = "https://api.openai.com"
allow = ["OPENAI_API_KEY"]
methods = ["POST"]
paths = ["/v1/chat/completions", "/v1/embeddings"]
Now the graph can be built by anyone, including a code-generating node, and the
key still cannot leave toward anywhere but api.openai.com.
Without running the proxy
ChatOpenAI takes an http_client, so the substitution can happen in your
process instead of a sidecar:
import httpx
from langchain_openai import ChatOpenAI
from seekrit.transport import AsyncSeekritTransport, SeekritTransport
allow = {"api.openai.com": ["OPENAI_API_KEY"]}
model = ChatOpenAI(
model="gpt-5.6-terra",
api_key="{{seekrit:OPENAI_API_KEY}}",
http_client=httpx.Client(transport=SeekritTransport(allow=allow)),
http_async_client=httpx.AsyncClient(transport=AsyncSeekritTransport(allow=allow)),
)
Weaker than the proxy, since it runs in the same process as the graph. In-process injection sets out the trade-off — and on LangChain it unlocks the next shape, which nothing else can express.
4. Scope a key to one tool
LangChain 1.x middleware wraps every model call and every tool call, so "which credentials may this tool use" becomes a line of configuration:
pip install 'seekrit[langchain]'
from langchain.agents import create_agent
from seekrit.langchain import SeekritCredentials
from seekrit.transport import SeekritTransport
transport = SeekritTransport(
allow={
"api.openai.com": ["OPENAI_API_KEY"],
"api.stripe.com": ["STRIPE_SECRET_KEY"],
},
require_scope=True,
)
agent = create_agent(
model=model, # built with http_client=httpx.Client(transport=transport)
tools=[refund, search],
context_schema=Context,
middleware=[
SeekritCredentials(
scope=lambda ctx: {"tenants": ctx.tenant},
model=["OPENAI_API_KEY"],
tools={"refund": ["STRIPE_SECRET_KEY"]},
),
],
)
The refund tool may substitute the Stripe key and nothing else. search may
substitute nothing at all — when tools is given it is exhaustive, so an
unlisted tool gets an empty allowlist and a prompt-injected search call cannot
reach a payment credential.
scope also chooses which secrets to resolve, off runtime.context. One
agent process can serve many tenants without holding two tenants' keys at once,
and without a per-tenant model instance: the middleware sets an ambient scope and
the transport resolves against it, so the model — which builds its HTTP client
once — stays a single object.
Gotchas
- LangSmith is a second credential.
LANGSMITH_API_KEY(orLANGCHAIN_API_KEY) belongs in seekrit alongside the provider keys. If you run the forward proxy withunmatched_host_policy = "deny", tracing egress toapi.smith.langchain.comis a separate host and needs its own rule, or traces silently stop. langgraph devreloads,seekrit rundoesn't re-resolve. The injected values are fixed for the life of the process. Rotate a secret and you restart the dev server — which is the honest behaviour, not a bug to work around.- Tool credentials are the interesting ones. A provider key buys tokens; a
Stripe or GitHub key bought by a tool call does something irreversible. Those
are the ones to move to shape 3, with
methodsandpathsset — or to shape 4, scoped to the one tool that should have them. - An environment variable is in reach of a deserialization bug.
CVE-2025-68664(CVSS 9.3, December 2025) defaultedsecrets_from_env=Trueinlangchain-core'sload, so a crafted payload could name any environment variable and get its value back. It is patched in 1.2.5 / 0.3.81 — and it is also the argument for shapes 3 and 4, where there is nothing in the environment to name.