Skip to content

Secrets ​

Secrets live on the host. Bottles refer to them by name. The gateway adds most of them to requests on the way out, and only env secrets reach the agent.

Summary ​

SecretStored inHeld at runtime byThe agent sees
secret:NAME env entrysecrets file, or typed at launchthe agent's env, and the orchestrator for scanningthe real value
secret:?<message> env entrytyped at launchthe agent's env, and the orchestrator for scanningthe real value
secret:${VAR} env entryyour shellthe agent's env, and the orchestrator for scanningthe real value
?<message> or ${VAR} env entrytyped at launch, or your shellthe agent's envthe value (it's an input, not a secret)
Literal env entrythe bottle filethe agent's envthe value (it isn't secret)
Route token (auth.value)secrets file (secret:NAME) or your shell (secret:${VAR})the gatewaynothing
Claude or Codex login (login: host)the harness's own login on the hostthe gatewaya placeholder
Model key (login: {oauth_token}, login: {api_key})secrets filethe gatewaya placeholder
Gitea token, or GitHub with --transport deploy-keysecrets filethe host, which mints a deploy key per repo for git-gatenothing
GitHub token, HTTPS transportsecrets filethe gateway's memory, for git-gatenothing
Canarymade per launchthe agent's env and the gatewaya decoy value
  • The secrets file is plain text, readable only by your user.
  • Bottles and agents name secrets. They never hold a value.
  • A value typed at a ? or secret:? prompt is never written to the secrets file. A missing secret:NAME is saved only if you agree.

Where secrets are stored ​

The secrets file ​

# ~/.bot-bottle/secrets
EXAMPLE_API_TOKEN=tok_live_abc123
BB_HOST_FORGE_GITEA_EXAMPLE_COM__ALICE_TOKEN=6f1c...

The file is ~/.bot-bottle/secrets. A named environment uses ~/.bot-bottle-envs/<name>/secrets, and BOT_BOTTLE_ROOT moves it.

  • One NAME=value per line. Blank lines and # comments are skipped.
  • The value is everything after the first =, taken literally. Quotes are part of the value.
  • A malformed line fails with its line number. The line itself is never printed.

Reserved names ​

Names starting with BB_HOST_ are bot-bottle's own. Only bot-bottle reads them, never a bottle or agent.

NameHolds
BB_HOST_CONSOLE_ACCESS_TOKEN, BB_HOST_CONSOLE_REFRESH_TOKENthe console's tokens for this host
BB_HOST_FORGE_<HOST>__<USER>_TOKENa forge user's token (see Forges)
  • A bottle naming one fails to load. That covers secret:NAME, secret:${VAR}, ${VAR}, a route's auth.value, a login and an env entry's own name.
  • A launch refuses one too, however the bottle was built.
  • Other names, such as BOT_BOTTLE_CLAUDE_OAUTH_TOKEN, are a bottle's to use.

bot-bottle refuses to read the file unless you own it and it has no group or other access. The error says to run chmod 600. bot-bottle's own writes replace the file atomically at mode 0600.

Who writes itWhat
bot-bottle setuppi's key: OPENROUTER_API_KEY from the OpenRouter sign-in, or, if you agree, the literal key in ~/.pi/agent/models.json as PI_<PROVIDER>_API_KEY
bot-bottle forge connecta forge's token (see Forges)
The paired consoleany name, through its Credentials tab
Youany line, with an editor

The file stays on the host. The orchestrator gets only ~/.bot-bottle/db/, which doesn't contain it.

Your shell ​

${VAR} and secret:${VAR} read your shell's environment. A ${VAR} found only in the secrets file is used, with a warning. secret:NAME reads only the secrets file. A launch from the console, or a child bottle, has no shell and reads only the file.

Harness logins ​

login: host reads the login your host's harness already keeps. It never copies it into the secrets file.

HarnessRead fromWhat's taken
Claude Code~/.claude/.credentials.json, else the macOS Keychain item Claude Code-credentialsthe OAuth access token, never the refresh token
Codex$CODEX_HOME/auth.json, else ~/.codex/auth.jsonthe access token of a ChatGPT or device login. An API-key login isn't forwarded.

How a bottle names a secret ​

Env entries ​

yaml
env:
  OPENAI_API_KEY: secret:OPENAI_API_KEY
  DB_PASSWORD: "secret:?Paste the staging DB password"
  NPM_TOKEN: "secret:${MY_NPM_TOKEN}"
  BRANCH: "?Which branch?"
  AWS_REGION: "${AWS_REGION}"
  NODE_ENV: development
FormValue comes fromSecret?When it's missing
secret:NAMENAME in the secrets fileyesa terminal launch asks once (hidden), then offers to save it. Any other launch fails, naming the file and NAME=<value>.
secret:?<message>asked at every launch, input hiddenyeswith nobody to ask, a saved value of the entry's name (secrets file, then environment) is used, with a warning
secret:${VAR}VAR in your shellyesthe launch fails, naming both variables
?<message>asked at every launch, input shownnoas secret:?
${VAR}VAR in your shellnoas secret:${VAR}
anything elsethe bottle file, as writtennonot applicable

NAME is letters, digits and _, not starting with a digit. A literal can't start with secret:. ${VAR} must be the whole value. prefix-${VAR} is a literal.

"Secret" means the value is push-scanned (8 or more characters), masked in transcripts and the console, and listed as secret in the trust prompt. A ? or ${VAR} entry is listed as input. One whose name looks secret (ends in KEY, TOKEN, SECRET, PASSWORD, PASSWD, PAT or CREDENTIAL(S)) warns that it "isn't marked secret".

An interactive launch asks for every ? and secret:? entry, even when a saved value exists, and says that secret:NAME would use it.

Route tokens ​

yaml
egress:
  routes:
    - host: api.example.com
      auth: {type: bearer, value: secret:EXAMPLE_API_TOKEN}

The gateway sends Authorization: Bearer <value> to that host. type: token sends Authorization: token <value>. type: header with a name puts the value, raw, in that header instead.

value is secret:NAME or secret:${VAR}. A plain ?, ${VAR} or secret:? is refused here. A missing or empty value fails the launch, naming the secret. A passthrough route can't carry a token. The route fields are in manifest.md.

Forges ​

sh
bot-bottle forge connect https://gitea.example.com

The token goes into the secrets file as BB_HOST_FORGE_<HOST>__<USER>_TOKEN, here BB_HOST_FORGE_GITEA_EXAMPLE_COM__ALICE_TOKEN. It's a reserved name, so no bottle can read it. The host database records the name (secret:NAME), never the value. bot-bottle forge remove drops the token with the connection.

  • Each part is upper-cased, with each run of other characters as one _. So __ only ever separates the host from the user.
  • Two forges or users that differ only in punctuation would share a name. Connecting the second is refused until the first is removed.

A bottle grants repos (gitea.example.com/acme/app:write) and never names a token. A bottle with a credentials: block, or a token in a forge description, fails to load. Details are in git-gate.md.

A github.com token from the bot-bottle GitHub App expires. The file keeps _REFRESH, _EXPIRES and _CLIENT_ID lines beside it, and the host renews it.

Host logins and model keys ​

yaml
agent_provider:
  harness: claude
  login: host
Agent settingThe gateway injectsThe agent holds
login: host (Claude)your Claude loginCLAUDE_CODE_OAUTH_TOKEN=egress-placeholder
login: {oauth_token: secret:NAME} (Claude)a claude setup-token token from secret NAMEthe same placeholder
login: {api_key: secret:NAME} (Claude)an Anthropic API key, raw in x-api-keyANTHROPIC_API_KEY=egress-placeholder
login: host (Codex)your Codex login, to api.openai.com and chatgpt.coman auth.json with placeholder tokens and the real expiry
login: {api_key: secret:NAME} (pi)the model provider's key, as a Bearer tokenapiKey: egress-placeholder

A missing or expired host login fails the launch and says what to run on the host. See install.md.

Where each value goes ​

The CLI resolves each name at launch and hands the values to the host controller, which hands them to the backend.

  • Env values go into the agent's environment.
  • Injected tokens go to the orchestrator, which serves them to the gateway per request.
  • Secret env values (secret:...) of 8 or more characters and the canary go to the orchestrator too, so git-gate can scan pushes for them.

The orchestrator stores what it's given in its registry.db, encrypted under a random per-bottle key. That key (ENV_VAR_SECRET) is kept in the agent's guest, not with the database.

The host reads the key back to restore a bottle's tokens when the infra restarts. The agent has the key but can't reach the database.

Env values never go on a host command line, where other host users could read them. On docker and macos-container they reach the agent through the launching process's environment. On firecracker they are sent over the ssh connection's input and written to a file inside the VM (backends.md).

What protects them ​

ProtectionCoversDoesn't cover
Gateway injectionroute tokens, host logins, model keys and forge tokens never enter the agentenv entries, which the agent holds
Credential strippinga key the agent adds itself is removed on inspected routespassthrough routes
Outbound DLP known_secretsinjected tokens in any request on an inspected route, including encoded and split formsenv values, which may go to their own host
Canaryany request carrying it on an inspected route is a 403, recorded for bot-bottle resolve-canarypassthrough routes
git-gate push scaninjected tokens, the canary and secret env values of 8 or more characters, anywhere in a push? and ${VAR} inputs, and shorter secret values
gitleaks on pushesanything with a known token shapeshapeless values
Trust prompteach route auth.value, forge token, secret: entry (listed as secret), ? and ${VAR} entry (listed as input), and provider login or key
Bottle file guarda saved version or a console edit holding a secrets-file value is refusedvalues shorter than 8 characters
Consolesees names only. Transcripts mask secret values and token shapes.? and ${VAR} inputs. Typed and shell secrets after the host controller restarts and re-adopts a running bottle.

Launch summaries, prompts and errors name variables, never values (env.py, secret_source.py).

Limits ​

  • The agent can send its own env values, secret or not, to any allowlisted host. Egress doesn't block them.
  • A passthrough route is an unscanned tunnel. Anything the agent holds can go there.
  • ? and ${VAR} inputs aren't scanned anywhere. Mark anything sensitive secret:.
  • The secrets file is plain text. File permissions are its only protection, so anyone with your user account can read it.
  • The console can set a secret but not read one. A compromised console could still point a route at a host of its choosing and send a secret there. See What pairing lets the console do.
  • A compromised gateway or orchestrator holds every running bottle's tokens. See the threat model.

Changing and removing a secret ​

To change a value, edit the file, set it from the console, or run forge connect again. Most kinds take effect at the next launch.

SecretHow to change itRunning bottles
Env entries, route tokens, login keysedit the file or your shellkeep the old value. Stop and resume to pick up the new one.
Claude or Codex loginlog in again on the hostupdated within seconds, or five minutes for a Keychain login. See Forwarded host logins.
GitHub token, HTTPS transportbot-bottle forge connect github, or automatic renewalthe gateway gets the new token within four minutes
Gitea or deploy-key tokenbot-bottle forge connect <url> againkeep the deploy keys minted at launch

A resume resolves every secret again. A bottle with preserve_container on docker or macos-container keeps the env it was created with. bot-bottle resume --fresh <bottle> starts a new guest that reads the new values (backends.md).

To remove a secret, delete its line from the file. No command removes one, and the console can only set values. bot-bottle uninstall --purge deletes the whole ~/.bot-bottle, secrets file included.

Planned ​

An encrypted vault is planned and not yet available. It will keep secrets in the OS keychain and sync them end-to-end encrypted between your hosts. Until then, the secrets file is the store.

Apache 2.0