Secrets
Store a password or API token once in your computer's own keychain, then refer to it by name everywhere — the value is write-only and no request ever returns it.
A secret is a credential: an API token, a webhook auth secret, a connector password. The agentic runtime needs one at the moment it dials an outside service, and at no other moment.
Kortecx stores secrets in your computer's own keychain — the same encrypted store your browser uses for saved passwords (macOS Keychain, Windows Credential Manager, the Linux kernel keyring via keyutils). You put a value in once, under a name. From then on, everything refers to the name.
The value is write-only. You send it on the way in and it is never sent back out — not to you, not to a model, not to a log or a run record. If you forget a token, you do not read it back from Kortecx; you get a fresh one from wherever it came from and set it again.
Store one
Secret writes change your host's keychain, so the runtime only accepts them from
a gateway bound to loopback — the local-only address 127.0.0.1, which no
other machine can reach. Start one:
kx serve --dev-allow-localLoopback is the gate
--dev-allow-local refuses a non-loopback address, so a gateway started this way
is local-only. If you instead run the gateway on a network address, kx secrets set and kx secrets rm are refused with permission_denied — a remote peer
must never be able to plant credentials in your keychain. Reading the names
does not need loopback, only an authenticated caller.
Set the value
Pick a name you will recognize later. Names may be 1–255 characters of
A-Z a-z 0-9 _ . -.
kx secrets set --name GITHUB_TOKEN --value ghp_xxxxxxxxxxxxxxxxxxxxIt prints stored.
import kortecx as kx
client = kx.default_client()
client.secrets.set("GITHUB_TOKEN", "ghp_xxxxxxxxxxxxxxxxxxxx") # -> Trueimport { KxClient } from "@kortecx/sdk";
const kx = new KxClient("http://127.0.0.1:50151");
await kx.secrets.set("GITHUB_TOKEN", "ghp_xxxxxxxxxxxxxxxxxxxx"); // -> trueSetting a name that already exists overwrites it. There is no separate "rotate" step — you set the new value under the same name and every reference follows.
Check what is stored
Listing returns names and timestamps, and nothing else. There is no request in the system that returns a secret's value.
kx secrets listThe names are printed one per line, for example:
GITHUB_TOKEN
KX_GMAIL_CREDENTIALAdd --json for the machine-readable form, which includes the first-stored and
last-updated wall-clock timestamps (in milliseconds since the Unix epoch; the
values below are illustrative):
kx secrets list --json{"names":[{"name":"GITHUB_TOKEN","created_unix_ms":1730000000000,"updated_unix_ms":1730000000000}],"has_more":false}page = client.secrets.list()
for row in page.names:
print(row.name, row.updated_unix_ms) # names + timestamps onlyconst page = await kx.secrets.list();
for (const row of page.names) {
console.log(row.name, row.updatedUnixMs); // names + timestamps only
}If nothing is stored, the CLI prints (no secrets stored).
Remove one
kx secrets rm --name GITHUB_TOKENIt prints removed, or not removed (no such secret) if that name was not
there. kx secrets remove --name GITHUB_TOKEN is an accepted alias.
client.secrets.remove("GITHUB_TOKEN") # -> True if a secret was removedawait kx.secrets.remove("GITHUB_TOKEN"); // -> true if a secret was removedRemoval is a write, so it needs the same loopback-bound gateway as set.
Where the name gets used
A stored secret does nothing on its own. It becomes useful when something needs to authenticate.
A connection. A connection is an outside tool
server the runtime dials. Register it with --credential-ref naming the secret:
kx connections add --name gh --command npx --arg -y --arg @some/github-mcp \
--credential-ref GITHUB_TOKEN--command is the program to run and each --arg adds one argument to it, in
order. The connection record stores the string GITHUB_TOKEN. When the runtime
dials that server it looks the name up, injects the value into that one call, and
drops it.
Registering a connection dials the server straight away, so store the secret first and add the connection second — otherwise the first dial runs without the credential.
The bundled providers fill the name in for you. This registers a connection
called gmail whose credential reference is KX_GMAIL_CREDENTIAL:
kx connections add --provider gmailEach provider runs a bundled connector program (for gmail, kx-connector-gmail),
which has to be resolvable next to kx or on your PATH before the dial can
succeed. kx connections doctor --provider gmail checks that locally, without
contacting the gateway, and tells you how to install it if it is missing. The
other providers are slack, discord and notion.
A webhook trigger. A trigger that authenticates an inbound request takes
--auth hmac_sha256 or --auth bearer together with --secret-ref <NAME>,
naming the secret the runtime checks the request against. See
scheduling and triggers. Same contract: the name travels, the
value does not — kx triggers list reports only whether a reference is attached.
An app's agentic loop. When an app lets a model choose the tool
to fire, the run carries a fixed set of secret names it is allowed to resolve — a
secret_scope. Before any tool call, the runtime checks that what the call asks
for is a subset of what the run was granted; anything outside it is refused. You
can see what a run was authorized to do in its trace:
kx react list --instance <instance-id><instance-id> is the run's 16-byte instance id, written as hex. Each turn
carries a grants line:
grants[tools: kortecx.diagnostics.echo@1; secrets: KX_API_KEY]That is the tool the chain may fire and the secret name it may resolve. Never a value.
How a name resolves
When the runtime needs the credential, it checks two places in order:
- The keychain — what
kx secrets setwrites. - An environment variable of the same name — if no keychain entry exists,
GITHUB_TOKENin the host environment still resolves.
The environment fallback keeps older setups working and covers hosts where you prefer to inject credentials at start-up. A name present in both resolves from the keychain.
The keychain may not exist
Some hosts have no keychain backend available — a headless Linux box without a
keyring service, for example. Kortecx says so (failed_precondition, "the OS
keychain is unavailable on this host") rather than pretending the write
succeeded. On such a host, use the environment fallback.
Why the value never travels
The runtime's rule is that every durable thing it writes down — a run record, a step result, a stored content payload, an observability record, the text a model sees — carries the secret's name. The value is resolved as late as possible, at the moment of the outbound call, and discarded straight after.
That is what makes the write-only rule enforceable rather than aspirational: if the value is only ever held for the duration of one network call, there is no place for it to leak into.
The list of names, on the other hand, is deliberately visible. Knowing which credentials a runtime holds, and which of them a given run was allowed to touch, is a governance question — you should be able to answer it without seeing a single secret.
What this is not
The local secret store is a single-system store for a runtime you operate yourself. A multi-tenant vault backed by a key-management service or hardware security module — with rotation policy, per-party scoping and audit — is not part of the local runtime, and this store makes no cryptographic claim beyond "your operating system's keychain".
Connections
Register an outside tool server and point it at a stored secret by name.
Authoring a connector
The security contract a connector must honour, including never echoing a credential back.
Approvals
Put a human in front of an action rather than in front of a credential.
CLI reference
Every verb, including the shared --endpoint and --token client flags.