Store a secret — Sails

[Sails](https://sails.app/)/[Guides](/guides/)

[Contact](https://sails.app/contact)    

 On this site

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at [sails.app](https://sails.app/) or [Add Sails](https://sails.app/connect).

# 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.

```bash
secret open github-token
```

Expected output (shape):

```text
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:

```bash
secret poll 8c1e…
# prints: ssec_a1b2c3d4...
```

Use the token. Exact `ssec_…` is replaced at call time:

```bash
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.

```bash
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).

```bash
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

`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:

*   `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

```bash
secret help
```

CommandWhoWhat 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 seeWhat it meansWhat 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 `upload`ed a `.env`

Workspace file, not a secret

`secret open`; delete the file

See [Send a file](/guides/how-to/send-a-file) when the job is a CSV or zip, not a credential.
