# Secrets

Encrypted credentials stored in Supabase Vault, scoped to a house. Storing one is the whole job: at dispatch every house secret is decrypted and exported as an env var into the sandbox, for `run_command` and for the coding agent behind `delegate_task`. An environment can restrict its boxes to a named subset (`secrets: { only: [...] }`, see [environments](../sandboxes/environments.md)).

```
secrets {
  house_id, name,              # UNIQUE(house_id, name) where deleted_at is null
  author_id,                   # who created it; an owner or this author may rotate or delete
  vault_secret_id,             # encrypted value lives in Vault, never in the table
  funded_by, provider_key_hash # 'arbe' + key hash when arbe minted it (LLM keys)
  deleted_at                   # soft delete
}
```

**Secrets belong to the house, not the person.** Any member can use any secret in the house, and a name means one value, whoever created it. `author_id` gates nothing but rotation and deletion. Names are immutable: `set` on an existing name rotates the value, and renaming is delete and recreate.

**What reaches a sandbox.** A bot with `run_command` can read the box's entire environment with a bare `env`, so the injected set is the blast radius. `buildBoxEnv` (`packages/core/dispatch/sandbox-shared.ts`) is the one place deciding it, with two rules:

1. Every house secret by default, or the environment's `only` subset.
2. House tier only, including arbe-funded keys. Operator infrastructure keys never enter a box.

That means the house's model key, BYOK or arbe-funded, is readable inside the sandbox by design; the funded key is capped, so this is accepted rather than proxied ([LLM keys](llm-keys.md)). A house without a model key gets `No model key for this house. Add OPENROUTER_API_KEY under house secrets.` A name in an `only` list with no secret behind it is left out and reported by `arbe env diagnose`; dispatch never refuses over it.

```sh
arbe secret list                          # names, rotation age, funding provenance
arbe secret set <name>                    # upsert from stdin; same name = rotate
arbe secret delete <name>                 # soft delete
echo "sk-or-..." | arbe secret set OPENROUTER_API_KEY
```

**GitHub.** You don't need to know the secret name. `arbe connection create github` (or Settings → Runtime → Connections) stores the token as `GITHUB_TOKEN` and also signs `git` and `gh` in inside every box (`packages/core/dispatch/connection-env.ts`).

**Nothing ever returns a value.** Values are read from stdin, never argv. The API (`GET/POST /api/secrets`, `GET/DELETE /api/secrets/:id`, `PUT /api/secrets/:id/value`) and `arbe secret view` are metadata only. Decryption happens only in `resolve_secrets_for_scope`, a service-role Postgres function the backstage calls in-process during a turn it already holds the fence for.

**Storage.** Supabase Vault (pgsodium AEAD, Supabase-managed key). Soft-delete keeps the metadata row and hard-deletes the Vault entry; a trigger cascades when a house is soft-deleted.

Code: `packages/core/schemas/secret.ts`, `apps/www/src/routes/api/secrets/`. See [environments](../sandboxes/environments.md), [dispatch](../chat/dispatch.md), [auth](auth.md).
