# AI frameworks

Every agent framework draws the same line: orchestration in, credential
management out. LangGraph, Mastra, CrewAI, Pydantic AI, both Agents SDKs — none
of them wants to be where your provider keys live, and all of them hand the
question back to you.

This page answers it twice: **the short version, which is three commands and no
code**, and then what to add as the agent grows teeth. Start at the top. Nothing
below the first section is required to have a working agent, and plenty of
projects never need any of it.

## Get running in five minutes

You need the CLI and an account:

```bash
npm install -g @seekrit/cli
seekrit login
```

None of the three steps below is framework-specific. If you already keep an app's
config in seekrit, skip to step 2.

### 1. Put the keys in an environment

An **environment** is the thing that holds secrets — one per app, per stage, like
`storefront/development`. Everything in it travels together, so put the agent's
provider keys in the environment its process should read. If they're in a `.env`
file today, that's one command:

```bash
seekrit secrets import .env --app storefront --env development
```

Or set them one at a time, or paste them into the
[dashboard](/docs/guides/web-app):

```bash
seekrit secrets set OPENAI_API_KEY sk-… --app storefront --env development
seekrit secrets set TAVILY_API_KEY tvly-… --app storefront --env development
```

No application yet? `seekrit app create --name Storefront --slug storefront`, or
create one in the dashboard and its environments come with it. The
[quickstart](/docs/quickstart) walks through a first-time account (keys,
passphrase, first secret) in more detail.

### 2. Mint a token for the agent

A **service token** is the credential a machine uses to read that environment.
It is bound to one environment when you create it:

```bash
seekrit token create --name agent-dev --app storefront --env development
# prints once:  skt_XXXXXXXX_…
```

Copy it now — the value is shown once and cannot be retrieved later. In the
dashboard the same thing is **Organization → Service tokens → Mint token**, or
the **no token** badge on an environment column, which pre-fills the binding.

The token carries its own private key, so it decrypts on its own: no passphrase,
no config file, nothing else to set up. See
[Service tokens](/docs/guides/service-tokens).

### 3. Run the agent through the CLI

```bash
export SEEKRIT_TOKEN=skt_XXXXXXXX_…

seekrit run -- python agent.py       # or: mastra dev, langgraph dev, pnpm dev…
```

`seekrit run` resolves the token's environment, decrypts locally, and injects
every value as an environment variable **in the child process only** — nothing on
disk, nothing in shell history, nothing for a coding agent to find in a file
later. Your framework picks the keys up exactly where it already looks:
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `TAVILY_API_KEY`, your database URL.

To see what the agent will get, without printing any values:

```bash
seekrit export --format dotenv | cut -d= -f1
```

> **Note:** **That is the whole integration.** No SDK, no import, no code change — you are not integrating with seekrit so much as declining to write a `.env` file. Delete the old one once the import worked: `seekrit run` still overlays a `.env` if one is there, and a file that exists is a file an agent can still read.

For containers and CI, [`seekrit-run`](/docs/guides/run) is the same behaviour as
a static binary with no Node dependency.

### If it didn't work

| Symptom | Cause |
| --- | --- |
| The framework says no API key was found | The key isn't in the environment the token is bound to. `seekrit export --format dotenv \| cut -d= -f1` lists what the token actually resolves; `seekrit whoami` prints the `org/app/env` it is bound to |
| The command runs but reads an old value | Something above the managed layer is winning. Precedence is `group < app-env < .env file < process env`, so a stale `.env` or an exported variable overrides seekrit. `seekrit run --explain` names the source layer of every variable, without printing values |
| `seekrit run` warns and the process starts anyway | Resolving is best-effort by design — a missing token, an unreachable API, or a decryption failure logs to stderr and still launches the command with whatever `.env` and `process.env` provide. `seekrit export` is the strict version: it errors instead |
| A value is right in the dashboard, empty in the process | The name has a typo, or the secret exists in a *different* environment of the same app. `seekrit secrets list --app <app> --env <env>` shows names per environment |

## Which secrets does the agent get?

**All of them, from the one environment its token is bound to** — plus the
slices of any [group](/docs/guides/environments) composed into that environment.
There is no per-key selection step on the seekrit side, and nothing to attach per
framework or per tool. The token's binding *is* the selection.

Two consequences worth knowing up front:

- **A secret you add later just shows up.** The next `seekrit run` resolves the
  environment as it stands now. You don't re-mint or re-grant anything to give an
  agent a new key — put it in the environment and restart the process.
- **A token is a decryption credential for that whole environment.** If it
  shouldn't hold everything in `storefront/development`, the fix is a narrower
  binding, not a narrower list of keys.

So the coarse control — the one the server enforces — is *which environment*:

| You want | Do this |
| --- | --- |
| The agent to read only some of an app's keys | Give the agent its own environment (`seekrit env create --app storefront --name "Agent dev" --slug agent-dev`) and bind its token there |
| Two agents with different keys | A token each, bound to different environments |
| Shared keys plus per-agent ones | Keep the shared ones in a **group** and compose it into each agent's environment |
| A key that works for one API and nowhere else | That's not a visibility question — see [hold a placeholder](#hold-a-placeholder-not-a-key) |
| One tool to spend a key the others can't | Same — see [scope a key to one tool](#scope-a-key-to-one-tool) |

The last two rows are the important distinction. Everything in this section is
about **what a token can read**. The levels further down are about **where a value
is allowed to go** once something can read it, which is a separate gate, lives in
your own config, and is optional.

## Where the seekrit token lives

One credential in, all the others out — so the remaining question is where *it*
goes. That depends only on where the agent runs, not on the framework:

| The agent runs… | Authenticate with |
| --- | --- |
| On your own machine | `seekrit login`. The session lives in `~/.config/seekrit/config.json`, so there's no token in your environment at all. A session isn't bound to an environment, so pass `--app` and `--env` to `run`/`export`, and expect a passphrase prompt (`SEEKRIT_PASSPHRASE` to skip it) |
| On a deployed server or container | `SEEKRIT_TOKEN` as a platform environment variable — the one variable your platform holds. Or hold nothing: [third-party sync](/docs/guides/third-party-sync) pushes the values into the platform's own environment instead |
| In CI | `SEEKRIT_TOKEN` as a repository or organization secret — see [CI/CD & containers](/docs/guides/ci-cd) |
| In an agent sandbox | Usually neither: resolve outside the sandbox and inject at create, or give the sandbox placeholders and a proxy endpoint. See [Agent sandboxes](/docs/guides/sandboxes) |
| Unattended, provisioning its own structure | `SEEKRIT_CLIENT_ID` / `SEEKRIT_CLIENT_SECRET` — machine credentials that mint and cache an admin token automatically. See the [MCP server](/docs/mcp) |

> **Warning:** **`seekrit run` passes its own environment through to the child.** A `SEEKRIT_TOKEN` you exported in your shell is therefore visible to the agent too — and a token decrypts *everything* its environment holds, not just what you injected. Fine while you're trying this out; on any machine where the agent can run shell commands, use `seekrit login` instead so there's nothing in the environment for it to find.

## Then tighten it, when you need to

Four levels, in order of how little your agent's process ends up holding. Each
one costs a bit more setup than the last, so take them when a specific thing
worries you — not up front.

### Wrap the process — where you already are

`seekrit run -- <your dev command>`. Zero code, every framework, every language.
The values are real environment variables inside the child, which means anything
the process can run can also read them.

**Good enough when** the process runs only code you wrote.

### Resolve in code

```python
import seekrit
seekrit.Client().into_env()      # or: secrets = seekrit.Client().resolve()
```

Three lines at startup with the [SDKs](/docs/guides/sdks) —
[Python](https://pypi.org/project/seekrit/),
[JS/TS](https://www.npmjs.com/package/@seekrit/sdk), Go, Ruby.

**Reach for it when** there's no wrapper to hang `seekrit run` off (a serverless
handler, a Worker, a notebook), or when different requests need *different*
credentials — a multi-tenant agent resolving per tenant rather than per process.

### Hold a placeholder, not a key

```python
ChatOpenAI(base_url="http://127.0.0.1:8080/openai/v1", api_key="{{seekrit:OPENAI_API_KEY}}")
```

The [egress proxy](/docs/guides/agent-proxy) holds the credential; your process
holds `{{seekrit:OPENAI_API_KEY}}`. On the way out the proxy swaps in the real
value — and only toward the hosts, methods, and paths you allowlisted,
default-deny. Anything in an environment variable can be printed, logged, or
POSTed somewhere else; a placeholder cannot.

**Reach for it when** the code holding the credential is not code you trust: a
[sandbox](/docs/guides/sandboxes), model-generated code, an agent whose next
action you cannot predict.

**Often still zero-code.** `OPENAI_BASE_URL` is a de facto standard — Pydantic
AI, CrewAI, and the OpenAI Agents SDK all read it, and Claude Code and the Claude
Agent SDK take `ANTHROPIC_BASE_URL`. Where a framework honours it, this level is
two environment variables and no source change:

```bash
export OPENAI_BASE_URL=http://127.0.0.1:8080/openai/v1
export OPENAI_API_KEY='{{seekrit:OPENAI_API_KEY}}'
```

Neither of those is a secret, so they belong in your compose file or Procfile,
not in seekrit.

**And often no proxy either.** The SDKs ship the same substitution engine as a
`fetch` wrapper and an `httpx` transport, so your code can hold the placeholder
without a sidecar — see
[in-process injection](/docs/guides/agent-proxy/in-process). Weaker than the
proxy, because it runs in the same process as the code holding the placeholder;
much stronger than an environment variable.

### Scope a key to one tool

A provider key buys tokens. A tool's Stripe or GitHub key does something
irreversible. The framework adapters narrow an allowlist to a single **tool
call**, so a prompt-injected `search` cannot reach a payment credential:

- **LangChain / LangGraph** — [`seekrit.langchain`](/docs/guides/frameworks/langgraph#4-scope-a-key-to-one-tool) middleware
- **Mastra** — [`seekritToolFetch`](/docs/guides/frameworks/mastra#5-a-key-one-tool-may-spend-and-the-others-may-not)
- **Pydantic AI** — [a toolset wrapper](/docs/guides/frameworks/pydantic-ai#4-a-key-one-tool-may-spend-and-the-others-may-not)

**Reach for it when** one tool in an agent can spend money or change state and
the others have no business touching that credential.

## What to add, and when

| If… | Add |
| --- | --- |
| You just don't want a `.env` file | Nothing — wrapping the process is the answer |
| It's a Worker, Lambda, or notebook with no wrapper | Resolve in code |
| Each request serves a different tenant | Resolve per run, not per process |
| The process runs code you didn't write | A placeholder, via the proxy |
| A key must work for one operation but not others | A placeholder, with `methods` and `paths` on the route |
| You want a placeholder but not a sidecar | [In-process injection](/docs/guides/agent-proxy/in-process) |
| One tool may spend a key and the others may not | Per-tool scoping |
| The agent must not hold the seekrit token either | Run the proxy beside it, or resolve outside its process entirely |
| You want several of these | Nothing conflicts; the proxy just sees a placeholder where a key used to be |

## Per framework

Each page shows the same levels in that framework's own idiom — the actual
constructor arguments, the actual dev command.

| Framework | | Start with | Worth knowing |
| --- | --- | --- | --- |
| [LangGraph / LangChain](/docs/guides/frameworks/langgraph) | Python | Wrap | Per-tool scoping via LangChain 1.x middleware |
| [Mastra](/docs/guides/frameworks/mastra) | TypeScript | Wrap | `model` takes a function of the request — per-tenant keys are first-class |
| [Pydantic AI](/docs/guides/frameworks/pydantic-ai) | Python | Resolve in code | `deps` is a real per-run seam; a toolset scopes one tool |
| [AI SDK](/docs/guides/frameworks/ai-sdk) | TypeScript | Wrap locally, resolve on serverless | `fetch` is a first-class provider option |
| [OpenAI Agents SDK](/docs/guides/frameworks/openai-agents) | Python | Wrap | Tracing is a second credential and a second egress host |
| [Claude Agent SDK](/docs/guides/frameworks/claude-agent-sdk) | Python / TS | Wrap | It deliberately ignores `.env`, so wrapping is the only zero-code path |
| [CrewAI](/docs/guides/frameworks/crewai) | Python | Wrap | The crew's tool keys are the ones to move to a placeholder |
| [LlamaIndex](/docs/guides/frameworks/llamaindex) | Python | Wrap | Ingestion pipelines hold their credential for a long run |

Not listed doesn't mean not supported: wrapping the process needs nothing from
the framework at all. If a framework starts a process, `seekrit run` goes in
front of it.

## Agents that manage their own secrets

The pages above are for *you* wiring up an agent. If the agent itself should
store and inject credentials — a coding agent setting up a project, say — install
the [agent plugin](/docs/guides/ai-agents/plugin) instead. It carries both MCP
servers and the skills that keep values out of files.
