GitHub Secrets Inventory
This is the single source of truth for which GitHub secrets exist, where they live, and which workflows consume them. If a future agent or engineer asks "can I delete this secret?" or "why does this exist?" — the answer is here, not buried across seven repos and many workflow YAMLs.
Maintenance rule: when you add a
secrets.*reference to any workflow YAML, add a row here in the same PR. When the last reference is removed, mark the row Deletable here in the same PR. The audit-drift command at the bottom catches divergence.
How GitHub secret scoping works in sv0
Secrets attach at one of three scopes:
| Scope | Applies to | When to use |
|---|---|---|
| Repository | Every workflow run in the repo | Cross-environment values (CF Access service tokens used by both deploy and visual-review, GHCR auth) |
| Environment: dev | Workflow runs that declare environment: dev | Anything specific to dev / PR-preview deploys |
| Environment: prod | Workflow runs that declare environment: prod | Anything specific to production deploys |
A secret named identically at both Environment scopes (e.g. WORKOS_API_KEY) holds two separate values — they don't share a backing store. This is intentional: prod and dev's WorkOS projects are different.
STAGING_* / PROD_* prefix convention
deploy-dev.yml reads only STAGING_*-prefixed secrets from the dev Environment. deploy-prod.yml reads only PROD_*-prefixed secrets from the prod Environment. Each workflow only ever consumes its own prefix from its own Environment scope.
The prefix exists because both names end up co-mounted into the same container .env file via docker-compose.deploy.yml's passthrough block (compose forwards every host env var literally, with ${VAR:-} defaulting unset values to empty string). The application code reads only the prefix that matches NODE_ENV. Without the prefix, dev's compose passthrough would clash with prod's at the var-name level.
When a STAGING_/PROD_ pair is collapsed into an unprefixed name, the deploy YAML changes from "always pass both prefixed pairs" to "Environment scope provides the unprefixed name; one pass to one slot."
Secret scope follows the resource, not the workflow count
The trap to avoid: "reused in N workflows" sounding like "shared across tiers." Those are different axes.
- Machine-bound or tier-bound (an SSH key that unlocks one VM, per-tier WorkOS credentials, per-tier connector creds) → environment-level, even if reused across multiple workflows that target that same tier.
- Cross-tier shared resource (one Cloudflare zone serving all tiers, one GHCR token, one shared metrics endpoint) → repo-level.
A dev-only SSH key reused in three dev-tier workflows is still a dev-tier secret. A Cloudflare API token whose zone covers prod + staging + dev is a repo-level secret. This rule was locked in 2026-05-15 after the sv0-platform#927 → #944 cycle, where moving the Hetzner SSH triple to repo level (briefly, in #940) was reverted because the key unlocks one machine, not all tiers.
External identities
GH secrets carry credentials, but the identities themselves (WorkOS Connect Apps, Atlas DB users, Azure user-assigned identities, federated credentials) live in external systems. This table is the lookup "does this thing already exist, and if so, what's it called?" for cases where someone is about to provision a new identity but should reuse an existing one.
Maintenance rule for this section: every PR that creates a WorkOS App, an Atlas DB user, or an Azure UAA / federated credential adds the row here. The PR description must name the identity. Without this surface, the engineer's only signal is grepping for the object's auto-generated ID, which they don't know yet.
WorkOS — Connect Apps & M2M
| Identity (WorkOS dashboard name) | Type | WorkOS workspace | client_id | Backing GH secret(s) | Created |
|---|---|---|---|---|---|
| main user-session app (AuthKit) | OAuth (PKCE) | staging + prod | client_… (varies per env) | WORKOS_CLIENT_ID (dev + prod env scopes) | original |
claude-code-agent | OAuth (device_code) | staging | client_01KQFZTMWXZ48RF2CXPQHN5179 | STAGING_WORKOS_APP_CLAUDECODE_CLIENT_ID / _CLIENT_SECRET (dev env scope) | 2026-04-30 (sv0-platform#715) |
claude-code-agent (prod) | OAuth (device_code) | prod | not yet provisioned | PROD_WORKOS_APP_CLAUDECODE_CLIENT_ID / _CLIENT_SECRET (prod env scope) | pending |
ci-staging-m2m | M2M (client_credentials) | staging | client_01KQHNSDZXKWQ0BR1JV248R80Z | STAGING_CI_M2M_CLIENT_ID / _CLIENT_SECRET (repo scope) | 2026-05-01 (sv0-platform#733) |
ci-prod-m2m | M2M (client_credentials) | prod | not yet provisioned | planned: PROD_CI_M2M_* | pending |
Scope mapping in registry / runtime:
claude-code-agentis registered inagent-clients.tsas adelegated_agent(introspection-gated; staff CLI device_code flow).ci-staging-m2mis NOT inagent-clients.ts— service-principal M2M JWTs are validated by the platform JWKS verifier directly, not the agent-client registry (see13-authentication-and-user-management.md).- The main user-session app is the OAuth audience for every
WORKOS_*JWT minted via the AuthKit cookie flow.
Atlas — DB users
| Identity | Atlas project | Cluster | Granted DBs | Used by | Created |
|---|---|---|---|---|---|
sv0_app (legacy) | sv0-prod | prod M10 | sv0_prod (only — sv0_dev / sv0_staging dropped in #93) | Hetzner prod (until DNS cutover); deletion deferred to Hetzner decommission per ADR-025 | original |
sv0_prod_app | sv0-prod | prod M10 | sv0_prod | Azure prod (when stood up; sits idle until then) | 2026-05-18 (sv0-infrastructure#93) |
sv0_staging_app | sv0-staging (separate Atlas project) | sv0-staging Flex | sv0_staging | Azure staging deploy workflow (composed MONGODB_URI from TFC outputs) | 2026-05-18 (sv0-infrastructure#97) |
sv0_dev_app (dev VM-internal Mongo) | n/a — container-local | n/a | n/a | dev-azure container Mongo (no Atlas user; auth disabled in container) | n/a |
Atlas password rotation: all of these passwords are TF-managed sensitive outputs (atlas_*_app_password). Never set manually in Atlas UI — drift will revert on next TFC apply. Pull from TFC API after apply; mirror into the matching MONGODB_URI GH secret per env.
Azure — User-assigned identities (UAA) & federated credentials
| Identity | Resource | Subscription RBAC | Federated subjects | Backing repo variable | Created |
|---|---|---|---|---|---|
gha-sv0-platform-deploy | UAA 35d3196e-7934-483e-945d-3a7645b7da6f | Contributor on rg-sv0-dev; Contributor on rg-sv0-staging (added #91) | repo:SecurityV0/sv0-platform:environment:dev; :environment:staging | AZURE_GHA_DEPLOY_CLIENT_ID (dev + staging env scopes; same client_id, different fed subjects) | dev 2026-05-13 (#74); staging 2026-05-18 (#91) |
gha-sv0-platform-deploy-prod (planned) | future UAA | Contributor on rg-sv0-prod | repo:SecurityV0/sv0-platform:environment:prod | AZURE_GHA_DEPLOY_CLIENT_ID (prod env scope, different value per ADR-024 §2 one-app-per-tier) | pending — Azure prod migration |
tfc-sv0-infrastructure | UAA c8ac2f77-215f-4bc1-978f-b9e7ec34da68 | Owner on subscription (broad); UAA on subscription (RBAC management) | TFC workspaces sv0-bootstrap, sv0-dev, sv0-staging, sv0-shared, sv0-prod, each plan + apply phase | (consumed by TFC, not GitHub) — see TFC_AZURE_RUN_CLIENT_ID workspace env var | original (#62) |
Why three deploy UAAs (one per blast-radius tier) instead of one with three subjects: ADR-024 §2 — federated credentials union RBAC at the SP, so one leaked GHA token would inherit every tier's blast radius if all three were on the same app. The pattern is one UAA per tier; within each, multiple repo:OWNER/REPO:environment:NAME federated subjects.
sv0-platform secrets
WorkOS — application auth
| Secret | Scope | Workflows / code that read it | Purpose | Status |
|---|---|---|---|---|
WORKOS_API_KEY | dev + prod | deploy-{dev,prod}.yml → API container env → env.ts (WorkOSEnv.apiKey) → workos-provider.ts | Server-side WorkOS REST API auth (org lookups, membership, webhooks, introspection) | Active |
WORKOS_CLIENT_ID | dev + prod | deploy-{dev,prod}.yml → WorkOSEnv.clientId | Main user-session OAuth app (AuthKit). Audience claim in every WorkOS-issued JWT. | Active |
WORKOS_AUTHKIT_DOMAIN | dev + prod | deploy-{dev,prod}.yml → WorkOSEnv.authkitDomain | M2M JWT issuer + JWKS endpoint (e.g. securityv0.authkit.app). Required for any AUTH_PROVIDER=workos deploy — used by both device_code and client_credentials grants. | Active |
WORKOS_REDIRECT_URI_ALLOWED_HOSTS | dev + prod | deploy-{dev,prod}.yml → WorkOSEnv.redirectUriAllowedHosts | Per-request redirect URI allowlist. Comma-separated list of hostnames; dev uses *.securityv0.com (single-label wildcard), prod uses app.securityv0.com. | Active |
SESSION_COOKIE_PASSWORD | dev + prod | deploy-{dev,prod}.yml → env.ts → iron-session seal | Cookie seal for sv0_session. Single source of truth for the seal password. | Active |
WORKOS_SUPER_ADMIN_ORG_ID | dev + prod | deploy-{dev,prod}.yml → auth.ts callback route + bearer JIT-upsert | WorkOS org whose active members are super-admin. The only super-admin signal. Required in prod. | Active |
WORKOS_WEBHOOK_SECRET | dev + prod | deploy-{dev,prod}.yml → env.ts (placeholder) | WorkOS webhook receiver auth. Receiver not yet wired. | Active stub — keep until receiver lands |
WorkOS — agent / M2M Connect Apps
These are separate WorkOS Connect Apps, not the main user-session app. Each holds its own client_id / client_secret pair.
| Secret | Scope | Workflows / code that read it | Purpose | Status |
|---|---|---|---|---|
STAGING_WORKOS_APP_CLAUDECODE_CLIENT_ID | dev | deploy-dev.yml → agent-clients.ts (registry) → bearer middleware introspection | The claude-code Connect App (device_code grant). Lets staff CLI mint delegated_agent tokens. | Active |
STAGING_WORKOS_APP_CLAUDECODE_CLIENT_SECRET | dev | deploy-dev.yml → agent-clients.ts → introspection HTTP Basic | Pair of the above. | Active |
PROD_WORKOS_APP_CLAUDECODE_CLIENT_ID | prod | deploy-prod.yml → agent-clients.ts | Same, prod Connect App. | Active |
PROD_WORKOS_APP_CLAUDECODE_CLIENT_SECRET | prod | deploy-prod.yml → agent-clients.ts | Same, prod. | Active |
STAGING_CI_M2M_CLIENT_ID | repo | deploy-dev.yml, pr-preview-admin.yml, visual-review.yml (inline curl to mint M2M token) | Service-principal Connect App used by CI for non-interactive API calls (visual-review, PR-preview admin tasks). Repo-level because visual-review.yml doesn't declare an environment — env scope would make the secret unreachable from that workflow. | Active |
STAGING_CI_M2M_CLIENT_SECRET | repo | (same workflows) | Pair of the above. | Active |
No
PROD_CI_M2M_*pair today. Prod CI auth has not been needed. If introduced, add aPROD_CI_M2M_*pair at the prod Environment scope and document it here.
Deploy + infrastructure
| Secret | Scope | Workflows that read it | Purpose | Status |
|---|---|---|---|---|
DEPLOY_HOST | dev + prod | deploy-dev.yml, deploy-dev-cleanup.yml, deploy-prod.yml, pr-preview-admin.yml | SSH host for the deploy target VM. | Active |
DEPLOY_HOST_KEY | dev + prod | (same) | known_hosts entry. | Active |
DEPLOY_SSH_KEY | dev + prod | (same) | SSH private key used by the runner. | Active |
METRICS_BEARER_TOKEN | dev + prod | deploy-{dev,prod}.yml → docker-compose.deploy.yml → API container env | Bearer token gating the /metrics Prometheus endpoint. | Active |
GITHUB_TOKEN | (built-in) | ci.yml, deploy-{dev,prod}.yml | Built-in scoped token; not user-managed. | Active (built-in) |
Cloudflare — Access service tokens, DNS, Pages
| Secret | Scope | Workflows that read it | Purpose | Status |
|---|---|---|---|---|
CF_ACCESS_CLIENT_ID_DEPLOY | repo | deploy-{dev,prod}.yml, token-health.yml | CF Access service token (Origin: app.securityv0.com, dev.securityv0.com, pr-N-dev.securityv0.com) — bypasses the login page for CI. | Active |
CF_ACCESS_CLIENT_SECRET_DEPLOY | repo | (same) | Pair of the above. | Active |
CF_ACCESS_CLIENT_ID_VISUAL | repo | visual-review.yml, token-health.yml | Service token for visual-review's headless Playwright runs. Distinct from _DEPLOY so it can be rotated independently. | Active |
CF_ACCESS_CLIENT_SECRET_VISUAL | repo | (same) | Pair of the above. | Active |
CLOUDFLARE_ACCOUNT_ID | repo | bootstrap-cf-access.yml, token-health.yml, visual-review.yml, visual-review-cleanup.yml, visual-review-stale-cleanup.yml | Account ID for sv0-reviews.pages.dev (PR visual-review site) deploys + CF Access app management. | Active |
CLOUDFLARE_API_TOKEN | repo | bootstrap-cf-access.yml, deploy-dev.yml, deploy-dev-cleanup.yml, visual-review.yml, visual-review-cleanup.yml, visual-review-stale-cleanup.yml | Cloudflare API token. Two unrelated uses: (a) DNS record management for securityv0.com (PR-preview CNAME create/delete in deploy-dev / cleanup, CF Access app bootstrap) and (b) wrangler Pages publishing for pr-N.sv0-reviews.pages.dev. Same token works for both because Pages and Zone:DNS:Edit are both scoped to the SecurityV0 account. | Active |
CLOUDFLARE_ZONE_ID | repo | deploy-dev.yml, deploy-dev-cleanup.yml | Zone ID for securityv0.com (732bc7b588772c6cd1b5b39516a63243). Public identifier, not secret-shaped, but kept in secrets for naming consistency with CLOUDFLARE_*. | Active |
CLOUDFLARE_API_TOKEN_ZERO_TRUST | repo | token-health.yml | API token scoped to the Zero Trust org (separate from the general API token) for CF Access app bootstrap and policy management. See #575. | Active |
sv0-connectors secrets
| Secret | Read by | Purpose | Status |
|---|---|---|---|
ENTRA_SERVICENOW_AZURE_CLIENT_ID | entra-servicenow-scan.yml | Azure App Registration client_id for the entra-servicenow connector's Microsoft Graph access. | Active |
ENTRA_SERVICENOW_AZURE_CLIENT_SECRET | entra-servicenow-scan.yml | Pair of the above. | Active |
ENTRA_SERVICENOW_AZURE_TENANT_ID | entra-servicenow-scan.yml | Azure tenant ID. | Active |
ENTRA_SERVICENOW_SNOW_INSTANCE | entra-servicenow-scan.yml | ServiceNow instance hostname. | Active |
ENTRA_SERVICENOW_SNOW_USERNAME | entra-servicenow-scan.yml | ServiceNow basic-auth username. | Active |
ENTRA_SERVICENOW_SNOW_PASSWORD | entra-servicenow-scan.yml | ServiceNow basic-auth password. | Active |
GITHUB_TOKEN | connectors-publish.yml | Built-in scoped token. | Active (built-in) |
Where is the
entra-servicenow-ci.ymlworkflow? It exists in.github/workflows/but reads no secrets — it's a unit-test-only workflow that runs without live credentials. Live integration tests run viaentra-servicenow-scan.ymlagainst a long-lived dev tenant.
Previously here, now deleted:
CF_ACCESS_CLIENT_ID+CF_ACCESS_CLIENT_SECRET(set 2026-04-02). No workflow ever consumed them — leftover from an early connector spike. Removed 2026-05-15 after audit. The connector runtime reads CF Access tokens from a.envfile on the connector host, not from GHA.
sv0-website secrets
| Secret | Read by | Purpose | Status |
|---|---|---|---|
CLOUDFLARE_ACCOUNT_ID | deploy-prod.yml, deploy.yml, staging.yml, report.yml, visual-review.yml, visual-review-cleanup.yml | Account ID for securityv0.com Pages deploys. | Active |
CLOUDFLARE_API_TOKEN | (same workflows) | API token for wrangler to publish the marketing site. | Active |
SLACK_WEBHOOK | notify-slack.yml | Slack incoming webhook URL for engineering channel — fires on key release events. | Active |
GITHUB_TOKEN | auto-merge-routine-prs.yml, staging.yml | Built-in scoped token. | Active (built-in) |
sv0-intelligence secrets
| Secret | Read by | Purpose | Status |
|---|---|---|---|
ANTHROPIC_API_KEY | weekly-incident.yml | Anthropic API auth for scripts that call Claude (incident summarization, weekly PM cleanup, etc.). | Active |
GH_TOKEN | weekly-incident.yml | User-managed GitHub token with cross-repo write permission. Distinct from the built-in GITHUB_TOKEN because that one is scoped to the run's own repo only. | Active |
sv0-documentation secrets
| Secret | Read by | Purpose | Status |
|---|---|---|---|
CLOUDFLARE_ACCOUNT_ID | docs-ci.yml | Account ID for docs.securityv0.com Pages deploy. | Active |
CLOUDFLARE_API_TOKEN | docs-ci.yml | API token for wrangler to publish the mkdocs build. | Active |
Repos with no workflow secrets
sv0-skills and sv0-demo-labs ship workflows that don't reference any secrets.* entries today. If that changes, add a section above.
VM ↔ secret mapping (2026-05-15)
Cross-reference of which secrets back which deploy target. Maintain alongside the per-name tables above; this view answers the operational question "when we retire VM X, which secrets become deletable?".
| VM / target | Tier | Auth mechanism | Secrets backing it | Sunset trigger |
|---|---|---|---|---|
Hetzner VPS 178.156.217.150 | dev (current) | SSH | dev.DEPLOY_HOST, dev.DEPLOY_HOST_KEY, dev.DEPLOY_SSH_KEY | DNS flip of dev.securityv0.com to Azure |
| Hetzner prod VM | prod (current) | SSH | prod.DEPLOY_HOST, prod.DEPLOY_HOST_KEY, prod.DEPLOY_SSH_KEY | Prod migrates to Azure (no firm timeline) |
Azure vm-sv0-dev-1 (ADR-024) | dev demo (bootstrap-only — no app stack yet) | OIDC federation, Entra app gha-sv0-platform-deploy | None — RBAC, not a secret. Public-ish app client_id lives in repo variable AZURE_GHA_DEPLOY_CLIENT_ID. | n/a (the canonical path) |
| Future Azure dev (long-running, full app stack) | dev (target) | OIDC, same Entra app | None planned | n/a |
| Future Azure staging | staging (per ADR-022 §3) | OIDC, separate Entra app gha-sv0-platform-deploy-staging | Tier-specific WorkOS pair in staging env scope (when env exists) | n/a |
| Future Azure prod | prod (per ADR-022) | OIDC, separate Entra app gha-sv0-platform-deploy-prod | Existing prod.* WorkOS scoped to that env | n/a |
Why three Entra apps: ADR-024 §2 rejects reusing one app across tiers — federated credentials union RBAC at the SP, so one leaked GHA token would inherit every tier's blast radius. The pattern is one Entra app per blast-radius tier, multiple federated credentials within (each tied to a repo:OWNER/REPO:environment:NAME subject).
Cross-tier secrets that survive every migration (because the underlying resource is one):
CLOUDFLARE_API_TOKEN— one Cloudflare zone (securityv0.com) serves all tiersCLOUDFLARE_ZONE_ID— sameCLOUDFLARE_ACCOUNT_ID— sameCLOUDFLARE_API_TOKEN_ZERO_TRUST— one Zero Trust orgCF_ACCESS_CLIENT_ID_DEPLOY/_SECRET_DEPLOY— service tokens valid for all CF Access apps (dev, prod, PR-preview)CF_ACCESS_CLIENT_ID_VISUAL/_SECRET_VISUAL— same shape, different token
Deferred cleanup checklist (do not act on these until the trigger fires):
- When dev DNS flips to Azure: delete
dev.DEPLOY_*triple, retiredeploy-dev.yml, decide fate ofpr-preview-admin.yml(Hetzner-tied helper) andbootstrap-cf-access.yml(Hetzner-perimeter CF Access app) - When prod migrates to Azure: delete
prod.DEPLOY_*triple, retiredeploy-prod.yml - When staging tier exists: add
stagingGitHub Environment, setstaging.WORKOS_*andstaging.SESSION_COOKIE_PASSWORD, register the new Entra app's federated credential
Adding a new secret — checklist
When you add a secrets.* reference to a workflow:
- Scope it correctly. Use the most-restrictive scope that works. Environment scope is preferred over repo scope when the value differs per env. Repo scope is fine for cross-environment values (e.g. CF Access service tokens used by both deploy and visual-review).
- Document the value's provenance. If it came from WorkOS / Cloudflare / Azure, link the dashboard URL or runbook entry that says where to regenerate it. Put that link in the runbook (
workos-production-configuration.md,cf-access-service-token-setup.md), not in this inventory — this doc covers the name → consumer → purpose mapping; runbooks own value provenance and rotation. - Add a row to this inventory in the same PR that adds the workflow reference. Inventory drift is the failure mode this doc exists to prevent.
- Match the prefix convention. Use
STAGING_/PROD_only when the value has a per-environment runtime difference AND will be co-mounted into the same container alongside its sibling (the docker-compose pattern). For repo-scope values that are the same across environments, no prefix. - Add a deletion plan in the Status column when the secret supports a deprecated code path. Include the PR number that removes it, and the pre-condition that must be true before deletion is safe.
Composite-action blind spot: the audit-drift command below scans
secrets.*references in workflow YAMLs only. A reusable workflow or composite action that consumes secrets viasecrets: inheritwould not appear in the grep output. As of this writing, no sv0 repo uses that pattern, so the audit is exhaustive. If we adopt composite actions later, augment the audit to followuses:references.
Local .env credentials (agent discovery surface)
The tables above cover GitHub-side secrets (consumed by Actions workflows). The parallel local-side store is <repo>/.env — gitignored, per-repo, holds the values an engineer or authorized agent needs when running scripts, hitting deployed APIs, or developing locally.
This section answers the question every fresh-context agent asks: "Do I have to bother the user for this credential, or does it already live somewhere on disk?" The answer for every key below is: on disk in <repo>/.env — check there first.
Authorization: authorized agents (Claude Code, Codex) may read
.envvalues. The values themselves come from 1Password — that's the user's bootstrap path, not an agent discovery surface (agents have no 1Password access; ask if a key is missing).Rules for agents reading
.env:
- Never commit
.envor paste its values into PR descriptions, commit messages, issue comments, session notes, or logs.- Never write
.envvalues to.scratch/or.claude/session-notes/.- If a key is missing or appears stale, ask the user — don't invent or fall back silently.
- Treat
<repo>/.env.exampleas the schema-of-record. If a key is in.envbut not in.env.example, that's drift worth fixing in the same PR you discover it.
sv0-platform .env (canonical store for platform-runtime + agent toolkit)
Two categories of keys coexist in one file. Platform-runtime keys are read by src/shared/config/env.ts at API boot. Agent-toolkit keys are read by scripts and ad-hoc agent automation.
Platform runtime
| Key | Purpose | GH-secret mirror |
|---|---|---|
NODE_ENV, PORT, LOG_LEVEL, CORS_ALLOWED_ORIGINS, TENANT_HEADER, UI_PORT | Standard runtime config. | n/a |
MONGODB_URI, MONGODB_DB | Local Mongo connection. MONGODB_DB overrides the URI path component — the Mongo client takes (uri, dbName) separately, so the URI path is ignored. | n/a (CI uses ephemeral Mongo) |
AUTH_PROVIDER, REQUIRE_AUTH | Auth gate selection. dev provider for local; workos for deployed. | n/a |
WORKOS_API_KEY, WORKOS_CLIENT_ID, WORKOS_AUTHKIT_DOMAIN, WORKOS_REDIRECT_URI_ALLOWED_HOSTS, WORKOS_SUPER_ADMIN_ORG_ID, WORKOS_WEBHOOK_SECRET | WorkOS staging tenant — server-side auth, super-admin signal, redirect allowlist, webhook receiver. | dev env WORKOS_* |
PROD_WORKOS_API_KEY, PROD_WORKOS_CLIENT_ID, PROD_WORKOS_AUTHKIT_DOMAIN | WorkOS prod tenant — only for prod-tier testing or cross-tenant migrations. | prod env WORKOS_* |
SESSION_COOKIE_PASSWORD | iron-session seal for sv0_session. | dev + prod env SESSION_COOKIE_PASSWORD |
TUNNEL_TOKEN | Cloudflare Tunnel token (only needed with --profile tunnel). | n/a |
STAGING_WORKOS_APP_CLAUDECODE_CLIENT_ID/_SECRET | Staff CLI Connect App (device_code grant). Required for npm run auth:login. | dev env (same names) |
STAGING_M2M_SPIKE_CLIENT_ID/_SECRET | Service-principal Connect App pair. Used by ad-hoc M2M agent scripts. | none — local-only |
ALLOWED_API_KEYS, API_KEY_HEADER | API-key auth (reserved, planned per sv0-platform#827). Inert today. | n/a |
Agent toolkit
| Key | Purpose | GH-secret mirror |
|---|---|---|
CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRET | CF Access perimeter bypass for deployed envs. Auto-loaded by scripts/lib/load-env.ts in every visual-*.ts script — no source .env needed. | repo CF_ACCESS_CLIENT_ID_VISUAL / _DEPLOY |
CLOUDFLARE_ACCOUNT_ID | SecurityV0 Cloudflare account ID. | repo CLOUDFLARE_ACCOUNT_ID |
CLOUDFLARE_API_TOKEN | Zone:DNS:Edit + Pages publish — same token works for both (account-scoped). Used for ad-hoc CF API curl from scripts. | repo CLOUDFLARE_API_TOKEN |
CLOUDFLARE_API_TOKEN_ZERO_TRUST | Separate token scoped to the Zero Trust org — CF Access app + policy management. | repo CLOUDFLARE_API_TOKEN_ZERO_TRUST |
CLOUDFLARE_API_TOKEN_SV0_TERRAFORM | Terraform-scoped CF token used by the TFC backend for sv0-infrastructure. | none — used from TFC, not GHA |
GH_PAT_ROUTINE | Personal access token with cross-repo write — needed for sprint reviews, weekly PM cleanup, cross-repo issue labelling. The built-in GITHUB_TOKEN in GHA is repo-scoped only. | none — local-only |
GRAFANA_API_TOKEN | Grafana Cloud read token — query Loki/Prom/Tempo from scripts, read dashboards/alerts. | none — local-only |
BETTER_STACK_CLAUDECODE_GLOBAL_API_TOKEN | BetterStack (Logs + Heartbeats) global API token, scoped to claudecode-* sources. | none — local-only |
Other repos
| Repo | .env keys | Purpose |
|---|---|---|
| sv0-website | CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, GH_PAT_ROUTINE | Marketing site build/deploy + automation scripts. |
| sv0-documentation | NOTION_TOKEN, NOTION_TOKEN_SECURITYV0 | Notion API for documentation pipelines (research import). |
| sv0-connectors | (no .env at repo root) | Per-integration .env files live under integrations/<connector>/.env; see each connector's README. |
Drift between .env and .env.example
<repo>/.env.example is the schema-of-record. The CI of each repo doesn't validate this, so drift accumulates silently. As of 2026-05-15, sv0-platform/.env.example covers every key in the live .env. If you add a new credential to .env, add it to .env.example (with a placeholder + comment) in the same PR.
To check drift locally:
diff \
<(grep -E "^[A-Z_][A-Z0-9_]*=" .env | sed 's/=.*//' | sort -u) \
<(grep -E "^#?\s*[A-Z_][A-Z0-9_]*=" .env.example | sed 's/=.*//' | sed 's/^# *//' | sort -u)
Empty output = clean.
Audit drift check
To verify this inventory matches the workflow YAML across every sv0 repo:
for repo in sv0-platform sv0-connectors sv0-documentation sv0-intelligence sv0-website sv0-skills sv0-demo-labs; do
echo "=== $repo ==="
find ~/dev/securityv0/repos/$repo/.github/workflows -name "*.yml" 2>/dev/null \
| xargs grep -lE "secrets\." 2>/dev/null | while read f; do
grep -oE "secrets\.[A-Z_][A-Z0-9_]+" "$f" | sort -u | while read s; do
echo " $s -> $(basename $f)"
done
done | sort -u
done
The output should match the Workflows that read it columns above. A secrets.* reference in the audit output that's missing from the inventory is documentation drift — open a PR to add the row. A row in the inventory that's absent from the audit output is dead — open a PR to delete the row (and the secret in GitHub if no other consumer exists).
Related
- WorkOS Production Configuration — secret values, rotation procedure, dashboard locations.
- CI/CD Operations — workflow-level deploy mechanics.
- Cloudflare Access Service Tokens — provisioning and rotation for CF Access tokens.
- Auth Simplification Plan (2026-05-08) — rationale for the current auth-secret shape.
- Multi-Instance Deployment — how compose passes these secrets into containers at runtime.