> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0009 — Secrets: two mechanisms

> Accepted · 2026-10-19

> [!NOTE]
>
> Amended by [0107](/adr/0107-connections-store-and-three-key-homes): tenant-owned credentials (API
> keys, certificates, OAuth tokens) are a third class, kept in the `@stackra/connections` store. The
> two mechanisms here are unchanged for our own keys.

> [!NOTE]
>
> Amendment proposed in [0257](/adr/0257-secret-manager-agent-and-the-secrets-inbox): rule 3 becomes
> "a human or the secret-manager flow sets a value; nobody reads one".

## Context

Secret values leak through files, logs, test fixtures, chat transcripts and CI debug steps. Every extra store is another
place to leak from and another thing to rotate. Developer tooling tokens and application secrets have different readers
and need different homes.

## Decision

Two kinds of secret, two mechanisms, never a third.

| Kind | Examples | Where the value lives | Who sets it | How code reads it |
| - | - | - | - | - |
| Application secret | payment keys, OAuth client secrets, webhook signing secrets | the runtime's secret store, per environment | a human | one declaration in code, read at call |
| Developer or CI token | MCP server tokens, vendor tokens CI fetches | the Infisical vault: `/agents`, `/ci` | a human, in the vault | `tools/scripts/mcp.sh`; CI via OIDC |

1. **Names in git, values in the vault.** A file may list a secret's name and purpose, never its value.
2. **Infisical is canonical for developer and CI tokens.** `/agents` is read only by `tools/scripts/mcp.sh`, which passes
   a token to the server process; agents never read it. CI reads `/ci` only, and authenticates by GitHub OIDC (no static token stored as a CI variable).
3. **A human sets, rotates and deletes every value.** An agent declares the secret in code and names key and purpose in its report.
4. **No `.env` and no `.env.example`.** There is no file fallback.
5. **Worker secrets and EAS secret variables are application secrets too.** The store differs, the rule does not.
6. **CI must never run** `infisical export`, `infisical secrets get` without `--silent`, `printenv`, `encore secret get`, or any step that echoes a token or its length.
7. **`pnpm verify` and tests need zero secrets.**
8. **Alternative recorded, not adopted:** Infisical Secret Sync pushing values into runtime stores. It makes a second writer per store. Revisit when more than one runtime holds the same secret (revival trigger below).

## Consequences

* Onboarding needs vault access, not a copied file.
* Where Encore is not deployed ([0011](/adr/0011-rejected-and-deferred-technologies)), the application-secret store is chosen by the first service that needs one, by ADR.

## Enforcement

* `.agents/hooks/guard-bash.mjs` denies `encore secret set/get`, `wrangler secret put/bulk`, reading `.env` and `.dev.vars`, and `printenv`; gitleaks runs in `lefthook.yml` and as `pnpm secrets:scan`.
* Planned, in the same change as `/constitution/secrets`: guard-bash rules for `infisical export` and `infisical secrets get` without `--silent`, with a hook test.
* Planned: a `secrets-doctor` script (names only), a no-`.env` validator check, and a CI workflow allowlist for `secrets.*`.

## Revival trigger

Reopen Secret Sync when a secret must be identical in three or more runtime stores and a human-only write has caused drift twice.

## Supersedes and sources

Supersedes: none. Sources: platform `docs/adr/0005-secrets-infisical-and-encore`, `0018-secrets-constitution`, `0019-application-secrets-single-source-and-propagation`; user rules `secrets.md`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.