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.

FileCommittedPurpose
.sops.yamlYesReviewable age recipient policy for managed secret files.
.stax/vault.jsonYesStax binding that tells the runner which SOPS files map to which environment.
infra/secrets/prod.enc.yamlYesEncrypted app, infra, and Terraform secrets for production.
infra/secrets/recipients.mdYesHuman-readable public key to person/role/onboard date mapping.
infra/secrets/README.mdYesProject-specific workflow, rotation, and downstream sync notes.
.envrcOptionaldirenv loader that evaluates stax secrets export.
~/.config/sops/age/keys.txt or macOS KeychainNoDeveloper 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 keygen
stax secrets pubkey
# Optional macOS Keychain identity ring:
stax vault age generate
stax 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 production
stax secrets edit --env production
stax secrets verify
git 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 values
stax secrets list --env production
# Export for direnv or one-off shell usage
eval "$(stax secrets export --env production --format shell)"
# Read one value for debugging only
stax 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 recipient
stax secrets add-recipient age1abc... --name "Ravi <ravi@example.com>" --role engineer
git diff .sops.yaml infra/secrets/recipients.md infra/secrets/prod.enc.yaml
# Offboarding removes decrypt access for future file versions
stax 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 verify
stax secrets sync runner
stax 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 environment
SOPS_AGE_KEY='AGE-SECRET-KEY-...' sops decrypt infra/secrets/prod.enc.yaml
# Equivalent Stax runner model
SOPS_AGE_KEY_CMD='op read op://prod/stax-age-key/password' stax secrets sync runner

Dashboard

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 prompt
stax secret list
stax secret delete NPM_TOKEN
stax secret import .env
stax secret sync
stax secret status

Other 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.

# 1Password
stax vault login
stax vault link my-vault
stax vault sync
# Doppler
stax vault login --backend doppler
stax vault link --backend doppler my-project production
stax vault sync
# Link an existing SOPS file without scaffolding a new one
stax vault link --backend sops-age --env production infra/secrets/prod.enc.yaml

Status

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]