Secrets
Store credentials for a project, decide who and which agents can use each one, and choose whether agent code can read the value.
A secret is an encrypted, per-project credential: an API key, a token, or a connection string that a session needs and that must not live in the repository. This page is for the people who set up a project, and for agents that store a value a person gives them.
kortix secrets set STRIPE_API_KEY=sk_live_...
That one command stores the value for everyone in the project. Kortix pushes
it to every active sandbox as the environment variable STRIPE_API_KEY. In a project whose kortix.yaml lists agents, also grant it
to the agent that needs it: see Which agents receive it.
What a secret is
Each secret has two names and one value.
| Name | What it is | Format |
|---|---|---|
| Identifier | The handle for every access rule: the CLI argument, the agent secrets grant, the audience. Matching ignores case. |
^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ |
| Key | The environment variable name in the sandbox. Kortix uppercases it. | ^[A-Z_][A-Z0-9_]{0,63}$ |
The identifier defaults to the key. Set a different identifier to keep a
second value under the same key. For example, GMAPS-primary and
GMAPS-backup can both resolve to GOOGLE_MAPS_API_KEY. An identifier keeps
its key: writing it again with a different key returns 409.
- Encryption. Kortix encrypts every value with AES-256-GCM, under a key derived per project with HKDF-SHA256.
- Write-only. No API, CLI command, or page returns a stored value. A value reaches a sandbox only through its exposure.
- Reserved names. Kortix rejects keys that start with
KORTIX_(platform values) orMS_TEAMS_(the Microsoft Teams connection), and the keyCODEX_AUTH_JSON(ChatGPT subscription sign-in). - Names that never reach a sandbox. Kortix stores these keys without an
error, but never delivers them to a session sandbox: every key that starts
with
OPENCODE_;PORT,PATH,HOME,PWD,USER,LOGNAME,SHELL,HOSTNAME,TERM,TMPDIR,NODE_ENV,NODE_OPTIONS,LD_PRELOAD,LD_LIBRARY_PATH; andSLACK_SIGNING_SECRET,SLACK_BOT_TOKEN. Use a different key, for exampleMY_PATH. Apps apply the same list: see Environment inside an App. - Model provider keys belong on the Models page or
kortix providers set. Kortix spends those keys server-side and keeps them out of the sandbox. Keys and ChatGPT accounts that members share across projects are account resources: see Shared keys and ChatGPT accounts. - Assigned usages. Kortix marks a secret that one of its own services spends: LLM gateway (a provider key), Connector (a connector credential), or Git (Kortix Git access). The Secrets page shows these read-only.
Add a secret
- Open Customize → Secrets in the project.
- Enter the key and the value.
- Pick Who can use it.
- Save.
A new secret is shared with everyone in the project and uses the Environment variable exposure.
kortix secrets set STRIPE_API_KEY=sk_live_... # everyone in the project
kortix secrets set PAYROLL_TOKEN=... --only-me # only you
kortix secrets set GOOGLE_MAPS_API_KEY=... --identifier GMAPS-backup
printf '%s' "$TOKEN" | kortix secrets set GITHUB_TOKEN=- # value from stdin
kortix secrets set ACME_API_KEY=... --scope connector # server-side, for a connectorOne command sets several KEY=VALUE pairs. --identifier (alias --id)
takes exactly one pair. One KEY=- per command reads its value from stdin.
--scope runtime is the default and loads the value into the sandbox.
--scope connector keeps it server-side for the connector gateway.
kortix env push --from .envEvery NAME=VALUE line becomes a project secret. The parser accepts
export NAME=VALUE and quoted values, and skips comments and empty values.
env push sets values only. It does not change any secret’s exposure.
Ask someone else for the value
kortix secrets request STRIPE_API_KEY
The command prints a link. Whoever opens it enters the value, and you never see it. The link can only write the named values. It cannot read any secret.
- The link is valid for 7 days. Set
--expires <minutes>for 1 minute to 30 days. Reuse a live link instead of minting a new one. - By default, a link stores the value server-side for connectors. Add
--scope runtimewhen the agent’s shell must receive it. - The link page asks Who can use it: everyone in the project, or only the person who asked. Choose the second for a personal credential.
- With
--scope runtimein a session, the command warns when the session’s agent will not receive a name, and prints the fix. The value is saved either way.
When an agent gets a value in chat
An agent that receives a value in a session stores it at once with
kortix secrets set. This needs project.write and project.secret.write.
If set returns 403 because the agent does not have project.write, the
agent sends a request link instead: kortix secrets request needs only
project.secret.write. Without project.secret.write, both commands return
403, and a person stores the value. An agent can store an environment secret
or a --scope connector secret. It cannot set an exposure, a host list, or an
audience.
Who can use it
Each value has an audience. It is the same Who can use it choice as a connector account.
| Choice | Who can use the value |
|---|---|
| Everyone in the project (default) | Every session in the project, including triggers, schedules, and shared sessions. |
| Only you | You directly, and your own private sessions. |
| Specific people, groups, or agents | The people and groups you pick, directly and in their own private sessions. The agents you pick, in every session of that agent. |
A session acts as two subjects:
- Its agent, always. A value shared with an agent reaches every session of that agent, triggers included. Anyone who can run the agent can use the value through it. Share with an agent when an unattended run needs the value.
- Its person, only while the session is private and only the person who started it prompts it. A prompt from someone else clears the person. Shared sessions, triggers, schedules, webhooks and email sessions have no person.
Kortix services that act for nobody (the Git proxy, channel installs, App
deployments) use values shared with everyone only. A connector spends a value
only for a caller in its audience. Anyone else gets credential_not_shared,
and no request leaves Kortix.
Several values can share one key. The session gets the value shared with its
person first, then the value shared with its agent, then the value shared with
everyone. Example: BILLING_API_TOKEN holds the team value, and the identifier
BILLING_API_TOKEN-finance (same key) is shared with a Finance group. The
agent’s grant admits both identifiers. Finance members get the Finance value in
their private sessions. Everyone else, and every trigger, gets the team value.
Change an audience on the Secrets page or with kortix secrets share. The
command replaces the whole audience.
kortix secrets share BILLING_API_TOKEN --user me
kortix secrets share BILLING_API_TOKEN-finance --group <group_id>
kortix secrets share NIGHTLY_REPORT_TOKEN --agent reporter
kortix secrets share BILLING_API_TOKEN --everyone
--user takes an email, a user ID, or me. Repeat --user, --group, and
--agent for more principals. Only a person sets an audience: an agent session
gets 403. People who manage the project’s secrets still see a value that is
not shared with them, marked Not shared with you, and can widen it.
Which agents receive it
A session receives a secret only when the agent it runs admits the identifier
in its secrets grant in kortix.yaml. Kortix applies both checks: the
audience of the value, and the grant of the agent.
kortix_version: 2
agents:
my-agent:
secrets: [STRIPE_API_KEY, GMAPS-primary]
reporter:
secrets: all
| Grant | What the agent receives |
|---|---|
No agents: block, or no kortix.yaml |
Every agent gets every environment secret. No agent gets an egress-enforced secret. |
secrets: all (or *) |
Every environment secret. No egress-enforced secret. |
secrets: [A, B] |
Exactly those identifiers, at environment or egress-enforced exposure. A disabled secret reaches no session. |
secrets omitted, or secrets: none |
No project secret. |
Agent not listed in a project that has an agents: block |
No project secret. |
An explicit list that admits two identifiers with the same key, both reaching
the session at the same audience rank (person, agent, or everyone), is a
configuration error (AmbiguousSecretGrantError). Under secrets: all, Kortix
picks one of them in identifier sort order.
Grant a secret to an agent
The Secrets page marks a secret that no agent can receive: No agent can receive this secret. Choose an agent there and confirm. From the CLI:
kortix secrets grant STRIPE_API_KEY --agent my-agent
Both commit chore(agents): grant <IDENTIFIER> to <agent> to kortix.yaml:
- An agent the manifest does not list gets a new entry with this one identifier.
- A listed agent keeps every other field. The identifier joins its list.
- An agent on
secrets: allgets an explicit list: every identifier the project has now, plus this one. A secret added later needs its own grant. - An agent that already admits the identifier needs no commit.
- When the commit adds the first
agents:block, the command warns that unlisted agents now get no project secret.
A grant only widens one agent’s list. To narrow or replace a list, use
kortix agents scope <agent> --secrets A,B (or all, none). See
Agents & permissions.
Inside a session, kortix secrets ls lists only the secrets the agent is
granted. A key the manifest declares outside the grant shows not granted.
An agent session cannot run kortix secrets grant: the API returns
403 agent_session_forbidden. A person with project.agent.write and
project.secret.read grants the secret.
Exposure: environment, enforced, none
The exposure answers one question: can agent code read the real value? The Secrets page asks it as Can your code read this value?
| Exposure | UI label | CLI value | The sandbox holds | Use it for |
|---|---|---|---|---|
| Environment (default) | Environment variable | environment |
The real value, as an environment variable | Values the code must compute with, and protocols that are not HTTPS |
| Egress-enforced | Enforce at the network | enforced |
A handle that is worth nothing on its own | HTTPS API keys and tokens |
| None | Disabled | none |
Nothing | A value kept on file and delivered to no session and no service |
Change it on the Secrets page or with kortix secrets delivery:
kortix secrets delivery STRIPE_API_KEY enforced --allow-host api.stripe.com
kortix secrets delivery STRIPE_API_KEY environment
kortix secrets delivery OLD_TOKEN none
To move a secret back to environment, set a new value first with
kortix secrets set. Otherwise the API returns 409 secret_rotation_required.
Only a person changes an exposure. An agent session gets 403.
Environment
The sandbox receives the real value. Agent code, and every process it starts, can read it, print it, and send it anywhere. Kortix cannot redact or audit that use. These secrets must stay on environment:
- Computed credentials. The value is an input to a calculation and never travels: AWS SigV4 signing keys, HMAC webhook-signing secrets, JWT client assertions, SSH and PEM private keys.
- Protocols that are not HTTPS. A Postgres connection string, SMTP credentials.
Egress-enforced
The sandbox receives a handle in the environment variable. The agent uses that variable as it would use the real key. Kortix replaces the handle with the real value outside the sandbox, only on requests to the hosts you list.
- The handle works in a header (
Authorization: Bearer $VAR,X-Api-Key, a cookie), in the URL path or query, and in a JSON or form body. Kortix finds it raw, URL-encoded, base64-encoded, or JSON-escaped. - A request to an unlisted host goes out unread and carries the literal handle. The upstream rejects it.
- An echo of the real value in a response comes back as
[REDACTED]. - Kortix writes an audit record for every relayed request.
- The mechanism is the same on every sandbox provider.
KORTIX_SECRET_CAPABILITIESin the sandbox lists each secret, the variable that holds its handle, and its hosts. It never contains a value.
Hosts are exact HTTPS hostnames, one per entry. The API rejects a wildcard
(*.example.com) with 400 secret_delivery_policy_invalid. The CLI also
rejects a path or a scheme.
api.example.com does not cover uploads.api.example.com: list both. Two
egress-enforced secrets can share a host.
Egress-enforced exposure needs the project flag secrets_egress, which is on
by default. With the flag off, moving a secret to egress-enforced returns
403 feature_disabled, and existing egress-enforced secrets keep working. See
Feature flags.
How the relay behaves
A listed host is reached through Kortix. The relay differs from a direct call:
| Behavior | Value |
|---|---|
| Streaming | Request and response stream. Server-sent events work. |
| Body size | 1 GiB each way (KORTIX_RELAY_MAX_REQUEST_BYTES, KORTIX_RELAY_MAX_RESPONSE_BYTES) |
| Time to response headers | 30 s (KORTIX_RELAY_HEADERS_TIMEOUT_MS) |
| Idle upstream connection | 600 s (KORTIX_RELAY_UPSTREAM_IDLE_TIMEOUT_MS). No total-duration limit. |
| Redirects | At most 3. No redirect is followed after the value is substituted. |
| Response headers returned | content-type, content-language, cache-control, etag, last-modified, retry-after, x-request-id, and the ratelimit-* and x-ratelimit-* limit, remaining, and reset headers |
| Clients intercepted | Only clients that honor https_proxy. Kortix sets it and the CA trust in the agent’s environment. A process started with a cleared environment sends the handle directly. |
The KORTIX_RELAY_* values are API settings for a self-hosted instance.
For a request that cannot go through the proxy, the agent sends it through Kortix explicitly, under the same host list:
kortix secrets call STRIPE_API_KEY https://api.stripe.com/v1/customers \
--method POST --header "authorization: Bearer $STRIPE_API_KEY" --data 'email=a@example.com'
secrets call buffers the exchange: a 1 MiB request body, a 5 MiB response, and
30 s for the whole call.
None
The secret is stored and encrypted. No session and no service receives it.
Verify it with two probes
Run both probes for an egress-enforced secret, in the sandbox of a session whose
agent lists the identifier. The example checks STRIPE_API_KEY. Add the echo
host for the test. --allow-host replaces the whole host list, so repeat every
host:
kortix secrets delivery STRIPE_API_KEY enforced --allow-host api.stripe.com --allow-host postman-echo.com
# Probe 1: the sandbox holds a handle, not the value.
env | grep '^STRIPE_API_KEY='
# expected: STRIPE_API_KEY=<handle>, not sk_live_...
# Probe 2: Kortix substitutes the value and redacts the echo.
curl -sS -H "authorization: Bearer $STRIPE_API_KEY" https://postman-echo.com/get
# expected: a JSON body with "Bearer [REDACTED]" in the echoed headers
| Probe 2 result | Meaning |
|---|---|
200, echo shows [REDACTED] |
Working. The real value went upstream, and the echo was redacted. |
200, echo shows the handle |
No substitution. Check the host list and the agent grant. |
401 from a real API, or a connection error |
No substitution, or the host is unreachable. Check the host list and the agent grant. |
After the test, remove the echo host:
kortix secrets delivery STRIPE_API_KEY enforced --allow-host api.stripe.com.
Rotate, remove and sync
Rotate. Set the value again with kortix secrets set or on the Secrets
page. Every write pushes to the project’s active sandboxes. No restart is
needed.
- A write to an environment or disabled secret pushes in the background.
- A write that touches an egress-enforced secret waits for the push. The
response carries
delivery_syncwithok,targeted,synced,failed, and the failed sessions. The next request through the relay uses the new value.
Undecryptable. Kortix encrypts every stored value with the server’s
encryption key. When that key changes (a self-hosted API_KEY_SECRET was
replaced), or a stored row is damaged, the value is lost:
- The Secrets page marks the row Undecryptable. For the shared value, someone who manages the project clicks Re-enter value and saves the value again. For your own value of a secret, which your sessions read in place of the shared one, click Delete my value: your sessions then read the shared value.
- Customize → Models → Providers marks an account key or a ChatGPT login Undecryptable. Its owner or an account admin clicks Re-enter key; the owner of a ChatGPT login clicks Reconnect.
GET /v1/projects/:id/secretsreturnsstatus: "undecryptable"("ok"otherwise) for the shared value, and nostatuswhen no shared value is set.mine.statusreports your own value.GET /v1/accounts/:id/secret-resourcesreturnsstatusfor each account key. The SDK reads them asProjectSecret.status,ProjectSecret.mine.statusandAccountSecretResource.status. No response carries the value.kortix secrets lsshowsundecryptable.--jsonreportsstatus: "undecryptable"andavailable: false.- A session start that needs the value fails with
Project secret <IDENTIFIER> cannot be decrypted with this server's encryption key. Re-enter its value.For your own value, the message starts withYour own value of project secret <IDENTIFIER>. - Any other request that needs the value answers
409withcode: "SECRET_UNDECRYPTABLE"and the same message. - A connector call returns
credential_undecryptable. See Connector errors.
Sync. Every prompt re-syncs the session’s secrets. To sync now, run
kortix secrets sync. In an agent session it pulls that session’s secrets and
grant. Run by a person, it re-pushes every active sandbox of the project.
Narrow an audience or exposure. A value already delivered as an environment variable stays readable in that sandbox until the session restarts. If it may have been read, rotate it at the provider. The same applies when you move a secret from environment to another exposure.
Remove. Run kortix secrets unset IDENTIFIER (several identifiers are
allowed), or delete the secret on the Secrets page. A secret a connector uses
returns 409 secret_connector_binding_exists: remove the binding first.
CLI reference
Every command takes --project <id>. Run kortix secrets --help for all flags.
| Command | What it does |
|---|---|
kortix secrets ls [--json] |
List identifiers, keys, exposure, and whether each has a value. A value Kortix cannot decrypt shows undecryptable. Never prints values. |
kortix secrets set KEY=VALUE … [--identifier <id>] [--scope runtime|connector] [--only-me] |
Create or update secrets. KEY=- reads the value from stdin. |
kortix secrets request NAME … [--scope runtime|connector] [--expires <minutes>] |
Create a link for someone else to enter values. Default scope: connector. |
kortix secrets share IDENTIFIER --user <email|id|me> --group <id> --agent <name> | --everyone |
Replace who can use the value. |
kortix secrets grant IDENTIFIER --agent <name> |
Add the identifier to one agent’s secrets list and commit. |
kortix secrets delivery IDENTIFIER environment|enforced|none [--allow-host <host>]… [--consumer llm-gateway|connector] |
Set the exposure. --allow-host is required for enforced and replaces the host list. --consumer goes with broker. |
kortix secrets call IDENTIFIER URL [--method] [--header name:value]… [--data | --data-file <path>] |
Send one HTTPS request through Kortix under the secret’s host list. |
kortix secrets sync |
Re-push secrets now. |
kortix secrets unset IDENTIFIER … |
Delete secrets. |
kortix env push --from <path> |
Upload every NAME=VALUE in a .env file. |
kortix env pull [--out <path>] [--force] |
Write a .env file with names only (NAME=), for you to fill in. |
delivery also accepts the stored names runtime (environment), egress
(enforced), denied (none), and broker. broker takes
--consumer llm-gateway|connector and marks a secret that the LLM gateway or
the connector gateway spends server-side.
Errors
| Error | Status | Cause | Fix |
|---|---|---|---|
credential_not_shared |
— | A connector call runs as someone outside the value’s audience. | Share the value with that person, or with the agent. |
credential_undecryptable / SECRET_UNDECRYPTABLE / … cannot be decrypted |
403 / 409 / — |
Kortix cannot decrypt the stored value: the server’s encryption key changed, or the row is damaged. | Enter the value again: Re-enter value on the Secrets page, or kortix secrets set. For your own value: Delete my value. |
PERSONAL_SECRET_REQUIRES_PRIVATE_SESSION |
409 |
You shared a session that holds a value shared only with you. | Start a new session, or share the value with everyone. |
agent_grant_unscoped |
— | Withheld reason: an egress-enforced secret does not reach a session whose agent grant is all or absent. |
Name the identifier: kortix secrets grant. |
AmbiguousSecretGrantError |
— | An explicit secrets list admits two identifiers with the same key at the same audience rank. |
Keep one of them in the list. |
| Identifier already exists with key … | 409 |
The identifier is in use with a different key. | Delete it first, or use a new identifier. |
feature_disabled |
403 |
The project turned secrets_egress off. |
Turn the flag on, or keep environment exposure. |
secret_delivery_policy_invalid |
400 |
The host list is not valid, for example a wildcard host. | List exact hostnames. |
manifest_v1_unsupported |
400 |
The project uses kortix.toml. |
Migrate to kortix.yaml, or edit the grant by hand. |
secret_not_grantable |
409 |
The secret is disabled. | Set an exposure first. |
secret_connector_binding_exists |
409 |
A connector uses the secret. | Remove the connector binding first. |
secret_rotation_required |
409 |
You moved a secret back to environment without a new value. | Set a new value, then change the exposure. |
agent_session_forbidden |
403 |
An agent session tried to grant a secret. | A person runs the grant. |
Agent sessions cannot change secret delivery policy / An agent cannot change who can use a secret |
403 |
An agent tried to set an exposure, a host list, or an audience, or to delete a secret that is not on environment exposure. | A person makes the change. |