Secrets

How project_secrets flow from the manifest to your sandbox.

The technical contract for secrets. For a walkthrough, see 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:

    # kortix.yaml (v2)
    env:
      required: [DATABASE_URL]
      optional: [STRIPE_API_KEY, WEBHOOK_SLACK_SECRET]
    # 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

ListEffect
requiredAdvisory. The dashboard surfaces it to nag the user about secrets to set. The session bootstrap does not currently block on missing values.
optionalSurfaced 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 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:

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

Secrets – Kortix Docs