# Secrets How project_secrets flow from the manifest to your sandbox. Canonical page: https://kortix.com/docs/reference/secrets The technical contract for secrets. For a walkthrough, see [Managing secrets](/docs/guides/managing-secrets). Secrets are per-project, encrypted at rest, and injected into every session as plain environment variables. They live in the platform's database (`project_secrets` table), never inline in your repo. Encryption: AES-256-GCM with HKDF-derived per-project keys rooted in the platform's master `API_KEY_SECRET`. Rotating the master key rotates every project's effective key without re-encrypting individual records. ## How they flow 1. **Declare a secret name** in the manifest: ```yaml # kortix.yaml (v2) env: required: [DATABASE_URL] optional: [STRIPE_API_KEY, WEBHOOK_SLACK_SECRET] ``` ```toml # kortix.toml (legacy v1) — same fields, TOML table [env] required = ["DATABASE_URL"] optional = ["STRIPE_API_KEY", "WEBHOOK_SLACK_SECRET"] ``` 2. **Set the value** in the Kortix dashboard's Environment variables page — or via `kortix secrets set` / `kortix env push`. 3. **When a session boots**, the platform decrypts every secret on the project and injects them as plain env vars into the sandbox. 4. **Your agent reads them** like any other env var (`process.env.DATABASE_URL`, `os.environ['DATABASE_URL']`, etc.). ## `required` vs `optional` | List | Effect | | --- | --- | | `required` | Advisory. The dashboard surfaces it to nag the user about secrets to set. The session bootstrap does not currently block on missing values. | | `optional` | Surfaced in the dashboard's Environment variables page so users can set them; missing is fine. | Treat `required` as a contract with the user, not a hard gate. Prefer `optional` for things with a sensible default-off behavior in your agent code. ## Identifier vs key Every secret row has two names: - **`identifier`** — the stable, unique-per-project handle. It's what you list in an agent's `secrets:` grant and what the dashboard shows as the secret's name. Format: `^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$` (letters, digits, `_`, `.`, `-`, starting with an alphanumeric, max 128 chars) — more permissive than the env-var key. - **`key`** (the manifest's `name`) — the actual env var injected into the sandbox (`process.env.KEY`). This is the env-var-shaped, ≤ 64-char name described in [Rules](#rules) below. For the common case (and every legacy/migrated secret) `identifier === key`, so the distinction is invisible — `secrets: [GITHUB_AGENT_TOKEN]` grants the identifier `GITHUB_AGENT_TOKEN`, which injects as `GITHUB_AGENT_TOKEN`. Identifiers only start to matter once a project has **multiple identifiers sharing one key** — e.g. `GMAPS-primary` and `GMAPS-backup` both resolving to `GOOGLE_MAPS_API_KEY`, so you can hold two candidate values under distinct handles while only one is ever live in the sandbox at a time: - If an agent's grant is `all` (or omitted entirely in a way that resolves to all), a collision picks a deterministic winner — identifier sort order — rather than erroring. - If an agent's grant is an **explicit list** that names two identifiers colliding on the same key, that's a configuration error: injection fails with an ambiguous-grant error naming the key and the conflicting identifiers, since there's no principled way to silently pick one for a deliberate list. ## Per-agent scoping (v2) In `kortix.yaml`, each entry in the `agents:` map has a `secrets` grant — which of the project's declared identifiers that agent receives as sandbox env vars. It's deny-by-default: an agent with no `secrets` key gets none, not every project secret. Give it `secrets: all` for the full set, or an explicit list to scope it down: ```yaml agents: kortix: secrets: all # every declared secret release-bot: secrets: [GITHUB_AGENT_TOKEN] # only this one ``` > **v1 default runs the other way** > In legacy `kortix.toml`, the equivalent field is named `env` (not `secrets`) and an **omitted** `env` on a declared `[[agents]]` entry defaults to **all** project secrets, not none — `env` was added after `connectors`/`kortix_cli` and had to default open so agents declared before it existed didn't silently lose access. Narrow it explicitly with a list or `"none"`. v2's `secrets` field starts closed instead. ## Rules - Names (keys) match `^[A-Z_][A-Z0-9_]{0,63}$` — env-var-shaped, capped at 64 chars. The manifest parser is looser (`^[A-Z_][A-Z0-9_]*$`, no length cap), so a name longer than 64 chars is accepted in `env:` (or `[env]`) but can never get a value. Keep names ≤ 64 chars. - Identifiers match `^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$` — see [Identifier vs key](#identifier-vs-key). Each identifier is unique per project (among shared, non-personal rows); re-creating an existing identifier with a different key is rejected rather than silently retargeting it. - The `KORTIX_*` prefix is reserved for platform variables; user secrets cannot use it. The secrets CRUD endpoint rejects it. The manifest parser does not enforce this, so declaring `KORTIX_FOO` as a secret name is accepted but no matching secret can be created. Don't. - Webhook triggers reference signing secrets by name only (`secret_env: WEBHOOK_FOO_SECRET`). The value is resolved at fire-time — the manifest never sees the plaintext. See [Triggers](/docs/reference/triggers). - Never commit a secret value into the repo. If you do by accident, rotate it via the dashboard and force-push a cleaned history. ## Rotation timing Secrets don't only take effect at session-create time. Creating, updating, or deleting a project secret (dashboard, `kortix secrets set`, or the API — including deletes and personal overrides) fires a live push to **every currently active sandbox on the project**, so a running session usually doesn't need to be restarted to pick up the change: 1. The platform re-resolves that sandbox's env snapshot (honoring the running agent's `secrets` grant) and POSTs it to the sandbox daemon's internal `/kortix/env` endpoint. 2. The daemon writes the full snapshot to the live agent env file immediately, which every new shell the agent spawns sources — so new tool calls see the new (or removed) value right away. 3. For gateway/provider-credential keys specifically — the ones that affect which LLM the sandbox talks to — the same push also restarts the in-sandbox `opencode` process, so the very next prompt in the session picks up the new credential without the user having to end the session and start a new one. Non-credential secrets update the shell env but don't force an opencode restart. > **Best-effort, not guaranteed** > The push is fire-and-forget: the API call that changed the secret returns before the sandbox push completes, and a failed push (sandbox unreachable, timeout, etc.) is only logged, not retried. If it fails, that sandbox keeps stale values until the next successful sync or until the session restarts. Treat propagation as fast (seconds) rather than instantaneous or guaranteed. ## Inspecting what's set The dashboard's Environment variables page (and `kortix secrets ls`) shows which secrets are declared in the manifest (`required` or `optional`) and which have a value set, marking required-but-missing names.