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.
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:
- Real shell or service environment variables win.
- A repo-root
.envfills in only values the environment hasn't already set. repoos.tomlis the committed source of non-secret config.- For fields that have an environment form, the environment overrides the
repoos.tomlvalue (for exampleREPOOS_RESEND_API_KEYfills[auth.emailProvider], andNTFY_BASE_URLoverridesntfyBaseUrl).
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. 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.
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_URLBindings 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_KEYGROQ_API_KEYREPOOS_WHISPER_KEYREPOOS_RESEND_API_KEYREPOOS_GOOGLE_CLIENT_SECRETREPOOS_AUTH_SESSION_SECRETREPOOS_AUTH_DEV_BACKDOOR_CODEREPOOS_SECRET_STORE_KEYREPOOS_TELEGRAM_PROVISIONING_KEYCLOUDFLARE_API_TOKENHETZNER_API_TOKENPATHHOMENODE_OPTIONSPORTHOSTREPOOS_OPENROUTER_API_KEYTYPESAFE_API_KEYREPOOS_CLEF_FLASH_API_KEYREPOOS_OPENCODE_GO_API_KEYREPOOS_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:
{
"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.
# 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:
# 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.comThis 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.
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.
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.
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 and 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
.envout of git and keep a tracked.env.examplewith 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 serveso 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.
# 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=1This block mirrors the tracked .env.example at the RepoOS repo root; the drift test fails if the two diverge.
See also: