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.
Every secret is bound to a web origin (scheme + host, for example https://api.stripe.com). HTTP substitution decrypts the token only when the request URL matches that origin. The same token sent to a different host fails closed and does not leak the value.
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 only against the origin you set. secret list shows the token and its allowed origin.
Worked example: GitHub token for curl
Job: call GitHub as the owner without putting a PAT in the repo.
--origin is required when you create a secret. Use scheme + host only (and a non-default port if any). No path, query, or wildcards.
secret open --origin https://api.github.com github-token
Expected output (shape):
Open this URL to submit the secret:
https://…/secrets/intake/…
Intake: 8c1e…
- Open the URL. Sign in as a repository owner on that intake page. A different user is rejected.
- Paste the PAT. Submit.
- Back in the shell:
secret poll 8c1e…
# prints: ssec_a1b2c3d4...
Use the token. Exact ssec_… is replaced at call time when the destination matches the origin:
curl -s -H "Authorization: Bearer ssec_a1b2c3d4..." https://api.github.com/user
# curl to a different host with the same token fails closed
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 --origin https://api.example.com "sk-live-..."
# prints: ssec_…
# or
printf '%s' "$KEY" | secret set --origin https://api.example.com --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.
Origin binding
Create always takes --origin. Existing tokens that have no origin cannot be used in HTTP until you set one:
secret chmod ssec_a1b2c3d4... --origin https://api.example.com
HTTP substitution fails closed when the destination origin does not match, the secret has no origin, or you lack access to the token. Errors never include the secret value.
SSH identity keys (ssh -i ssec_…) and foreign Postgres DSNs (createdb --url ssec_…) decrypt without an HTTP origin check (they never go over curl). The origin still blocks using that same token in HTTP to another host.
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 (destination = MCP server URL) gitHTTPS--token/ URL userinfo (destination = remote URL)- Webhook
--secretand similar config - SSH identity:
ssh user@host -i ssec_… - Foreign Postgres:
createdb app.db --url ssec_…
Handlers also auto-source a folder .env of KEY=ssec_… (or plain values). Tokens stay unsubstituted until an outbound call. repo env secret refs stay as ssec_… in the process environment so curl -H "Authorization: $KEY" still goes through origin checks.
OAuth MCP (mcp connect) and integration connect store their own tokens. You do not secret set those.
Common uses
| Task | Example |
|---|---|
| MCP server with API key | secret set --origin https://mcp.example.com "…" then mcp add … --header "x-api-key=ssec_…" |
| Stripe webhook signing secret | secret set --origin https://api.stripe.com "whsec_…" then webhook add … --secret ssec_… |
| GitHub HTTPS PAT | secret set --origin https://github.com "ghp_…" then git clone … --token ssec_… |
| Any outbound HTTP from bash | Pass ssec_… in headers or body; curl resolves it only for the allowed origin |
Flags
secret help
| Command | Who | What it does |
|---|---|---|
secret open --origin URL [LABEL] | owner | Print intake URL immediately |
secret poll INTAKE_ID | owner | Wait for the browser submit; print ssec_… |
secret open --origin URL --wait [LABEL] | owner | Block until submitted. Avoid in bash_exec |
secret set --origin URL VALUE | owner | Store from the shell; print ssec_… |
secret set --origin URL --stdin | owner | Read the value from stdin |
secret list | member | Tokens you can see (includes allowed origin) |
secret chmod ssec_… --origin URL | creator or owner | Set or replace the allowed origin |
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 |
|---|---|---|
Create rejected: missing --origin | Origin is required | secret open --origin https://api.example.com LABEL |
| curl / MCP fails closed after create | Destination host ≠ secret origin | Match --origin to the real API host, or secret chmod ssec_… --origin … |
| Token has no origin | Created before origin binding | secret chmod ssec_… --origin https://api.example.com |
| Intake page rejects you | Intake needs a repository owner | Sign in as an owner on the printed URL |
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 |
You cannot use that ssec_… | Visibility or missing access | secret list; secret chmod if you should share it |
You committed sk- / whsec_ / a DSN | Wrong store | secret set --origin …, put ssec_… in config, rotate the leaked value |
You uploaded a .env | Workspace file, not a secret | secret open --origin …; delete the file |
See Send a file when the job is a CSV or zip, not a credential. See SSH to a host when the secret is an SSH identity key.