Secrets
Storing secrets, scoping them to tiers, injecting them into sessions, and rotating them.
A secret an agent can read is a secret in model context, in a log, and potentially in model output. The platform's job is to keep the window in which that is possible as narrow as the work allows.
POST /v1/secrets store
GET /v1/secrets list (metadata only — never the value)
PATCH /v1/secrets/{id} rotate
DELETE /v1/secrets/{id} remove
Store
curl -sS "$WB_BASE_URL/secrets" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{
"name": "github_token",
"value": "ghp_…",
"project_id": "proj_7fTn21",
"tiers": [2],
"max_ttl_s": 900,
"rotation": { "reminder_days": 90 }
}'
{
"id": "sec_github_token",
"name": "github_token",
"project_id": "proj_7fTn21",
"tiers": [2],
"max_ttl_s": 900,
"version": 1,
"created_at": "2026-10-06T09:14:22Z",
"last_injected_at": null
}
The value is encrypted with a per-organization key and is never returned by any API, to any principal, including organization owners. Lost means rotate, not recover.
Injection
curl -sS "$WB_BASE_URL/sessions" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{
"tier": 2,
"secrets": [
{ "ref": "sec_github_token", "as": "GITHUB_TOKEN", "ttl_s": 600 }
]
}'
Three properties worth stating explicitly:
- Not in the session environment. A secret is attached to the processes that need it, not
exported globally.
envin a shell does not list it, which means a model asked to "print the environment" does not get it. - Not in snapshots. A snapshot of a session with injected secrets does not contain them. A restored session must be given them again.
- Expires. After
ttl_sthe value is scrubbed from the process environment. Long-running sessions re-request throughwbagentrather than holding a secret for hours.
Tier scoping
The single most important control.
{ "tiers": [2] }
| Tier | Risk of injecting a secret |
|---|---|
| 0 | Handler-held, never visible to the model. Lowest risk |
| 1 | Visible to model-authored code. A secret here is a secret the model can print |
| 2 | Visible to shell commands the model wrote. Same risk, broader surface |
| 3 | Visible to the browser session and to any page it visits via JavaScript |
| 4 | Visible on screen, and in any recording |
Scope to tier 0 and tier 2 by default. A secret scoped to tier 1 is a secret in model context the first time the agent decides to debug by printing it.
Short-lived exchange
Wherever the upstream supports it, store the long-lived credential and inject a derived short-lived one.
{
"name": "github_app",
"type": "github_app",
"app_id": "1182",
"private_key": "-----BEGIN…",
"exchange": { "scope": "repo:read", "ttl_s": 600 }
}
The platform exchanges the app key for an installation token at injection time. The session holds a ten-minute, read-scoped token; the long-lived key never leaves the vault.
Supported exchanges: GitHub App installation tokens, OIDC token exchange (any compliant provider),
AWS STS AssumeRole, GCP service-account impersonation, Vault dynamic credentials.
For anything without an exchange, the same effect is achievable by having a tier 0 tool hold the credential and expose only the operation the agent needs — which is usually a better design anyway.
Rotation
curl -sS -X PATCH "$WB_BASE_URL/secrets/sec_github_token" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "value": "ghp_new…" }'
New sessions get the new version immediately. Running sessions keep the version they were injected with until their TTL expires, so rotation never breaks work in flight.
last_injected_at tells you whether a secret is still in use before removing it.
Reminders fire as secret.rotation_due webhooks at the configured interval.
What not to do
Do not put secrets in env. Plain environment variables on a session are not secrets: they are
in the session object, in the API response, in logs, and visible to everything in the guest.
// wrong
{ "env": { "GITHUB_TOKEN": "ghp_…" } }
// right
{ "secrets": [{ "ref": "sec_github_token", "as": "GITHUB_TOKEN" }] }
Do not bake secrets into templates. A template is content-addressed, cached across hosts, and shared with everyone who can use it. A secret in a template is a secret in a widely distributed image.
Do not inject a credential broader than the task. An agent that reads one repository does not need an organization-wide token. Scope at the upstream, not just at the platform.
Do not let an agent ask for a secret. Secrets are declared by your backend at session creation. There is no API a session can call to request one, deliberately — an agent that could request credentials is an agent that can be persuaded to.
Audit
Every injection is recorded: which secret, which version, which session, which end user, what TTL. Visible in the customer's audit log and queryable.
curl -sS "$WB_BASE_URL/audit?action=secret.injected&created_after=2026-10-01T00:00:00Z" \ -H "Authorization: Bearer $WB_API_KEY"
The value is never in an audit entry. The fact of injection is.
Customer-managed keys
Enterprise organizations can supply their own KMS key for secret encryption.
{
"encryption": {
"provider": "aws_kms",
"key_arn": "arn:aws:kms:ap-south-1:…:key/…",
"role_arn": "arn:aws:iam::…:role/workspaces-build"
}
}
Revoking the platform's access to that key makes every secret under it immediately unusable, including to the platform. Sessions requesting those secrets fail to start. That is the intended behaviour of a kill switch, and it is stated in the confirmation dialog in those terms.