seekrit
← all posts

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

CredentialSetup
Model key, local development1
Model key, your own deployment2, 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 reach4, 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.