These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

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…
  1. Open the URL. Sign in with platform OAuth on that intake page. Admin login alone does not authorize it.
  2. Paste the PAT. Submit.
  3. 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...
VisibilityWho can use it
owner (default)Creator only
groupCreator + --group NAME
membersAny 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:

  • curl URL (whole-token only), -H values, and -d / --data bodies
  • MCP server headers / auth and tool --json arguments
  • Webhook --secret and 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
CommandWhoWhat it does
secret open [LABEL]ownerPrint intake URL immediately
secret poll INTAKE_IDownerWait for the browser submit; print ssec_…
secret open --wait [LABEL]ownerBlock until submitted — avoid in bash_exec
secret set VALUEownerStore from the shell; print ssec_…
secret set --stdinownerRead the value from stdin
secret listmemberTokens you can see
secret chmod ssec_… --visibility …creator or ownerChange who can use it
secret unset ssec_…creator or ownerRevoke

--timeout SECS applies to secret open / secret poll.

Failure modes

What you seeWhat it meansWhat to do
Intake page rejects you after Admin loginIntake needs platform OAuth on the MCP public URLSign in on the printed URL, not only Admin
secret open deniedOwner-onlyAn owner runs it, or elevate that goal
secret poll times outNobody submitted, or timeout elapsedsecret open again
curl / MCP fails closedYou cannot use that ssec_…secret list; secret chmod if you should share it
You committed sk- / whsec_ / a DSNWrong storesecret set or secret open, put ssec_… in config, rotate the leaked value
You uploaded a .envWorkspace file, not a secretsecret open; delete the file

See Send a file when the job is a CSV or zip, not a credential.