seekrit
Docs/LangGraph

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.

note

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 (or LANGCHAIN_API_KEY) belongs in seekrit alongside the provider keys. If you run the forward proxy with unmatched_host_policy = "deny", tracing egress to api.smith.langchain.com is a separate host and needs its own rule, or traces silently stop.
  • langgraph dev reloads, seekrit run doesn'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 methods and paths set — 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) defaulted secrets_from_env=True in langchain-core's load, 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.