Secrets
A secret is a broker-held value delivered to bound members’
runners and injected into the agent process as an environment
variable at spawn. The agent’s shell tools — gh, terraform,
npm publish, anything that reads an env var — find the credential
where they expect it, while the value never appears in briefing
prose, system prompts, MCP traffic, or the session transcript. An
admin drops the key once on the broker; every runner for the bound
members picks it up on start, on any machine, with nothing to put in
shell profiles.
Each secret has an immutable slug (the address), a target
envName (GITHUB_TOKEN), an optional description, and a
write-only value:
csuite secrets add gh --env GITHUB_TOKEN --description "org CI PAT"
csuite secrets set-value gh # prompts without echo; or pipe stdin
csuite secrets bind gh scout
On scout’s next csuite claude-code / csuite codex start, the
runner resolves the member’s secrets over its authenticated channel
and merges them into the agent child’s environment. The agent just
sees $GITHUB_TOKEN.
How a secret reaches an agent
admin ──REST/CLI/UI──▶ registry (secrets + bindings + enc values)
│ KEK-encrypted at rest (enc-v1)
▼
runner ── GET /secrets/resolve (own bearer) ──▶ broker decrypts,
│ returns ONLY the caller's delivery set, keyed by envName
▼
childEnv = { ...process.env, ...secrets, ...runnerManagedVars }
│ └── runner vars always win
▼
agent process (claude / codex) — $GITHUB_TOKEN is just set
- Delivery follows the tool-source rule exactly:
delivered = enabled && (allMembers || bound). There is nosecrets.managebypass — admins bind themselves like anyone else. - The resolve endpoint is the only read path for values, it only ever returns the calling member’s own set, and every delivery is logged broker-side (names, never values).
- Secrets are resolved once, at spawn — the process environment is frozen from then on. Rotating a value or changing bindings takes effect on each member’s next runner start; the change event in the secret’s thread says so.
--no-secretson either runner verb skips resolution entirely.
Write-only, encrypted at rest
The value is set via PUT /secrets/:slug/value (or
csuite secrets set-value, or the web UI) and never returned by any
endpoint — list/detail responses expose only hasValue: true. At
rest it is KEK-encrypted (enc-v1: envelope, the same scheme as
TOTP secrets and tool-source credentials), and value writes fail
closed when no KEK is active.
Guardrails
- Reserved names.
envNamemust be an uppercase POSIX name, and runner-managed prefixes (CSUITE_,OTEL_,CLAUDE_CODE_,CODEX_) plus loader/interpreter control variables (PATH,LD_*,DYLD_*,NODE_OPTIONS,BASH_ENV,GIT_ASKPASS, …) are rejected at save time. Without this, asecrets.manageholder could break trace capture or gain code execution on every runner machine. The runner re-checks the same list defensively before injecting, and merges its own managed vars after secrets so they always win. - One variable, one secret.
envNameis unique across the registry, so a member’s resolved map can never carry two values for one variable. - Trace redaction. Everything a runner captures is scrubbed with
the core redactor before upload — and the runner registers each
resolved secret value with it at startup, before capture
begins. The broker does the same for the values it ingests
server-side (OTLP bodies). An agent that runs
envorecho $GITHUB_TOKENleaks[REDACTED]into the stored activity stream, not the key.
Threat model — what this is and is not
Env delivery keeps secrets out of context and stored traces. It does not keep them from the agent: anything in the environment is readable by any command the agent executes, and once an agent echoes a value into its own context, the model has seen it — redaction protects the broker-side record, nothing can unsee it. Two consequences:
- Least-privilege the credential at its source (scope the PAT) — same doctrine as tool sources.
- For secrets the agent should never be able to read at all, use a broker-executed custom tool instead: the credential stays on the broker and only the tool’s results reach the agent. Secrets are the complementary primitive, for when the agent’s own CLI tooling needs the credential locally.
Administration
Gated on the secrets.manage leaf (see
permissions). Four surfaces: the REST
API (/secrets*), csuite secrets (see
CLI reference), the web UI (Secrets in the
nav), and — for members holding the leaf — the secrets_* MCP tools
in the agent toolbox (see MCP tools).
Slugs are immutable — the event thread key secret:<slug> rides on
them; envName and description are the mutable fields.
Registry changes fan out as data.kind = 'secret' channel events to
the secret’s delivery set plus every secrets.manage holder — never
carrying the value, and doubling as the “restart your runner to pick
this up” signal.
Agent-set values
Agents holding secrets.manage can set values too
(secrets_set_value) — the write-only invariant holds regardless of
author. The tradeoff mirrors tool_sources_set_credential: an
agent-set value passes through that session’s transcript. Treat that
as accepted for self-generated keys (a minted token, a
self-provisioned service account) and avoidable for human-held ones
— the common split is AI wires up the secret, human drops the
value via the web UI so it never enters agent context.
Note for existing teams: stored permission presets don’t
automatically gain new leaves — add secrets.manage to your admin
preset (csuite presets set admin --permissions ...). Fresh installs
include it via the wizard.