CrewAI
CrewAI reads OPENAI_API_KEY and OPENAI_BASE_URL from the environment, and its
LLM class takes api_key and base_url directly. All three shapes are short.
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 crew.py
seekrit run -- crewai run
from crewai import Agent, Task, Crew
researcher = Agent(role="Research Specialist", goal="Conduct comprehensive analysis")
search = Task(description="Research the topic: {topic}", expected_output="A report", agent=researcher)
crew = Crew(agents=[researcher], tasks=[search])
print(crew.kickoff(inputs={"topic": "AI safety"}))
CrewAI's own docs tell you never to commit API keys and to use secret
management; this is that, without the .env step in between.
2. Resolve in code
import seekrit
from crewai import LLM
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)
3. Never hold the 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}}",
)
# 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"]
# The crew's tools, bounded to the operations they actually need.
[[route]]
prefix = "/github"
upstream = "https://api.github.com"
allow = ["GITHUB_TOKEN"]
methods = ["GET", "POST"]
paths = ["/repos/*/*/issues", "/repos/*/*/issues/*"]
Without running the proxy
CrewAI has its own seam for this: LLM(interceptor=…). An interceptor is
CrewAI's contract for touching the raw request a provider is about to send, and
CrewAI installs it as that client's HTTP transport — so the key is resolved,
substituted into one outbound request, and exists nowhere else.
pip install 'seekrit[crewai]'
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"]},
),
)
)
This is per-LLM instance, so two agents in one crew can hold two different
credentials — the thing the LiteLLM module global below cannot do.
Check which provider you routed to. CrewAI sends an LLM to a native
provider or to its LiteLLM fallback based on the model string, and the
LiteLLM fallback accepts interceptor= and never calls it — your crew
would send the literal {{seekrit:OPENAI_API_KEY}} upstream and fail with a
provider auth error that names nothing useful. ensure_intercepted turns that
into an error at construction. Interceptors are honoured by the native OpenAI
provider (and everything built on it: OpenRouter, DeepSeek, Ollama, vLLM,
Cerebras, DashScope) and by Anthropic. Gemini, Azure and Bedrock reject one
outright, so those tell you themselves.
If you are staying on LiteLLM, its only seam is a module global:
import httpx, litellm
from seekrit.transport import AsyncSeekritTransport, SeekritTransport
allow = {"api.openai.com": ["OPENAI_API_KEY"]}
litellm.client_session = httpx.Client(transport=SeekritTransport(allow=allow))
litellm.aclient_session = httpx.AsyncClient(transport=AsyncSeekritTransport(allow=allow))
It works, and being process-wide it cannot scope per request. In-process injection sets out the trade-off against the proxy.
4. Scope a key to one agent or one tool
A crew is several agents sharing one process, so by default every agent can
reach every credential the token resolves. CrewAI's before_llm_call and
before_tool_call hooks carry the agent and the tool, so both bounds are
expressible:
from crewai import LLM
from seekrit.crewai import SeekritCredentials, 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"],
"api.stripe.com": ["STRIPE_SECRET_KEY"],
},
require_scope=True,
),
)
)
with SeekritCredentials(
agents={"Researcher": ["OPENAI_API_KEY"], "Biller": ["OPENAI_API_KEY"]},
tools={"refund": ["OPENAI_API_KEY", "STRIPE_SECRET_KEY"]},
):
crew.kickoff(inputs={"topic": "AI safety"})
Read that as two independent bounds that are intersected, narrowest wins:
agentsis what that agent may ever inject, model call or tool call. TheResearchercan spend the provider key and nothing else, so a prompt injection in a search result cannot reach Stripe through it.toolsis what a given tool may inject for an agent already allowed it.refundreaches Stripe — but only for an agent whose own list allows it, which is what stopsallow_delegationfrom being a way to widen.
Both are exhaustive when given: an agent or a tool not named there may inject nothing at all, so the crew you add an agent to tomorrow fails closed.
scope= picks which secrets to resolve, from the hook context — so one crew
process can serve many tenants without holding two tenants' keys at once:
SeekritCredentials(scope=lambda ctx: {"tenants": ctx.crew.inputs["tenant"]})
require_scope=True is not optional here. CrewAI deliberately swallows an
exception raised inside a hook, so scoping never enforces by raising —
enforcement is the interceptor refusing a request whose scope is missing or
too narrow. Without require_scope, a scope that failed to reach the
interceptor would fall back to the full allowlist, which is the wrong
direction to fail.
Gotchas
- The tools are the reason to bother. A crew is several agents sharing one
process, so by default every agent effectively has every tool's credentials.
Shape 4 bounds which credential each agent and tool may inject; the proxy's
methodsandpathsbound what can be done with one an agent legitimately has. They answer different questions — a research agent that may hold the GitHub token still should not be able toDELETEwith it. kickoffis one long process. Values resolved at start hold for the whole run. For a crew that runs for hours, either restart on rotation, setttl_seconds=0on the interceptor so every call re-resolves, or use shape 3, where the proxy re-resolves on its own interval.- Delegation crosses no process boundary.
allow_delegationmoves work between agents in the same process, so nothing stops a delegate from asking for work its own credentials would not cover — but withagents=above, the credential does not travel with the task: the injection is decided per call from the agent making it, and intersected with the tool's own list. For two agents that must not even be able to read the same secrets, that is still two processes with two tokens. - The hook registry is process-global. CrewAI registers hooks on module-level
lists rather than on a
Crew, so an installedSeekritCredentialsapplies to every crew in the process, and the executors snapshot the LLM hook list when they are built — install beforekickoff, not during.