| name | sops-age-env-migration |
| description | Convert software repositories from plaintext dotenv files to SOPS + age encrypted dotenv files. Use when asked to replace .env files with encrypted secrets, migrate a repo to sops/age, update Node/Python/Ruby/Go/Docker/Make dev and test commands to decrypt at runtime, add agent-facing docs such as AGENTS.md or CLAUDE.md, or verify that a repo no longer depends on long-lived plaintext .env files. |
SOPS + age env migration
Core rule
Never leave decrypted secret files in the repo. Do not print secret values. Make the normal project commands decrypt into process environment at runtime, then validate that the encrypted file is ciphertext and the app/test commands still work.
Workflow
- Inspect the repo before changing it:
pwd, git status --short --branch
rg -n "dotenv|load_dotenv|godotenv|direnv|process\\.env|os\\.environ|ENV\\[|--env-file|env_file|\\.env(\\.|$)" .
- package/task files:
package.json, pyproject.toml, Makefile, docker-compose*.yml, Procfile, bin/*, CI files.
- Decide the encrypted file name:
- Use encrypted
.env only if the app will not auto-load it as plaintext/ciphertext, or you remove/disable the automatic dotenv loader.
- Prefer
.env.sops for frameworks that auto-load .env (next, vite, Rails, many Python CLIs) so ciphertext is not parsed by the app.
- Create or update
.sops.yaml with the age recipient and a narrow path_regex.
- Encrypt the secret file using the project toolkit when available:
scripts/encrypt-env REPO ENV_FILE
- or
sops encrypt --input-type dotenv --output-type dotenv --filename-override ENV_FILE --in-place ENV_FILE
- Update project commands so humans and agents decrypt at process start:
sops exec-env .env 'command' for encrypted files named .env
scripts/sops-env .env.sops -- command or toolkit scripts/run-with-env .env.sops -- command for .env.sops
- Keep plaintext
.env, .env.local, and other .env.* fallbacks ignored or absent. Ensure the encrypted file is not ignored and will be committed.
- Be explicit in
.gitignore edits. For .env.sops, a common pattern is keeping .env* ignored but adding !.env.sops.
- Validate:
sops decrypt --input-type dotenv --output-type dotenv ENV_FILE >/dev/null
- project tests or a representative smoke command through the SOPS wrapper
scripts/validate-repo REPO ENV_FILE -- smoke command when the toolkit is available.
- Report exactly what changed and what command proved the migration.
Conversion Patterns
Use the SOPS wrapper at the outermost command boundary. Plain sops exec-env infers dotenv parsing from filenames ending in .env; for .env.sops, copy templates/sops-env and templates/read-age-key-from-keychain into the target repo as scripts/sops-env and scripts/read-age-key-from-keychain.
scripts/sops-env .env.sops -- npm run dev:raw
scripts/sops-env .env.sops -- uv run pytest
scripts/sops-env .env.sops -- go test ./...
Do not wrap a command in itself. For package scripts, create raw/internal scripts:
{
"scripts": {
"dev": "scripts/sops-env .env.sops -- npm run dev:raw",
"dev:raw": "next dev",
"test": "scripts/sops-env .env.sops -- npm run test:raw",
"test:raw": "vitest run"
}
}
For Makefiles:
SOPS_ENV ?= .env.sops
.PHONY: test test-raw
test:
scripts/sops-env $(SOPS_ENV) -- $(MAKE) test-raw
test-raw:
uv run pytest
Language References
Read references/language-patterns.md when adapting a specific stack. It covers common Node/TypeScript, Python, Ruby/Rails, Go, Docker Compose, Makefile, and CI patterns.
Read references/validation-checklist.md before declaring the migration done.
Guardrails
- Do not commit private age identities, decrypted
.env files, screenshots of secrets, shell history, or logs containing secrets.
- Do not use
sops decrypt ENV_FILE > .env as a steady-state solution.
- Do not assume encrypted
.env is drop-in compatible with framework dotenv loaders.
- Do not bulk-migrate multiple repos until one repo has passed runtime validation.
- If existing
.env is already committed in git history, note that encryption now does not remove historical exposure; recommend rotation and history remediation separately.