Secrets.
A secret is a token or a key you keep in your NaN account so your automations can use it without you pasting it anywhere: a GitHub token, an API key for one of your services, or your own key with an AI provider. You save it once, and then pick it by name in the automations, runs and settings that need it.
Secrets are write-only. Once saved, nobody can read a value back: not the portal, not the CLI, not the API, not you. You can replace a value, never see it.
To add one right away, go to Add a secret.
What a secret is
Every secret has:
| Part | What it is |
|---|---|
| Name | How you pick it, for example GITHUB_TOKEN. Capitals, digits and _, starting with a letter, up to 64 characters, unique in your account. It cannot be renamed: add a new secret and delete the old one. |
| Kind | Secret or Provider key. See Two kinds. Fixed when you create it. |
| Value | The token or key itself. Never shown again. |
| Description | Optional, for you: what it is for, which repository, when it expires. Up to 256 characters, one line. Never put the value here. |
Where it lives. The value is encrypted with a key that belongs to your account alone before it is stored, and only the encrypted value is kept. It is decrypted only when a run that was given it starts, and only the secrets that run was given. NaN never sends a secret anywhere to check it: a wrong or expired token shows up when a run uses it.
What you see instead of the value. The list in the portal, nan secrets ls and the API show the metadata only:
- A hint: the last 4 characters, as
…a1b2, when the value has at least 20 characters and those 4 are letters, digits,_or-. Otherwise no hint. NaN keeps no other trace of the value, not even a hash. - The size of the value, the description and, for a provider key, its provider and base URL.
- Used by: the automations that use it (or Not used by any automation).
- Updated, Last used (or never) and Last changed from. Last used is updated when a run receives the secret, at most once a minute.
Two kinds
| Kind | In the portal | What it is for |
|---|---|---|
generic | Secret | A token or value your automations receive as an environment variable: a GitHub token, a Slack webhook, an API key for your own service. |
provider_key | Provider key | Your own key with an AI provider, used as the inference of runs and automations instead of NaN inference (bring your own key, BYOK). |
To be told in Slack, Microsoft Teams or Discord when an automation needs you, you do not need a secret: add a notification channel. Its URL is kept write-only like a secret.
Secret
The value is saved exactly as you paste it, up to 16 KiB: it is never trimmed, so a pasted key is never silently changed. It must be text (UTF-8), with no control characters other than tabs and line breaks. Line breaks inside the value are fine (a PEM private key, for example), but not at its start or end.
Provider key
A provider key also has a Provider, fixed when you create it (to switch providers, add another key):
| Provider | In the portal | provider |
|---|---|---|
| Anthropic | Anthropic | anthropic |
| OpenAI | OpenAI | openai |
| Google Gemini | Google Gemini | gemini |
| Any endpoint that speaks the OpenAI API (Together, Groq, OpenRouter, your own server) | OpenAI-compatible | openai_compatible |
Its value is the key: one line without spaces, 8 to 4096 characters of plain ASCII. An OpenAI-compatible key also has a Base URL: the root of the endpoint, usually ending in /v1, for example https://api.together.xyz/v1. It is the only part of a secret you can change besides the value and the description.
The list shows it as Provider key · Anthropic (or its provider), with its base URL when it has one.
Base URL rules
NaN checks the base URL when you save it, and never connects to it: only the agent in your workspace does.
httpsonly, up to 512 characters. A port is fine (https://llm.example.com:8443/v1).- A public host name (with at least one dot) or a public IP address.
localhost, private and link-local addresses, and names ending in.local,.internal,.lan,.home.arpaor.svcare refused. - Not NaN’s own hosts (
nan.builders,helmcode.comand their subdomains): for NaN models, choose NaN as the inference. - No
user:password@, no query string (?...), no fragment (#...) and no spaces.
Never put a token in the base URL
The key goes in Value, where it is encrypted and masked. The base URL is not a secret: the portal, nan secrets ls and the API show it as it is, to you and to any credential that can read your secrets’ metadata. A URL with a token inside is refused when it has user:password@ or a query string, but any other way of hiding a token in it (a path segment, for example) would be shown in clear.
Manage secrets in the portal
Your secrets are in your settings at cloud.nan.builders: open Settings, then Secrets, below Tokens.
Add a secret
- Press Add secret and type the Name.
- Pick the Kind: Secret for a token your automations receive, Provider key for your own AI provider key. For a provider key, pick the Provider and, for OpenAI-compatible, type the Base URL.
- Paste the Value: up to 16 KiB for a secret, one line of 8 to 4096 characters for a provider key.
- Add a Description if you like, and press Save secret.
If saving fails, the portal clears the value for safety (“The value was cleared for safety. Paste it again to retry.”): fix what it says and paste it again.
Replace the value
Replace value, on the secret’s row, asks for the new value. The old one is overwritten and cannot be recovered. The name, the description and every automation that uses the secret stay as they are: When you replace or delete a secret says which runs get the new value.
Change the description or the base URL
Description edits the description. Base URL (only on an OpenAI-compatible provider key) edits its base URL, with the same rules. The name, the kind and the provider cannot be changed.
Delete a secret
Delete asks you to confirm, and the secret is gone for good. While an automation uses it, as one of its secrets or as its provider key, deleting is refused and the portal lists those automations: remove it from them first.
Manage secrets from the CLI
With the NaN CLI 0.1.33 or newer, where your account has the write API (it is being opened gradually):
nan secrets ls # names and metadata, never values
nan secrets show GITHUB_TOKEN # one secret's metadata
nan secrets set GITHUB_WRITE_TOKEN --value-file ./token.txt --description "repo acme/api, write"
gh auth token | nan secrets set GITHUB_READ_TOKEN --value-stdin
nan secrets set ANTHROPIC_KEY --value-file key.txt --kind provider_key --provider anthropic
nan secrets set TOGETHER_KEY --value-file key.txt --kind provider_key \
--provider openai_compatible --base-url https://api.together.xyz/v1
nan secrets edit GITHUB_WRITE_TOKEN --description "expires in December"
nan secrets rm GITHUB_READ_TOKEN
- The value never goes on the command line, where it would stay in your shell history and be visible to other programs on your machine.
--value-file PATHreads it from a file,--value-stdinfrom a pipe, and--value-stdinrefuses to read from the terminal, so a value is never typed in clear. If you typed a value on the command line by mistake, it is in your history now: revoke it where it was issued and set a new one. - If the file can be read by other users of your machine, the CLI warns you (
chmod 600it). - One line break at the end is dropped (files usually end with one).
setcreates the secret, or replaces the value of the one with that name.--kind(genericorprovider_key),--provider(anthropic,openai,geminioropenai_compatible) and the name are fixed when it is created.editchanges the description (--description ""clears it) or the base URL of an OpenAI-compatible key. Not the value: that isset.rmasks before deleting (answering no exits with code 1);--yesskips the question in scripts. It is refused while an automation uses the secret, and the CLI lists which.show,editandrmtake the secret’s name or its id.ls,show,setandedittake--jsonand print the metadata as JSON.- Until your account has the write API, the commands that change secrets tell you to use the portal and exit with code 64.
The CLI uses the first credential it finds: --token-file, then your nan auth login session, then NAN_TOKEN, then a token saved with nan auth login --api-token. With a token, it needs secrets:read to list secrets or find one (show, edit and rm look the secret up first, even by id), and secrets:write to change them. A token with only secrets:write can still create a new secret with set; replacing the value of an existing one needs both. An API key (sk-...) is refused before anything is sent.
Manage secrets from the API
The same operations are in the API, under https://api.nan.builders/v1/secrets:
| Method and path | Scope | What it does |
|---|---|---|
GET /v1/secrets | secrets:read | Lists your secrets: metadata only. |
GET /v1/secrets/{id} | secrets:read | One secret’s metadata. |
POST /v1/secrets | secrets:write | Creates a secret: {"name", "value", "kind"?, "provider"?, "base_url"?, "description"?}. 201, or 200 on an idempotent retry. |
PUT /v1/secrets/{id}/value | secrets:write | Replaces the value: {"value"}. |
PATCH /v1/secrets/{id} | secrets:write | Changes description and, on an OpenAI-compatible key, base_url. |
DELETE /v1/secrets/{id} | secrets:write | Deletes it. 204. |
Which credentials work:
- Your CLI session, from
nan auth loginon your own machine. - A platform token with the scope of the route. You create it in the portal, Settings → Tokens, signed in through your browser: a token with a
secrets:scope cannot be created from the CLI or by another token. The two scopes are independent:secrets:writedoes not includesecrets:read. See Tokens for automations. - Never an API key (
sk-...), the key inside a workspace or a session from an older CLI: they get403 credential_not_allowed(workspace_key_not_allowedfor the workspace key). A token without the scope gets403 token_scope_denied.
Where your account does not have the write API yet, every /v1/secrets route answers 404.
Create a secret from a file, so the value never appears in your shell history or in a process list:
jq -n --rawfile v token.txt '{name: "GITHUB_WRITE_TOKEN", value: ($v | rtrimstr("\n")), description: "repo acme/api, write"}' |
curl https://api.nan.builders/v1/secrets \
-H "Authorization: Bearer $NAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-github-write-token-1" \
--data-binary @-
The answer is the secret’s metadata. It never carries a value, by any route:
{
"id": "…",
"object": "secret",
"name": "GITHUB_WRITE_TOKEN",
"kind": "generic",
"provider": null,
"base_url": null,
"hint": "…a1b2",
"value_bytes": 93,
"description": "repo acme/api, write",
"used_by": [{ "id": "…", "name": "PR review api" }],
"updated_by": "platform_token",
"created_at": "…",
"updated_at": "…",
"last_used_at": null
}
- Idempotency.
Idempotency-Key(1 to 128 characters ofA-Z a-z 0-9 . _ : -) makes a retried create safe: the same key with the same name, kind and provider returns the secret already created (200), and the retried value is ignored. The same key for a different secret is409 idempotency_key_reused. Deleting the secret frees its key. Without the header, a retry gets409 name_taken. - Request body. One JSON object of up to 32 KiB, with only the documented fields.
- Errors come as
{"error": {"message", "type", "param", "code"}}.paramnames the field that is wrong and never repeats what you sent.secret_in_useandsecrets_too_largeadd theautomationsinvolved. - Rate limit. Changes to secrets (create, replace, edit, delete) share one allowance per account: a burst of 30, then 120 per hour, counting the portal, the CLI and the API together. Beyond it,
429 rate_limited.
Every route, body and error is in API reference → Secrets.
Use secrets in automations
A secret does nothing until you give it to an automation. Secrets reach only the runs of the automations you assign them to: a run you start with nan run or from a workspace’s Runs tab never receives any (except the provider key you choose for that run).
Assign a secret
In the automation’s Secrets card, Add a secret and pick, for each one:
- The secret, by name.
- The variable name the run sees it as. It can differ from the secret’s name, and it cannot be a name the runtime sets itself:
OPENAI_API_KEY,OPENAI_BASE_URL,HOME,USER,PATH,LANG,CREDENTIALS_DIRECTORYor anything starting withNAN_,PI_orHERMES_. - Who receives it:
| Option | API | Who gets the value | Use it for |
|---|---|---|---|
| Agent (read access) | agent | The agent, as an environment variable, in every run. | Tokens the agent needs to read things: clone a private repository, fetch a pull request, read CI logs. |
| Actions only (write access) | actions | Only the step that applies the actions an automation proposes. That step runs no agent and no AI model. The agent never sees an actions secret. | Write tokens: push, comment, review, label, open pull requests. |
From the CLI or the API, the same assignment is one line of the automation’s secrets:
secrets:
- { secret: GITHUB_READ_TOKEN, env: GITHUB_TOKEN, exposure: agent }
- { secret: GITHUB_WRITE_TOKEN, env: GITHUB_TOKEN, exposure: actions }
The same variable name can appear once per option. An automation takes up to 20 secrets, and everything one run receives (its secrets and its provider key) must add up to 48 KiB at most. A run only starts if every secret it needs can be read; otherwise it fails with secrets_unavailable and nothing runs without them. The rest of the automation’s settings are in Automations.
The GitHub token pattern
The usual GitHub setup uses two fine-grained personal access tokens, each limited to the one repository the automation works on, with an expiration date:
| Save it as | Permissions | Assign it as |
|---|---|---|
GITHUB_READ_TOKEN | Read only: Contents, plus Pull requests, Issues or Actions if the agent reads them. | GITHUB_TOKEN, Agent (read access). NaN also uses it to clone a private repository with Fresh clone per run. |
GITHUB_WRITE_TOKEN | What the actions need: Contents read and write to push, Pull requests read and write to open, comment on and review pull requests, Issues read and write to comment and label. | GITHUB_TOKEN, Actions only (write access). Required for an automation that acts on a repository. |
Both rows use the variable name GITHUB_TOKEN, each for its own receiver: the agent reads with one, NaN writes with the other. A public repository the agent only reads needs no read token. The details of each permission are in A GitHub token for actions, and a complete walkthrough in the PR gatekeeper guide.
Your own provider key
A provider key is not assigned as a variable: you choose it as the inference. In a run or an automation, set Inference to My own key, pick the Provider key and type the Model. The agent of that run gets the key in its environment, the way its agent expects it, and calls your provider with it. From the CLI, nan run --byok-secret ANTHROPIC_KEY --model claude-sonnet-4-5 "..." takes the secret’s name, never the key.
The whole flow, who can use it, billing and its own errors are in Use your own provider key (BYOK) and, for automations, Use your own provider key.
Keep your secrets safe
Masking in logs and events
When a run starts, every value it receives (its secrets, its provider key and the secrets of the workspace’s own environment file) is added to a mask. Before anything the run writes is stored or shown, each occurrence is replaced with [masked]:
- In the run’s events and log, its result and the free texts of a proposal: commit messages, pull request titles and bodies, comments, reviews and the paths of changed files.
- The value itself, and its common encodings: base64, URL-encoded and JSON-escaped.
- The start of a value printed on its own: if the agent prints the first characters of a token (
GITHUB_TOKEN=ghp_6Z8A...), a fragment of 8 characters or more is masked too.
Masking has limits. Values shorter than 8 characters are not masked at all, so keep real secrets long. Fragments shorter than 8 characters, the end of a value, or a fragment glued to more letters or digits can get through. A branch name or a label that would contain a secret is not rewritten: the proposal is refused (proposal_invalid). If the diff of a push contains the value of one of the run’s secrets, the portal warns you before you approve.
Masking protects what NaN stores and shows. It does not stop an agent that holds a secret from sending it somewhere.
What the agent can do with a secret
An agent that receives a secret as Agent (read access) can read it, print it, use it and send it over the network, like any program you run with that variable set. That is by design: it is how the agent clones, fetches and calls your services. What it does with it is your responsibility, so:
- Give the agent read tokens only. Keep every token that can write as Actions only: those never reach the agent or any AI model.
- Limit every token to the one repository or resource it is for, with an expiration date.
- Remember that text written by others reaches the agent. A webhook delivery, a pull request, an issue or a test in a pull request can try to make the agent print or send a secret (prompt injection). That is why Fresh clone per run ignores pull requests from forks and outside contributors by default: see Pull requests from forks and outside contributors.
- For a provider key, set a spending limit at your provider.
Write access is the dangerous part
A token that can write is worth much more to whoever steals it than one that can only read: it can push code, merge, delete branches or change settings.
- A GitHub write token in the agent’s hands can do anything its permissions allow, without going through the approval of your automation. Assign it as Actions only (write access), so only NaN’s apply step uses it, and only for the actions you allowed.
- A NaN platform token with
secrets:writecan replace any of your secrets (for example, swap the GitHub token an automation pushes with). One withautomations:writecan create an automation that receives every secret you have, with instructions of its choosing and nothing waiting for your approval: that is as good as reading them all. Give these scopes only to tokens that need them, with a short expiry, and never hand them to an agent. - Run
nan auth loginon your own machine, never inside a workspace: the agents that run there can read the session it saves, and a CLI session can change your secrets.
Rotate them
Prefer short-lived tokens, and rotate them before they expire or as soon as one may have leaked:
- Create the new token at its source (GitHub, your provider).
- Replace value in the portal, or
nan secrets set NAME --value-file new.txt. The name stays, so every automation keeps using it with no change. - Revoke the old token at its source. NaN cannot revoke a token for you: replacing it in NaN does not stop the old one from working elsewhere.
If a token leaked, revoke it at its source first, then replace it. Writing the expiry date in the Description (“expires 2026-12-31”) helps you see what is due.
When you replace or delete a secret
- Replacing a value takes effect at the next run that starts, including runs already queued: a run reads its secrets when it starts. Runs already working keep the value they started with. A replace is refused with
409 secrets_too_largeif the new value would push one of the automations that use it over 48 KiB per run. - Deleting is refused with
409 secret_in_usewhile an automation uses the secret, as one of its secrets or as its provider key. The answer lists those automations: change them first, then delete. A run on your own key whose provider key was deleted after it was queued fails withsecret_missing.
Who changed it last
Every secret records where its last change came from: Last changed from in the portal (Browser, CLI or API token), CHANGED FROM in nan secrets ls and updated_by in the API (browser_session, cli_session or platform_token). A change you did not make stands out: if you see one, revoke the tokens you no longer use in Settings → Tokens and replace the secret.
Limits
| Limit | Value |
|---|---|
| Secrets | 100 per member |
| Name | Up to 64 characters: capitals, digits and _, starting with a letter |
| Value of a Secret | Up to 16 KiB |
| Value of a Provider key | 8 to 4096 characters, one line, no spaces |
| Description | Up to 256 characters |
| Base URL | Up to 512 characters |
| Secrets per automation | 20 |
| Everything one run receives | 48 KiB, its provider key included |
| Changes to secrets | A burst of 30, then 120 per hour, counting the portal, the CLI and the API together |
| API request body | 32 KiB |
Adding a secret or replacing a value needs an active membership. Without one you can still list and delete your secrets.
If something goes wrong
| What you see | Why, and what to do |
|---|---|
400 invalid_request with a param | That field breaks a rule: name (capitals, digits and _), value (size, control characters, a line break at the start or end, or a provider key with spaces), kind, provider, base_url (rules), description or Idempotency-Key. |
403 credential_not_allowed | An API key (sk-...) or a session from an older CLI. Use your nan auth login session or a platform token with the scope. |
403 workspace_key_not_allowed | The key inside a workspace. It never reads or changes secrets. |
403 token_scope_denied | The token lacks secrets:read or secrets:write. Create one with the scope in Settings → Tokens. |
403 browser_session_required, or Browser session needed in the portal | Your account does not have the write API yet, so changes need a browser session: sign in again with the email link from your browser. |
403 platform_access_required | Adding or replacing needs an active membership. You can still list and delete. |
404 on every /v1/secrets route | Your account does not have the write API yet. Use the portal. |
404 not_found | No secret with that id in your account. Check with nan secrets ls. |
409 name_taken | You already have a secret with that name. Replace its value instead, or retry a create with the same Idempotency-Key. |
409 too_many_secrets | You have 100. Delete one to add another. |
409 idempotency_key_reused | That Idempotency-Key already created a different secret. Use a new key. |
409 secret_in_use | An automation uses it. Remove it from the automations listed, then delete. |
409 secrets_too_large | The new value would push the automations listed over 48 KiB per run. Use a shorter value or remove other secrets from them. |
429 rate_limited | More than a burst of 30 changes, or 120 per hour. Wait and try again. |
503 secrets_unavailable | Secrets are briefly unavailable. Try again in a few minutes. |
| The CLI exits with code 64 and tells you to use the portal | Your account does not have the write API yet. Use the portal. (The CLI also uses code 64 for most other refusals in this table: read the message it prints.) |
The automation cannot be saved without a GITHUB_TOKEN | An automation with a GitHub repository needs a secret assigned as GITHUB_TOKEN, Actions only (write access). |
A run fails with secrets_unavailable | Its secrets could not be read when it started. It is retried for a short while first. Start it again later. |
A run fails with secret_missing | A secret it needs was deleted after it was queued: its provider key, or a secret removed from the automation and then deleted. |
A run fails with config_error | Among other causes, its secrets add up to more than 48 KiB. Remove some from the automation. |
| A run fails with an authentication error from GitHub or your provider | The token is wrong, expired or revoked, or lacks a permission. Replace its value. |