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
  1. Delivery follows the tool-source rule exactly: delivered = enabled && (allMembers || bound). There is no secrets.manage bypass — admins bind themselves like anyone else.
  2. 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).
  3. 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.
  4. --no-secrets on 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. envName must 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, a secrets.manage holder 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. envName is 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 env or echo $GITHUB_TOKEN leaks [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.