seekrit
Docs/CrewAI

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.

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 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.

caution

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:

  • agents is what that agent may ever inject, model call or tool call. The Researcher can spend the provider key and nothing else, so a prompt injection in a search result cannot reach Stripe through it.
  • tools is what a given tool may inject for an agent already allowed it. refund reaches Stripe — but only for an agent whose own list allows it, which is what stops allow_delegation from 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"]})
note

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 methods and paths bound 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 to DELETE with it.
  • kickoff is one long process. Values resolved at start hold for the whole run. For a crew that runs for hours, either restart on rotation, set ttl_seconds=0 on 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_delegation moves work between agents in the same process, so nothing stops a delegate from asking for work its own credentials would not cover — but with agents= 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 installed SeekritCredentials applies to every crew in the process, and the executors snapshot the LLM hook list when they are built — install before kickoff, not during.