Last updated 2026-05-07
Secrets & Backends
The recommended production model is git as the source of truth: commit one SOPS-encrypted file per environment, encrypt the file data key to every approved age recipient, and let Stax decrypt/export/sync it when a local runner or deployment needs values.
Mental model
SOPS generates a random data key for each encrypted file. The file contents
are encrypted once with that data key, and the data key is wrapped to each
age recipient in .sops.yaml. That is why the same encrypted
file can be decrypted by different developers using different private keys.
| File | Committed | Purpose |
|---|---|---|
.sops.yaml | Yes | Reviewable age recipient policy for managed secret files. |
.stax/vault.json | Yes | Stax binding that tells the runner which SOPS files map to which environment. |
infra/secrets/prod.enc.yaml | Yes | Encrypted app, infra, and Terraform secrets for production. |
infra/secrets/recipients.md | Yes | Human-readable public key to person/role/onboard date mapping. |
infra/secrets/README.md | Yes | Project-specific workflow, rotation, and downstream sync notes. |
.envrc | Optional | direnv loader that evaluates stax secrets export. |
~/.config/sops/age/keys.txt or macOS Keychain | No | Developer private age identity. Never commit it. |
Tooling boundary
Stax deliberately reuses SOPS format and behavior instead of inventing a
parallel encrypted-file format. Install sops for editing,
verification, exporting, and runner sync. The standalone age
CLI is optional because Stax can generate age identities itself and pass
them to SOPS through standard environment variables.
brew install sops
# Standard SOPS key file:stax secrets keygenstax secrets pubkey
# Optional macOS Keychain identity ring:stax vault age generatestax vault age public
Identity lookup order is compatible with SOPS: SOPS_AGE_KEY,
SOPS_AGE_KEY_FILE, SOPS_AGE_KEY_CMD, then Stax's
macOS Keychain item, then the standard SOPS age key file. A remote key
broker can be integrated by setting SOPS_AGE_KEY_CMD. If the
command source cannot expose its public recipient, pass
--recipient age1... during stax secrets init.
Bootstrap a project
stax secrets init --env productionstax secrets edit --env productionstax secrets verifygit add .sops.yaml .stax/vault.json .envrc infra/secrets/git commit -m "chore(secrets): bootstrap encrypted store"
The default file is infra/secrets/prod.enc.yaml. Supported
encrypted shapes are flat YAML, JSON, and dotenv files containing scalar
string, number, or boolean values. stax secrets init also
installs a local pre-commit hook when no hook exists yet; the hook runs
stax secrets verify.
Daily use
# List names without leaking valuesstax secrets list --env production
# Export for direnv or one-off shell usageeval "$(stax secrets export --env production --format shell)"
# Read one value for debugging onlystax secrets get STRIPE_SECRET_KEY --env production
With the scaffolded .envrc, day-to-day use is just
direnv allow once per checkout. From then on, entering the
workspace exports the decrypted environment exactly like a local GitHub
Actions run would see it.
Recipients
# Teammate sends an age1... public recipientstax secrets add-recipient age1abc... --name "Ravi <ravi@example.com>" --role engineergit diff .sops.yaml infra/secrets/recipients.md infra/secrets/prod.enc.yaml
# Offboarding removes decrypt access for future file versionsstax secrets remove-recipient age1abc...stax secrets recipients
Recipient changes are code changes. Stax updates .sops.yaml,
rewraps managed files with sops updatekeys, and keeps
infra/secrets/recipients.md in sync. Removing a recipient does
not erase values they may already have seen, so the command prints a
rotation checklist for every secret key.
Runner sync
The runner has a local mirror so workflows can read
${{ secrets.NAME }} and
${{ vars.NAME }} like GitHub Actions.
For the SOPS backend, the encrypted file remains read-only from the runner;
edit with stax secrets edit, then sync.
stax secrets verifystax secrets sync runnerstax run deploy -i environment=production
The local mirror lives under
~/.local/share/local-runner/secrets/<workspace-hash>.json.
Set RUNNER_SECRETS_KEY if you want that mirror encrypted at
rest with PBKDF2-SHA512 + AES-256-GCM. Without the key, the mirror is a
convenience cache, not the canonical source.
CI and automation consumers
For GitHub Actions or another remote runner, add a CI-only age recipient to
.sops.yaml and store only that private age key in the platform
secret store. Everything else rides in git as encrypted data.
# Example CI environmentSOPS_AGE_KEY='AGE-SECRET-KEY-...' sops decrypt infra/secrets/prod.enc.yaml
# Equivalent Stax runner modelSOPS_AGE_KEY_CMD='op read op://prod/stax-age-key/password' stax secrets sync runnerDashboard
The dashboard's /secrets page shows the repo-backed store,
managed files, recipients, verification state, runner mirror keys, backend
status, and actions for Verify and Sync Runner.
Mutating the encrypted file and recipient policy intentionally stays in the
CLI because those edits should be reviewable git changes.
Legacy runner store
stax secret still manages ad hoc local runner secrets and
variables directly. Use it for throwaway local values, but prefer
stax secrets for any value that deploys production.
stax secret set NPM_TOKEN # hidden promptstax secret liststax secret delete NPM_TOKENstax secret import .envstax secret syncstax secret statusOther backends
1Password, Doppler, R2, and GitHub repository secrets remain supported as downstream or alternate backends. They are useful when a team already has a central vault, but the default deployment workflow should keep the encrypted canonical data in the repository.
# 1Passwordstax vault loginstax vault link my-vaultstax vault sync
# Dopplerstax vault login --backend dopplerstax vault link --backend doppler my-project productionstax vault sync
# Link an existing SOPS file without scaffolding a new onestax vault link --backend sops-age --env production infra/secrets/prod.enc.yamlStatus
stax secrets status# ✓ SOPS: /opt/homebrew/bin/sops# ✓ Identity source: file /Users/me/Library/Application Support/sops/age/keys.txt# • age1...## ✓ Canonical source: git (.stax/vault.json)# • infra/secrets/prod.enc.yaml [env=production]