Managing secrets in CrewAI crews
Frameworks
CrewAI reads OPENAI_API_KEY and OPENAI_BASE_URL from the environment. The
shortest fix for a crew that runs on a .env file is to inject the values at
launch and delete the file:
seekrit secrets import .env --app crew --env development && rm .env
seekrit run -- crewai run
The crew's code does not change. The credentials exist only in the child process. This post covers that setup and the two stronger ones, and explains why a crew needs the stronger ones more than a single agent does.
Why is a crew different from one agent?
A crew is several agents in one Python process. They share one environment.
Every agent in the crew can use every credential any tool in the crew needs.
allow_delegation moves work between agents but does not move it across a
security boundary. If a research agent and a payments agent run in the same
crew, the research agent's process holds the Stripe key.
CrewAI's own docs say never to commit API keys and to use a secrets manager. That handles where the key is stored. It does not handle which agent can use it. The setups below handle both.
Setup 1: wrap the process
seekrit run -- python crew.py
seekrit run -- crewai run
Every secret granted to the token arrives as an environment variable. The
crew reads OPENAI_API_KEY the way it always did. No .env file exists on
disk, so there is nothing for a
file-reading agent or a postinstall script to
pick up.
Values resolved at start hold for the whole kickoff. A crew that runs for
hours does not see a rotated key until it restarts. If that matters, use
setup 3, where the proxy re-resolves on its own interval.
Setup 2: resolve in code
import seekrit
from crewai import LLM, Agent
secrets = seekrit.Client().resolve()
llm = LLM(model="openai/gpt-5.6-terra", api_key=secrets["OPENAI_API_KEY"])
researcher = Agent(role="Research Specialist", goal="Analyse", llm=llm)
Use this when the crew runs inside a server you do not launch with a wrapper, such as a queue worker. Resolve once at boot. Do not resolve per task.
Setup 3: the process never holds the key
Point the LLM at the seekrit proxy and give it
a placeholder instead of a key:
from crewai import LLM
llm = LLM(
model="openai/gpt-5.6-terra",
base_url="http://127.0.0.1:8080/openai/v1",
api_key="{{seekrit:OPENAI_API_KEY}}",
)
The proxy swaps the placeholder for the real key on the way out. The rule list is default-deny and can bound methods and paths per route:
# 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"]
[[route]]
prefix = "/github"
upstream = "https://api.github.com"
allow = ["GITHUB_TOKEN"]
methods = ["GET", "POST"]
paths = ["/repos/*/*/issues", "/repos/*/*/issues/*"]
The second route is the reason to do this for a crew. The GITHUB_TOKEN can
read and create issues. It cannot delete a repository, push a commit, or
change a webhook, even if a prompt-injected agent asks it to. The methods
and paths fields are how you stop a research agent from performing a write
the crew's task graph never intended.
Without running the proxy
CrewAI has a hook for this of its own: an LLM takes an interceptor, and
CrewAI installs it as that provider client's HTTP transport.
from crewai import LLM
from seekrit.crewai import SeekritInterceptor, ensure_intercepted
llm = ensure_intercepted(
LLM(
model="openai/gpt-5.6-terra",
api_key="{{seekrit:OPENAI_API_KEY}}",
interceptor=SeekritInterceptor(allow={"api.openai.com": ["OPENAI_API_KEY"]}),
)
)
Because that is per LLM instance rather than per process, two agents in one
crew can hold two different credentials. It still shares the crew's address
space, so it remains a weaker boundary than the proxy — the
in-process injection page sets out the
trade-off.
ensure_intercepted is worth the line. CrewAI routes an LLM to a native
provider or to its optional LiteLLM fallback based on the model string, and the
fallback accepts interceptor and never calls it — so a model string that falls
through would send the literal placeholder to your provider.
Setup 4: bound what each agent may inject
The hard case in the section above is two agents that should not have the same
reach. A crew is one process, so by default every agent holds every credential
and delegation moves work between agents that are all equally privileged.
CrewAI's before_llm_call and before_tool_call hooks carry the agent, so that
bound is now expressible inside the crew:
from seekrit.crewai import SeekritCredentials
with SeekritCredentials(
agents={"Researcher": ["OPENAI_API_KEY"]},
tools={"refund": ["OPENAI_API_KEY", "STRIPE_SECRET_KEY"]},
):
crew.kickoff()
The Researcher may spend the provider key and nothing else — including through
a tool, because the two lists are intersected and the narrower wins. Both are
exhaustive, so an agent you add tomorrow starts with nothing rather than with
everything. Pair it with require_scope=True on the interceptor: CrewAI
deliberately swallows exceptions raised inside a hook, so the refusal has to come
from the interceptor rather than the hook.
What this still is not: a process boundary. Both agents' credentials are resolvable by the same token in the same address space, and it is seekrit's code deciding which one to inject. For agents that must not be able to read the same secrets at all, that is still two processes with two tokens, or the proxy issuing each a session ticket.
Which setup for which credential
| Credential | Setup |
|---|---|
| Model key, local development | 1 |
| Model key, your own deployment | 2, or 3 if the crew runs generated code |
| A tool credential (GitHub, Stripe, a database) | 3, with methods and paths set |
| Two agents that must have different reach | 4, or 3 with session tickets — and two processes if they must not share a token |
Setup
Three commands put the keys in an environment, mint a token bound to it, and
export SEEKRIT_TOKEN. The
frameworks guide has
them. Values are encrypted in the browser or CLI before they are stored, so
the service holding them cannot read them. The
full CrewAI page, including the LiteLLM fallback and the gotchas, is at
/docs/guides/frameworks/crewai.