Skip to content

intertwine/sops-encrypted-envs-mac

Repository files navigation

SOPS + age encrypted dotenvs for macOS

SOPS + age encrypted dotenv workflow on macOS

A macOS-only toolkit for replacing local plaintext .env files with SOPS-encrypted dotenv files, plus an agent skill that helps Codex and Claude apply the pattern safely.

This repo gives you three things:

  • a macOS workflow for keeping the age private identity in Keychain while committing only encrypted dotenv files
  • a reusable agent skill that teaches Codex and Claude-style agents how to audit, migrate, and validate real repositories
  • supporting scripts, templates, docs, examples, and article drafts so the workflow is easy to adopt and easy to explain

What this repo is

If you have a pile of local repos with plaintext .env, .env.local, or other .env.* files, this repo helps you move them to a safer default:

  • commit ciphertext, not plaintext
  • keep the private age identity outside the repo
  • decrypt into process environment at command start
  • validate with a real smoke command instead of assuming encryption alone means the repo still works

This repo is intentionally macOS only. The local helper scripts use the macOS security CLI and Keychain as the default place to store the private age identity.

SOPS and age, briefly

SOPS is a tool for encrypting config files. In this repo, it encrypts dotenv files so the values on disk look like ENC[...] ciphertext instead of real API keys, tokens, or database URLs.

age is the key format SOPS uses here. It has two parts:

  • a public recipient, which looks like age1... and can be committed
  • a private identity, which looks like AGE-SECRET-KEY-... and must stay secret

This toolkit stores the private identity in macOS Keychain. When you run a wrapped command, SOPS asks Keychain for the private identity, decrypts the dotenv values into the child process environment, and exits. Your app still reads normal env vars. The repo stores ciphertext.

Why this exists

Plaintext dotenv files are convenient, but they are also easy to forget about. They get swept up by backups, editor indexing, broad rg searches, shell helpers, and coding agents doing repo-wide inspection. They are often gitignored, which helps with accidental commits, but it does nothing for the fact that the secrets still sit on disk in a long-lived plaintext file.

The approach here is simple:

  1. generate an age identity
  2. store the private identity in macOS Keychain
  3. commit only encrypted dotenv files plus .sops.yaml
  4. run apps, tests, and scripts through sops exec-env or the included wrapper
  5. validate the runtime path with a real command

That makes local repos less likely to expose secrets during broad scans, backups, and agent work without pretending this is a full enterprise secrets platform.

What's inside

Path Purpose
skills/sops-age-env-migration/ Agent skill for Codex and Claude-style agents
scripts/setup-age-keychain Generate an age identity, store it in Keychain, write the public recipient
scripts/install.sh Install the skill into Codex and/or Claude skill directories
scripts/audit-envs Find plaintext .env and .env.* files
scripts/encrypt-env Encrypt one repo's dotenv file and create .sops.yaml when needed
scripts/validate-repo Decrypt-check, ignore-check, and run a real smoke command
templates/ Copyable .env.sops wrapper scripts for target repos
docs/ Installation, usage, and security guidance
examples/ Before/after migration patterns you can adapt
articles/ Drafts for social posts, blog posts, or longer explainers

Quickstart

1. Install prerequisites

brew install sops age

2. Clone the repo and create a Keychain-backed age identity

git clone https://github.com/intertwine/sops-encrypted-envs-mac
cd sops-encrypted-envs-mac
./scripts/setup-age-keychain

That stores the private age identity in macOS Keychain and writes the public recipient to age-recipient.txt. The recipient is safe to commit or share. The private identity is not.

The first decrypting command may trigger a macOS Keychain approval dialog for /usr/bin/security. Click Always Allow if you want local dev commands to stop prompting. If you are working over SSH or another headless session, approve access once from a local GUI session first.

3. Install the agent skill

./scripts/install.sh

Defaults:

  • installs into both Codex and Claude skill directories
  • uses symlinks so local edits to the skill are picked up immediately

Useful flags:

  • --copy
  • --codex-only
  • --claude-only
  • --uninstall
  • --dry-run

4. Validate the local setup

./scripts/validate-local

Use it on one repo

Standard .env path

Use this when the app does not auto-load .env as plaintext at startup.

./scripts/audit-envs /path/to/repo
./scripts/encrypt-env /path/to/repo .env
# update .gitignore so encrypted .env can be committed
./scripts/validate-repo /path/to/repo .env -- uv run pytest

Auto-loading framework path: .env.sops

Frameworks like Vite, Next.js, Rails, and many Python CLIs will eagerly read .env. For those, prefer .env.sops and a wrapper boundary:

mv /path/to/repo/.env /path/to/repo/.env.sops
./scripts/encrypt-env /path/to/repo .env.sops
mkdir -p /path/to/repo/scripts
cp templates/sops-env /path/to/repo/scripts/sops-env
cp templates/read-age-key-from-keychain /path/to/repo/scripts/read-age-key-from-keychain
chmod +x /path/to/repo/scripts/sops-env /path/to/repo/scripts/read-age-key-from-keychain
./scripts/validate-repo /path/to/repo .env.sops -- npm run test:raw

Pass the raw command that expects environment variables. If the public script already wraps SOPS, validate the corresponding internal command like test:raw, dev:raw, or serve-raw to avoid double-wrapping.

Common .gitignore changes

The point is to stop ignoring the encrypted file while still ignoring any plaintext fallbacks you intentionally keep.

Standard .env path:

# before
.env
.env.*

# after
.env.local
.env.development
.env.test

.env.sops path:

# before
.env
.env.*

# after
.env
.env.*
!.env.sops

Using the agent skill

Once installed, you can hand a repo to your coding agent and be explicit about the runtime command you want proven:

Migrate this repo from plaintext dotenv files to SOPS + age, prefer .env.sops if the framework auto-loads .env, and validate with npm run test:raw.

The skill is designed to teach agents to:

  • inspect the repo before editing
  • choose .env vs .env.sops correctly
  • update wrapper commands instead of writing decrypted files back to disk
  • validate with a real smoke command
  • report the exact command that proved the migration

Daily workflow after migration

Once a repo is migrated, developers should run the commands that decrypt at the outer boundary: npm run dev, npm test, make test, scripts/sops-env .env.sops -- uv run pytest, sops exec-env .env 'uv run pytest', or whatever wrapper the repo exposes. You do not run validate-repo on every edit. Use validate-repo when you first migrate a repo, after changing wrapper commands, or after rotating recipients.

Security caveats

This repo improves local secret hygiene. It does not solve every secret-management problem.

  • It helps with plaintext-on-disk risk, accidental commits, broad local searches, and coding-agent inspection of repos.
  • It does not protect you from malware or an attacker already running as your macOS user.
  • It does not prevent secrets from leaking into logs, crash reports, shell history, or test output.
  • It does not erase historical exposure if plaintext .env files were already committed.
  • It is intentionally a local developer workflow, not a full team secret distribution platform.

Read docs/security.md before publishing this pattern inside a team.

Further hardening

This repo is deliberately scoped. If you need stronger guarantees, add controls outside the scope of this toolkit:

  • Per-person or per-device recipients so one stolen key does not unlock every repo
  • Secret rotation after migration, especially if the plaintext file ever lived in git history
  • Push protection and secret scanning on GitHub or your git host
  • 1Password if you want a more user-friendly human secret store than raw Keychain usage
  • Vault, Infisical, Doppler, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault if you need shared access, RBAC, audit trails, dynamic secrets, or production-grade service-to-service secret delivery

The rule of thumb: this repo is for making local repos less brittle and less leaky. When you need team policy, central auditability, or dynamic credentials, graduate to a real secret manager.

CI is intentionally secondary to the local workflow here. If you need CI decryption, use a dedicated CI recipient or SOPS_AGE_KEY secret, not the macOS Keychain helper. docs/security.md includes a minimal GitHub Actions example plus recipient-rotation guidance.

Docs, examples, and articles

Development

./scripts/validate-local
shellcheck scripts/* templates/sops-env templates/read-age-key-from-keychain

License

MIT - see LICENSE.

About

macOS toolkit and agent skill for turning local plaintext .env files into SOPS + age encrypted dotenvs backed by Keychain.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages