---
url: https://docs.repoos.org/environment-and-secrets.md
---
# Environment and secrets

RepoOS separates configuration into three buckets so a project stays safe and
predictable:

| Place | Good for | Committed? |
| --- | --- | --- |
| `repoos.toml` (repo root) | project behavior — board, previews, checks, releases | Yes — git-tracked |
| `.env` (repo root, gitignored) | secrets that belong to this repository | No |
| machine / service environment | machine-wide provider keys and infrastructure credentials | No |

**No secret belongs in `repoos.toml`.** Set provider and session secrets in
`.env` or the machine environment instead. The one secret field the Settings UI
will write to the file is `whisper.apiKey` — and you should still prefer
`REPOOS_WHISPER_KEY` in `.env`. The full list of project settings is in the
[repoos.toml reference](/configuration).

## What gets loaded, and in what order

RepoOS reads the repo-root `.env` at startup, before resolving any value that can
come from the environment. The loader is intentionally conservative:

1. Real shell or service environment variables win.
2. A repo-root `.env` fills in only values the environment hasn't already set.
3. `repoos.toml` is the committed source of non-secret config.
4. For fields that have an environment form, the environment overrides the
   `repoos.toml` value (for example `REPOOS_RESEND_API_KEY` fills
   `[auth.emailProvider]`, and `NTFY_BASE_URL` overrides `ntfyBaseUrl`).

The `.env` parser is deliberately minimal: one `KEY=value` per line, blank lines
and `#` comments ignored, optional surrounding single or double quotes stripped.
There is no `export` keyword and no variable interpolation. A key already present
in the process environment is never overwritten by the file.

RepoOS provider/auth/infrastructure credentials and application project secrets
have different consumers and management flows. Root `.env` loading configures
the RepoOS process; named environments are injected only into explicitly bound
local child processes.

## Project secrets in `.env`

The legacy root `.env` behavior above remains in effect for RepoOS configuration.
Named project environments are a separate application-secret store. The storage foundation for that
model is documented in [ADR 0010](../docs/adr/0010-named-project-environments-and-file-backed-secrets.md).
Environment names and source files must be registered deliberately; RepoOS
does not discover `.env*` files, infer names from filenames or `NODE_ENV`, or
load these project values into its own provider/auth process environment.
Registered sources have ordered precedence (later sources win); duplicate keys
inside one source are rejected. `.env.example` contributes expected key names
only. File writes preserve unrelated lines and comments, use a stale-content
check, and reject tracked, unsafe, or symlinked destinations. The existing
startup loader remains unchanged for projects that have not opted in.

Named values reach local child processes only through an explicit binding.
The **Secrets** entry is always available between Dev and Agents in the sidebar
and mobile tabs, including in a new project with no environments yet, so you
can create its first environment. Its selector changes the environment being
viewed; it does not bind or switch any process. Bind a consumer and grant only
the keys it needs:

In the **Secrets** page, create an environment to make an empty ignored source
file, then add or replace values through masked inputs and an explicit Save.
Every key renders as one compact line — status dot, monospace key name, short
badges (inherited, public prefix, pending restart), and a decorative masked
placeholder — with the per-key actions (replace value, remove key) behind a
three-dot options menu; removing a key still asks for confirmation. The list
header shows the key count, a search field for key names, and the add action.
The page shows expected names from `.env.example` plus each key's source,
consumers, visibility, and restart state in the key's hover/focus tooltip; it
never reads example values into the form or reveals saved values. Keys provided
only by an earlier source are read-only in the page because writes and removals
apply to the final writable source. A concurrent file edit requires reloading
the inventory before retrying.

Because values are ordinary dotenv text on this machine, an **Open in editor**
link next to the environment's file name opens that registered source file in
your configured editor (`editor.command`, or
`dev.inspector.editorCommand`). It appears only on a local connection, only
when an editor command is configured, and only when the source file exists —
never through a tunnel or remote session, and the browser never receives any
value.

```sh
repoos secrets bind dev:web dev-nick DATABASE_URL STRIPE_SECRET_KEY
repoos secrets bind preview:web integration DATABASE_URL
repoos secrets bind check:default integration DATABASE_URL
```

Bindings are stored in the local, gitignored
`.repoos/environment-bindings.json` and are scoped to `dev:<target>`,
`preview:<target>`, or `check:default`. Target names are normalized to
lowercase hyphenated IDs. Binding or value changes require a running consumer
to restart before it uses the new value. Unbound consumers receive no named
project values. Granted keys missing from the selected environment are removed
from the child's inherited environment; there is no fallback to another named
environment, including `production`. Machine/service keys (`REPOOS_*` and the
reserved credential and process keys below) cannot be granted. Named values are injected into
child processes only; RepoOS never installs them into its own `process.env`.
Names beginning `VITE_`, `NEXT_PUBLIC_`, or `PUBLIC_` are labeled **public**
because frontend tooling commonly embeds them in client assets. The prefix is a
visibility warning, not an access control. Other names are labeled
server-only, which describes intended use rather than a sandbox guarantee.

### Reserved project keys

Project environment writes reject these credential and process keys
(case-insensitively), in addition to every name beginning with `REPOOS_`:

`OPENAI_API_KEY`
`GROQ_API_KEY`
`REPOOS_WHISPER_KEY`
`REPOOS_RESEND_API_KEY`
`REPOOS_GOOGLE_CLIENT_SECRET`
`REPOOS_AUTH_SESSION_SECRET`
`REPOOS_AUTH_DEV_BACKDOOR_CODE`
`REPOOS_SECRET_STORE_KEY`
`REPOOS_TELEGRAM_PROVISIONING_KEY`
`CLOUDFLARE_API_TOKEN`
`HETZNER_API_TOKEN`
`PATH`
`HOME`
`NODE_OPTIONS`
`PORT`
`HOST`
`REPOOS_OPENROUTER_API_KEY`
`TYPESAFE_API_KEY`
`REPOOS_CLEF_FLASH_API_KEY`
`REPOOS_OPENCODE_GO_API_KEY`
`REPOOS_DEEPINFRA_API_KEY`

`repoos check` applies `check:default` to local check-step children. Remote
validation does not receive project values from these bindings. This limits
what RepoOS injects; it is not a security sandbox against an unrestricted local
agent or process.

Register environments in `.repoos/environments.json`, for example:

```json
{
  "environments": [{
    "id": "dev",
    "sources": [{ "id": "local", "path": ".env.dev", "examplePath": ".env.example" }]
  }]
}
```

`repoos secrets list dev` shows key names, set/missing status, source,
visibility, and a content version for safe concurrent edits. Use
`repoos secrets add|replace|delete dev KEY`; add and replace read the value
from stdin or a hidden terminal prompt. Values are never returned by the API
or printed by the CLI. Writes and deletes apply only to the **last** registered
source file; keys that appear only from an earlier source are read-only in the
API, CLI, and UI. Both replace and delete return a conflict until the key is
present in the writable source. Each mutation includes the metadata version it
was based on; a concurrent edit returns a conflict and requires reloading the
inventory. Metadata reads are available to signed-in members; mutations require
an authenticated admin, reject mismatched browser origins, and write value-free
audit entries to `.repoos/secret-audit.ndjson`.
Fixed RepoOS credentials remain managed through their dedicated settings and
provider routes. A project environment does not sync values to a provider or
deployment platform; deployment bindings are local metadata only and do not
publish or upload values.

Use a gitignored `.env` at the repository root for RepoOS provider keys and
session secrets. Use named project environments for application credentials
such as a project's `DATABASE_URL`.

```bash
# Auth
REPOOS_RESEND_API_KEY=re_...
REPOOS_GOOGLE_CLIENT_SECRET=...
REPOOS_AUTH_SESSION_SECRET=...
REPOOS_AUTH_DEV_BACKDOOR_CODE=your-local-code

# Model providers and voice transcription
REPOOS_OPENROUTER_API_KEY=...
REPOOS_OPENCODE_GO_API_KEY=...
REPOOS_DEEPINFRA_API_KEY=...
REPOOS_WHISPER_KEY=...

# Provider fallbacks for whisper
GROQ_API_KEY=...
OPENAI_API_KEY=...
```

Keep placeholders only in documentation and examples. Never copy a real value
from a production machine into a repo, an issue, or a shared example.

## Machine or service environment

Use the machine or service environment for credentials that are not unique to
one repo — a shared provider token, a host-level SSH key path, a self-hosted
service URL:

```bash
# Shared infrastructure credentials
CLOUDFLARE_API_TOKEN=...
HETZNER_API_TOKEN=...
REPOOS_REMOTE_SSH_KEY=/absolute/path/to/private_key

# Self-hosted ntfy server
NTFY_BASE_URL=https://ntfy.example.com
```

This is the right place for a machine-wide Cloudflare or Hetzner token: it does
not belong in the repo and should not be copied into every checkout. The
repo-root `.env` is better for per-project secrets that only this repository
should see. Prefer your platform's secret store — systemd `Environment=`,
launchd, Docker secrets, or a CI secret store — over a file on disk for
machine-wide credentials.

## What file-backed means

Named project values are ordinary dotenv text in gitignored files. They are
**not encrypted at rest** and are not an encrypted vault. Restricted file
permissions where supported, atomic writes, and refusal of tracked or unsafe
destinations reduce accidents; they do not protect against a user,
administrator, backup system, malware, or another local process that can read
the files. Use an operating-system or hosted secret manager when that level of
protection is required.

Version 1 is write-only through RepoOS: the UI and API report names,
set/missing state, source, visibility, and grants, but never return saved
values — the Secrets page's masked column and options menu hold no reveal or
copy control, by design. A local administrator who can read the source file can
still retrieve them, which is what the page's "Open in editor" link (local
connections only) is for: edit the file directly in your own editor. Explicit bindings grant selected keys to a local dev, preview, or check
consumer. Worktrees do not inherit production access, and remote validation
receives no project values. This limits RepoOS-managed injection only. Agents
and commands run as local processes with the user's OS permissions; unrestricted
processes can read other accessible files, inherit ambient machine values, or
use the network.

## Supported environment variables

These are the variables RepoOS reads for users. Everything else with a `REPOOS_`
prefix is internal (see below).

### Auth and login

| Variable | Purpose |
| --- | --- |
| `REPOOS_RESEND_API_KEY` | API key for the Resend email OTP provider. |
| `REPOOS_GOOGLE_CLIENT_SECRET` | Client secret for Google OAuth. |
| `REPOOS_AUTH_SESSION_SECRET` | Session-signing secret. Auto-generated at startup if unset, so you don't have to set one. |
| `REPOOS_AUTH_DEV_BACKDOOR_CODE` | Local-only code that replaces the emailed OTP on a dev instance. Ignored when `NODE_ENV=production`. |

These belong in `.env`, never in committed config — the Settings API refuses to
write them to `repoos.toml`. See [Authentication](/authentication).

### Encrypted credential storage

| Variable | Purpose |
| --- | --- |
| `REPOOS_SECRET_STORE_KEY` | 32-byte key encoded as 64 hexadecimal characters or base64, used to encrypt and decrypt recoverable credentials. Missing or invalid keys fail closed; there is no plaintext fallback. |
| `REPOOS_TELEGRAM_PROVISIONING_KEY` | Key the repo instance presents to the official Telegram provisioning service (#0559) when starting/redeeming a managed provisioning request. Leave unset until the service is deployed; Bring Your Own Bot Token never uses it, and a bot token itself is never set through the environment. |

Keep this key in the server environment and out of task worktrees. Changing it
requires rewrapping existing records while the old key is still available;
losing it makes those credentials unrecoverable. See [Telegram](/telegram).

### Attachment storage (Neon Object Storage)

| Variable | Purpose |
| --- | --- |
| `REPOOS_NEON_STORAGE_ENDPOINT` | The branch's S3 endpoint URL for Neon Object Storage. |
| `REPOOS_NEON_STORAGE_REGION` | The branch's S3 region, e.g. `us-east-2`. |
| `REPOOS_NEON_STORAGE_BUCKET` | The bucket attachment objects are stored in. |
| `REPOOS_NEON_STORAGE_ACCESS_KEY_ID` | The storage credential's S3 access key id. |
| `REPOOS_NEON_STORAGE_SECRET_ACCESS_KEY` | The storage credential's S3 secret access key. |
| `REPOOS_NEON_STORAGE_PREFIX` | Optional key prefix isolating this instance's objects. |

These are only read when `[storage] provider = "neon"`; a repo that never
configures cloud storage never needs them. They live in `.env`, never in
committed config, and are the credential half of the model — the `repoos.toml`
key selects the provider, the environment supplies how to reach it. Verified
endpoint/limit/pricing details: [`docs/neon-object-storage.md`](https://github.com/repoos/repoos/blob/main/docs/neon-object-storage.md).

### Model providers and voice transcription

| Variable | Purpose |
| --- | --- |
| `REPOOS_OPENROUTER_API_KEY` | OpenRouter key used for the Agents page's live spend view. |
| `REPOOS_OPENCODE_GO_API_KEY` | OpenCode Go key used for the Agents page's live spend view. |
| `REPOOS_DEEPINFRA_API_KEY` | DeepInfra key used for the Agents page's live credit/usage view. |
| `REPOOS_WHISPER_KEY` | API key for the configured voice-to-text provider. |
| `GROQ_API_KEY` | Fallback key used when `[whisper] provider = "groq"`. |
| `OPENAI_API_KEY` | Fallback key used when `[whisper] provider = "openai"`. |

RepoOS prefers the RepoOS-specific name when present. The generic provider keys
are provider-aware fallbacks for whisper — an `OPENAI_API_KEY` is never sent to
Groq, or the reverse.

### Infrastructure and remote validation

| Variable | Purpose |
| --- | --- |
| `CLOUDFLARE_API_TOKEN` | Required by Cloudflare Tunnel and Access setup. |
| `HETZNER_API_TOKEN` | Required by the remote validation runner. |
| `REPOOS_REMOTE_SSH_KEY` | Absolute path to the private SSH key the remote validation runner uses. |

### Notifications

| Variable | Purpose |
| --- | --- |
| `NTFY_BASE_URL` | Overrides `ntfyBaseUrl` in `repoos.toml` for a self-hosted ntfy server. |
| `REPOOS_PUBLIC_URL` | Absolute base URL for task deep links in outbound Telegram notifications (for example `https://repo.example.com`). When unset, RepoOS uses the first configured tunnel hostname or the control-plane `publicOrigin` after the server binds. |

### Runtime and build

| Variable | Purpose |
| --- | --- |
| `REPOOS_RUNTIME` | `auto` (default), `bun`, or `node`. |
| `REPOOS_BUN_PATH` | Explicit path to a Bun binary. Only set it if Bun is installed somewhere not on `PATH` — a set-but-missing path makes RepoOS fall back to Node rather than searching `PATH`. |
| `REPOOS_STRICT_BUILD` | `1` makes a stale build exit with an error, the same as `strictBuild = true` in `repoos.toml` (or the `--strict-build` flag). |

These are the supported public controls for how RepoOS starts itself and treats
a stale build. See
[Configuration → Worktrees and runtime](/configuration#worktrees-and-runtime)
and [Server and UI](/configuration#server-and-ui).

## Internal, test, and reload variables — do not set these

The codebase uses many other variables for process management, the reload loop,
and the check pipeline. They are implementation details, not project settings,
and should not be copied into `.env` as if they were configuration.

Examples: `REPOOS_AGENT`, `REPOOS_RELOAD`, `REPOOS_PREVIEW_CHILD`,
`REPOOS_PROCESS_TITLE`, `REPOOS_RUNTIME_REEXEC`, `REPOOS_CHECK_CHANGED`,
`REPOOS_TEST_WORKERS`, `REPOOS_SKIP_TESTS`, `REPOOS_REMOTE_VALIDATION_DONE`,
`REPOOS_FORCE_BUILD`,
`REPOOS_SKIP_BUILD`, `REPOOS_STRICT_TIMING`, `REPOOS_TASK_ID`, `REPOOS_RUN_ID`,
`REPOOS_NO_WORKTREE_GC`, `REPOOS_HANDOFF_DEADLINE_MS` and `REPOOS_LOCAL_CHECK_TIMEOUT_MS` (raise both when local handoff gates run long), and the legacy emergency override `REPOOS_NO_AUTO_RELOAD` (`1` stops the server restarting itself when `dist/` changes). The normal control is **Settings → General → Automatic server reload** (`autoReload` in `repoos.toml`).

If you need a project setting, use `repoos.toml` or a supported override
documented above. If a variable is only used to drive RepoOS's own processes,
leave it to the runtime.

## Rotation and safe placeholders

* Rotate a key as soon as it is exposed in git, CI logs, a screenshot, or a
  ticket attachment.
* Keep `.env` out of git and keep a tracked `.env.example` with placeholders
  only.
* Never commit a real key, a half-redacted secret, or a placeholder that looks
  like a real credential.
* Prefer generated or per-host secret management (systemd, launchd, Docker, a
  CI secret store) for machine-wide credentials.
* After rotating, restart `repoos serve` so the new value is loaded.

## `.env.example`

A tracked `.env.example` is the safe contract for a repo: it shows which secrets
are expected without revealing real values.

```bash
# Copy to .env and fill in what this repo needs. `.env` is gitignored; this
# file is tracked so the project documents which secrets are expected without
# exposing real values.
#
# RepoOS loads `.env` from the repo root at startup; real shell/service env vars
# still take precedence over it. No key belongs in repoos.toml. Full contract:
# user-docs/environment-and-secrets.md

# Auth / login
REPOOS_RESEND_API_KEY=re_...
REPOOS_GOOGLE_CLIENT_SECRET=...
REPOOS_AUTH_SESSION_SECRET=...
REPOOS_AUTH_DEV_BACKDOOR_CODE=your-local-code

# Encrypted credential storage (Telegram bot tokens and other recoverable secrets)
# Required to store/read secrets: 32 bytes encoded as 64 hex characters or base64.
# Missing or invalid keys fail closed; there is no plaintext fallback.
# REPOOS_SECRET_STORE_KEY=<32-byte-key-as-hex-or-base64>

# Telegram managed provisioning (#0559 service) — key the instance presents to
# the official provisioning service when redeeming a project bot credential.
# Leave unset until the service is deployed; BYO bot tokens never live here.
# REPOOS_TELEGRAM_PROVISIONING_KEY=...

# Attachment storage — Neon Object Storage (#0660). Only read when
# [storage] provider = "neon" in repoos.toml. The endpoint/region/bucket come
# from the branch's storage state in the Neon Console; the key id and secret
# come from a storage-scoped credential (neon credentials create --scope
# storage:read --scope storage:write). Never committed: .env only.
# REPOOS_NEON_STORAGE_ENDPOINT=https://br-….storage-c-2.us-east-2.aws.neon.tech
# REPOOS_NEON_STORAGE_REGION=us-east-2
# REPOOS_NEON_STORAGE_BUCKET=repoos-attachments
# REPOOS_NEON_STORAGE_ACCESS_KEY_ID=nak_live_...
# REPOOS_NEON_STORAGE_SECRET_ACCESS_KEY=nsk_live_...
# REPOOS_NEON_STORAGE_PREFIX=

# Model providers / voice transcription
REPOOS_OPENROUTER_API_KEY=...
REPOOS_OPENCODE_GO_API_KEY=...
REPOOS_DEEPINFRA_API_KEY=...
REPOOS_WHISPER_KEY=...
GROQ_API_KEY=...
OPENAI_API_KEY=...

# Infrastructure / third-party runners
CLOUDFLARE_API_TOKEN=...
HETZNER_API_TOKEN=...
REPOOS_REMOTE_SSH_KEY=/absolute/path/to/private_key

# Self-hosted notifications
# NTFY_BASE_URL=https://ntfy.example.com
# Absolute base URL for task deep links in Telegram (e.g. https://repo.example.com).
# Falls back to the first tunnel hostname, then server publicOrigin after listen.
# REPOOS_PUBLIC_URL=https://repo.example.com

# Runtime and build overrides — leave these commented unless you need them.
# REPOOS_RUNTIME=auto           # auto | bun | node
# REPOOS_BUN_PATH=/path/to/bun  # only if Bun is not on PATH
# REPOOS_STRICT_BUILD=1         # same as strictBuild = true in repoos.toml

# Internal / process-only variables are not normal configuration and should be
# left to the runtime. Do not add them here as if they were project settings:
# REPOOS_AGENT=1
# REPOOS_RELOAD=1
# REPOOS_PREVIEW_CHILD=1
# REPOOS_RUNTIME_REEXEC=1
# REPOOS_CHECK_CHANGED=main
# REPOOS_TEST_WORKERS=4
# REPOOS_SKIP_TESTS=1
# REPOOS_FORCE_BUILD=1
```

This block mirrors the tracked `.env.example` at the RepoOS repo root; the drift
test fails if the two diverge.

See also:

* [repoos.toml reference](/configuration)
* [Authentication](/authentication)
* [Deployments and releases](/deployments-and-releases)
* [Troubleshooting](/troubleshooting)
