Store a secret
API keys, signing secrets, and database URLs belong in encrypted platform storage. You keep an opaque ssec_… token in commands and config. The plaintext never goes in git, in a page, or in chat if you can avoid it.
upload / download are the wrong tool. Those links write ordinary workspace files.
Only repository owners can secret open / secret set. Members can secret list tokens they are allowed to see.
Done looks like
secret poll (or secret set) prints a token that starts with ssec_. A later curl or MCP header that contains that exact token succeeds. secret list shows it.
Worked example: GitHub token for curl
Job: call GitHub as the owner without putting a PAT in the repo.
secret open github-token
Expected output (shape):
Open this URL to submit the secret:
https://…/sails/secrets/…
Intake: 8c1e…
- Open the URL. Sign in with platform OAuth on that intake page. Admin login alone does not authorize it.
- Paste the PAT. Submit.
- Back in the shell:
secret poll 8c1e…
# prints: ssec_a1b2c3d4...
Use the token. Exact ssec_… is replaced at call time:
curl -s -H "Authorization: Bearer ssec_a1b2c3d4..." https://api.github.com/user
Store ssec_a1b2c3d4... in MCP headers, webhook --secret, or a .db connection line — not ghp_….
Do not pass --wait from bash_exec. Same buffering problem as upload --wait: you never see the URL. Print, then secret poll INTAKE_ID.
Scripts: set from the shell
Plaintext goes through the session. Prefer secret open when a human is present.
secret set "sk-live-..."
# prints: ssec_…
# or
printf '%s' "$KEY" | secret set --stdin
Who can use the token
Default visibility is owner (the creator only).
secret list
secret chmod ssec_a1b2c3d4... --visibility members
secret chmod ssec_a1b2c3d4... --visibility group --group writers
secret unset ssec_a1b2c3d4...
| Visibility | Who can use it |
|---|---|
owner (default) | Creator only |
group | Creator + --group NAME |
members | Any repository member |
secret chmod / secret unset: creator or repo owner.
To rotate plaintext without changing the id (so MCP headers keep working), use Admin → repo Settings → Secrets → Set value on that token.
Where substitution happens
Exact ssec_… matches are replaced when the platform makes the outbound call:
curlURL (whole-token only),-Hvalues, and-d/--databodies- MCP server headers / auth and tool
--jsonarguments - Webhook
--secretand similar config - SSH identity:
ssh user@host -i ssec_…
If you cannot read that token, substitution fails closed — the call does not send a placeholder.
OAuth MCP (mcp connect) and integration connect store their own tokens. You do not secret set those.
Flags
secret help
| Command | Who | What it does |
|---|---|---|
secret open [LABEL] | owner | Print intake URL immediately |
secret poll INTAKE_ID | owner | Wait for the browser submit; print ssec_… |
secret open --wait [LABEL] | owner | Block until submitted — avoid in bash_exec |
secret set VALUE | owner | Store from the shell; print ssec_… |
secret set --stdin | owner | Read the value from stdin |
secret list | member | Tokens you can see |
secret chmod ssec_… --visibility … | creator or owner | Change who can use it |
secret unset ssec_… | creator or owner | Revoke |
--timeout SECS applies to secret open / secret poll.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
| Intake page rejects you after Admin login | Intake needs platform OAuth on the MCP public URL | Sign in on the printed URL, not only Admin |
secret open denied | Owner-only | An owner runs it, or elevate that goal |
secret poll times out | Nobody submitted, or timeout elapsed | secret open again |
| curl / MCP fails closed | You cannot use that ssec_… | secret list; secret chmod if you should share it |
You committed sk- / whsec_ / a DSN | Wrong store | secret set or secret open, put ssec_… in config, rotate the leaked value |
You uploaded a .env | Workspace file, not a secret | secret open; delete the file |
See Send a file when the job is a CSV or zip, not a credential.