- TypeScript 98.9%
- JavaScript 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| scripts | ||
| src | ||
| .gitattributes | ||
| .gitflow | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| cliff.toml | ||
| eslint.config.mjs | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.mjs | ||
| vitest.integration.config.mjs | ||
sops-wrapper
A tiny CLI (secrets) for committing encrypted .yaml, .json and .env secrets to git using
SOPS and age.
- Self-contained: the pinned SOPS binary is downloaded on first use (SHA-256 verified) into a per-user cache. Nothing needs to be installed on the system.
- Convention over config: encrypted files are named
*.enc.*; the plaintext twin is gitignored automatically. - Your key stays yours: the age private key is read from
SOPS_AGE_KEY/SOPS_AGE_KEY_FILE, from SOPS's default key file if you keep one, or typed into a hidden prompt. The wrapper never writes it to disk.
Install
The package is published to a public npm registry on Forgejo, not to npmjs.org. Point the @freefoote scope at it in
the project's .npmrc. No login or token is needed:
@freefoote:registry=https://git.freefoote.net/api/packages/open-source/npm/
Then install it and run it:
pnpm add -D @freefoote/sops-wrapper
pnpm exec secrets help
It needs Node 24+. The CLI is one bundled file with no runtime dependencies and no install scripts, so a Docker build
can install it like any other package (copy the .npmrc in before pnpm install).
pnpm 11 and later hold back versions published within the last day (minimumReleaseAge). To install a new release
straight away, add the scope to minimumReleaseAgeExclude in pnpm-workspace.yaml:
minimumReleaseAgeExclude:
- '@freefoote/*'
Releases and their notes are on the Forgejo releases page and in CHANGELOG.md.
Quick start
secrets init # writes .sops.yaml, generates a keypair and prints the private key ONCE
# -> store the AGE-SECRET-KEY-1... line in your password manager
echo 'API_KEY=hunter2' > .env
secrets encrypt .env # creates .env.enc, adds /.env to .gitignore
git add .env.enc .sops.yaml .gitignore
secrets decrypt # prompts for the key, restores every plaintext file
secrets edit .env.enc # edit in $EDITOR, re-encrypted on save
Where the private key comes from
Commands that decrypt (decrypt, edit, rekey) look for the age private key in this order:
SOPS_AGE_KEY(the key itself) orSOPS_AGE_KEY_FILE(a path to a file holding it).- SOPS's default key file, if it exists:
$XDG_CONFIG_HOME/sops/age/keys.txtor~/.config/sops/age/keys.txton Linux,~/Library/Application Support/sops/age/keys.txton macOS (or theXDG_CONFIG_HOMEone when that is set), and%APPDATA%\sops\age\keys.txton Windows. It must contain anAGE-SECRET-KEY-1...line (the fileage-keygenorsecrets keygenprints is fine). - A hidden prompt on the terminal. Without a terminal the command fails instead.
Prompting is the safest default: the key lives only in your password manager and in memory for one command. Keep a key file only on machines you control and trust, such as a locked-down deployment workstation with no other users or AI agents, and make it readable only by you:
mkdir -p ~/.config/sops/age && chmod 700 ~/.config/sops/age
# paste the AGE-SECRET-KEY-1... line into the file, then:
chmod 600 ~/.config/sops/age/keys.txt
Naming convention
| Plaintext (gitignored) | Encrypted (committed) |
|---|---|
app.yaml / .yml |
app.enc.yaml |
config.json |
config.enc.json |
.env, .env.prod |
.env.enc, .env.prod.enc |
Commands
| Command | What it does |
|---|---|
init [--recipient age1...]... |
Create .sops.yaml; generates a keypair when no recipient is given |
keygen [--add] |
Print a new keypair; --add registers its public key |
recipient list|add|remove [age1...] |
Manage who can decrypt (public keys only) |
encrypt [files...] |
Encrypt named files; with no args, re-encrypt every plaintext that has an .enc twin |
decrypt [files...] [--force] [--stdout] |
Decrypt all or named files; refuses to overwrite local edits unless --force |
edit <file> |
Edit in place via SOPS |
rekey [files...] |
Re-encrypt to the current recipient list (run after recipient add/remove) |
check |
For CI / pre-commit: fails if an .enc file isn't really encrypted or its plaintext is tracked |
install |
Pre-download the SOPS binary and print its path |
sops [args...] |
Run the managed SOPS binary directly |
Notes
- Encrypting is non-deterministic, so
secrets encryptproduces a new ciphertext even if nothing changed. Prefersecrets editfor changes to existing files to keep diffs small. - Only values are encrypted: keys and comments (for example in
.envfiles) stay readable in the.encfile, so diffs and reviews remain useful.secrets initwritesencrypted_regex: '.*'into.sops.yamlfor this, andsecrets encryptadds it to existing.sops.yamlrules that don't already choose what to encrypt (rules with their ownencrypted_regex,unencrypted_regexor suffix settings are left alone). Files encrypted earlier keep their encrypted comments until youdecryptandencryptthem again. - Encrypted files are read with CRLF line endings converted to LF, so git's
core.autocrlf=trueon Windows does not breakdecrypt,editorrekey. To stop git converting them at all, add*.enc text eol=lfand*.enc.* text eol=lfto.gitattributes;secrets checkwarns when an encrypted file has CRLF endings. - Removing a recipient does not revoke access to ciphertext already in git history; rotate the underlying secrets.
- Set
SECRETS_CACHE_DIRto relocate the binary cache. SetSECRETS_DEBUG=1for full error stacks.
Development
pnpm install
pnpm start -- help # run from source
pnpm test # unit tests
pnpm test:integration # real SOPS download + full encrypt/decrypt workflow
pnpm build # bundle to dist/cli.js (also run by pnpm pack and pnpm publish)
Upgrading SOPS
Edit SOPS_VERSION and the per-platform SHA-256 digests in src/platform.ts (digests are listed on the SOPS GitHub release
page, or in its checksums.txt).
Licence
MIT. See LICENSE.