Status: partly live.
tools/scripts/mcp.sh and the guard-bash rules exist. Not built yet:
secrets-doctor, a no-.env validator check, the CI OIDC identities and any secret()
declaration (there is no Encore app).The two kinds
There is no third kind. No
.env, no .env.example, no CI-provider repository secret, no value in infrastructure state, a manifest, a record, a log, an artifact or a cache.
A Cloudflare Worker secret is an application secret too: same rule, the store is the Worker’s
secret bindings. An EAS secret environment variable (licence tokens, store credentials) is also
set by a human, with
eas env:create --visibility secret.Rules for everyone, agents included
- Adding an application secret is an agent’s job; setting it is not. Declare
secret("NAME")in the service, then name the key and its purpose in your report. - Setting, rotating or deleting a value is a human’s job, or the
secret-manageragent’s through the secrets inbox, which writes a value without anyone seeing it (ADR-0257). - Adding a developer token: a human adds it to Infisical
/agents. Reference it in.mcp.jsonas a bare$KEY, not${KEY}: Claude Code expands the braced form itself and silently disables the server when the variable is unset. Say so in your report. - Never read a value.
.agents/hooks/guard-bash.mjsdeniesinfisical export,infisical secrets getwithout--silent,encore secret set/get,wrangler secret put/bulk,printenv, a bareenv, and reading.envor.dev.vars. - Tests and the CI
verifystage need zero secrets. A test that needs a real credential is a design finding. Use provider fakes (see Testing). - The image never bakes a value. Runtime secrets come from the environment the platform injects.
- Local overrides for a service go in a gitignored file (target name:
services/<name>/.secrets.local.cueonce a service uses Encore).
The secrets inbox
Status: live for Infisical (
pnpm secrets:import). Runtime-store (Encore) secrets join the same
flow once a tool for that store exists.1
The owner drops a file
One file per secret in
~/Secrets-inbox/ (outside every repository, never synced or backed up to a shared place). The file name is the secret name: SCREAMING_SNAKE, one extension stripped, so STACKRA_REVIEWER_PRIVATE_KEY.pem becomes STACKRA_REVIEWER_PRIVATE_KEY. A short value such as an App ID is a file holding just that value; one trailing newline is stripped. Files must be regular (no symlinks, no folders) and at most 64 KB.2
Optionally, a target
<NAME>.target.yaml next to it with env: (dev, staging, prod; default dev) and path: (/agents or /ci, or a sub-folder; default /agents). Flags --env, --path and --name override it.3
The agent dry-runs
pnpm secrets:import --dry-run checks the name, target and file, reaches Infisical, and prints per secret NAME env:path ok would-overwrite: yes|no.4
Production asks first
For
prod the dry run also prints a one-time --confirm <token>, valid 30 minutes and bound to the name, env, path and file content. The agent shows the owner the dry-run line and asks yes or no; only on yes does it re-run with the token. The token is spent before the write starts, so it never works twice. It is a process rule, not an enforced yes: the agent that sees it could use it. Where the owner’s yes must be enforced, use the request page (STA-187); there the owner’s own submit is the yes.5
The import
pnpm secrets:import --actor secret-manager checks the CLI supports NAME=@file, then writes each value with infisical secrets set NAME=@<0600 temp file> (never on argv; the temp file is a dot-folder inside the inbox, removed at once, on Ctrl-C, or at the next start), captures all CLI output without echoing it, reads the value back and compares SHA-256 hashes in-process, then deletes the inbox file and its sidecar. On any failure the file stays and the line says why, in a fixed, value-free phrase (not logged in to Infisical, read-back hash does not match, …).~/.stackra/secrets-audit.log (outside the repo): timestamp, actor, name, env, path, action, result. Names only. The production tokens are HMACs under a per-machine key in ~/.stackra/secrets-confirm.key, so a token says nothing about the value.
Why the script is the boundary: a hook cannot reliably tell which agent is calling, so any caller may run
pnpm secrets:import, and the script is written so that no caller can get a value out of it. The hooks are an accident guard, not a security boundary: guard-bash refuses commands that name Secrets-inbox or the confirm key (also by a glob like Secrets-inbo?) or recursively read the home directory; guard-paths refuses Read, Grep (path and glob), Glob, Write and Edit that reach the inbox; the OpenCode guard plugin mirrors the literal and glob checks. A process that builds the path at runtime gets past them; agents are told never to try.
A future local request page (the owner types a value into a form on this machine) calls the same core, importSecret({ name, env, path, valueStream, actor }), which returns only { name, ok, reason }.
Infisical layout
SCREAMING_SNAKE as <VENDOR>_<PURPOSE> (CONTEXT7_API_KEY, HEROUI_PERSONAL_TOKEN, EXPO_TOKEN). Never a customer name, never a value-shaped string. .infisical.yaml (tracked; it holds no secret) holds the project id and domain the CLI needs.
How mcp.sh works
.mcp.json passes a secret as a bare $VAR argument, for example "x-heroui-personal-token:$HEROUI_PERSONAL_TOKEN". tools/scripts/mcp.sh:
- Checks the
infisicalCLI and.infisical.yamlexist and that/agentsis reachable. - Runs
infisical run --path=/agents --silent -- …, expands each$VARinside that subprocess andexecs the server. The value exists only in that process tree. - If the vault is unreachable it fails only when an argument references a variable, and the message names the server and the variable. Servers that need no token launch unchanged.
.env fallback on purpose: a second source is a second place a token goes stale, and the failure it causes is the quiet kind, a server that launches with an empty environment and fails somewhere else.
To see which names exist, run node tools/scripts/secrets-doctor.mjs (target: it reports names only, never values; not built yet).
CI reads /ci through OIDC, one step at a time
- Every pipeline job that needs a token logs in to Infisical with the job’s OIDC token, then reads
/cifor that one step. No token is stored as a CI variable. - One machine identity per environment, each trusting only its own OIDC subject (repository plus environment).
productionadditionally requiresmain. - The token exists as the process environment of that step. It is never written to the job’s env file, a file, an artifact or a cache, and never echoed, not even its length.
- The
verifystage holds no identity at all. Onlydeploy,promoteand release jobs log in. - CI never writes a secret value to any store.
guard-bash, to be grepped out of workflow sources by a CI check):
infisical export,infisical secrets getwithout--silent,infisical secrets setprintenv,envwithout a filter,setin a shell step,catof any.env*encore secret get/set,wrangler secret put/bulk- any “print the token length” debug step
- anything from
infisical runstored in the job’s env file, outputs or an artifact
How a human sets an application secret
Use the vendor’s UI where there is one, so the value never sits in shell history. Record the name, environment, date and your handle in the owning member’s
AGENTS.md, never the value.
Rotation
Overlap, never revoke-first:1
Create the new value at the vendor
A second token, key or client secret, with the same minimum scope.
2
Set it where it lives
A human writes it to Infisical
/ci or /agents, or to the runtime’s store for an application
secret.3
Let the consumer pick it up
A no-op pipeline run, a redeploy or a rollout. A running service does not see a new value on its
own.
4
Prove it works
One real call that uses the new value (
whoami, a read-only API call, one test email). Confirm
by metadata, never by printing the value.5
Revoke the old value at the vendor
Only now. Where a vendor cannot hold two values, declare the single-value window before you
start and keep it as short as the roll allows.
Record each roll in the member’s
AGENTS.md: the name, environment, date, handle, whether the window overlapped, and the call that confirmed it.
A value was exposed
1
Revoke at the vendor first
Not in Infisical first: the leaked value stays valid until the vendor revokes it. Rewriting git
history does not un-leak it.
2
Rotate
Follow the rotation steps above. For an application secret, a human re-sets it.
3
Purge
Delete the CI run logs, artifacts and any cache key that held it.
4
Record and close the gap
Note it in the member’s
AGENTS.md and open a finding against the step that leaked it. Add the
pattern to guard-bash and .gitleaks.toml.Checks that back this page
See also CI, Mobile release and Dependencies.