# Secrets in Modal

Modal's unit of environment is a `modal.Secret`, and the interesting one here is
`Secret.from_dict()` — it is **built where your code runs**, not stored in
Modal's dashboard. That makes Modal a particularly clean fit: seekrit stays the
single source of truth, and Modal holds the values only for as long as the
function or sandbox that received them.

## Inject into a Function at deploy time

```python
import os
import modal
import seekrit

secrets = seekrit.Client(token=os.environ["SEEKRIT_TOKEN"]).resolve()

app = modal.App("storefront-agent")

@app.function(
    secrets=[
        modal.Secret.from_dict(
            {
                "OPENAI_API_KEY": secrets["OPENAI_API_KEY"],
                "TAVILY_API_KEY": secrets["TAVILY_API_KEY"],
            }
        )
    ],
)
def run_agent(prompt: str) -> str:
    import os

    # The keys are ordinary environment variables in here.
    ...
```

`Secret.from_dict()` runs on your machine (or in CI) when you `modal deploy`, so
the values travel with that deployment. Named keys, not the whole `secrets` dict
— see [Pick names, not the whole
environment](/docs/guides/sandboxes#pick-names-not-the-whole-environment).

> **Note:** **This binds values at deploy time.** A rotated secret does not reach a deployed Modal function until you deploy again. That is the honest trade for keeping Modal out of the loop; if you need rotation to land without a deploy, read the [proxy section](#keep-the-credential-outside-the-sandbox) below, where the function holds no key at all.

## Inject into a Sandbox

Sandboxes take the same `secrets=` list, and this is the more interesting case
because a sandbox is usually running something you trust less than your own
function body:

```python
sb_app = modal.App.lookup("agent-sandboxes", create_if_missing=True)

sandbox = modal.Sandbox.create(
    app=sb_app,
    secrets=[modal.Secret.from_dict({"OPENAI_API_KEY": secrets["OPENAI_API_KEY"]})],
)

process = sandbox.exec("python", "-c", "import os; print(bool(os.environ['OPENAI_API_KEY']))")
print(process.stdout.read())
```

## Why there is no Modal sync connector

Every other agent-hosting page under
[third-party sync](/docs/guides/third-party-sync) exists because the platform has
a secrets API seekrit's servers can call. **Modal does not.** Its control plane
at `api.modal.com` speaks gRPC and nothing else — secrets are managed through the
dashboard, the `modal secret` CLI, or the Python/JS/Go SDKs, all of which talk
that same gRPC surface. There is no REST endpoint to push to.

That turns out to be the right answer anyway. Sync means seekrit decrypting your
environment on its own servers, which seekrit's
[zero-knowledge rule](/docs/concepts/encryption) permits only where no client
exists at the moment values are needed. With Modal a client always exists: your
`modal deploy`, or your code calling `Sandbox.create`. `Secret.from_dict()` keeps
decryption on your side, which is strictly better than a connector would have
been.

## Keep the credential outside the sandbox

For untrusted code, do not inject at all. Run
[`seekrit-proxy`](/docs/guides/agent-proxy) **where the sandbox cannot reach into
it** — the machine that created the sandbox — and give the sandbox a placeholder
and a URL:

```bash
# On the host, beside the code that calls Sandbox.create.
docker run --rm -e SEEKRIT_TOKEN=skt_… \
  -v "$PWD/seekrit-proxy.toml:/seekrit-proxy.toml" \
  -p 8080:8080 seekritdev/proxy --listen 0.0.0.0:8080
```

```toml
# seekrit-proxy.toml
[[route]]
prefix = "/openai"
upstream = "https://api.openai.com"
allow = ["OPENAI_API_KEY"]
methods = ["POST"]
paths = ["/v1/chat/completions", "/v1/embeddings"]
```

```python
sandbox = modal.Sandbox.create(
    app=sb_app,
    secrets=[
        modal.Secret.from_dict(
            {
                # A placeholder, not a key. Worthless if it leaks.
                "OPENAI_API_KEY": "{{seekrit:OPENAI_API_KEY}}",
                "OPENAI_BASE_URL": "https://proxy.internal.example/openai",
            }
        )
    ],
)
```

The agent's SDK sends `Authorization: Bearer {{seekrit:OPENAI_API_KEY}}`; the
proxy swaps in the real value and forwards it. The sandbox holds no seekrit
token, so there is nothing in it to steal — and the `paths` list means the key
cannot be spent on anything but chat completions and embeddings even by code that
finds the endpoint.

> **Warning:** **Do not run the proxy inside the same sandbox as untrusted code.** The proxy needs `SEEKRIT_TOKEN`, and a Modal sandbox is one process space — an agent running as the same user can read it out of the proxy's environment, which hands over the whole environment rather than the two keys you meant to share. The boundary only holds when the token is somewhere the agent cannot read.

## See also

- [Agent sandboxes](/docs/guides/sandboxes) — the two shapes and when each is right
- [Agent proxy](/docs/guides/agent-proxy) — the full proxy configuration
- [Notebooks](/docs/guides/notebooks) — `seekrit.load()`, if you are driving Modal from a notebook
