No description
  • TypeScript 98.9%
  • JavaScript 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Daniel Foote bfb4b840b7
Some checks failed
Dependency audit / audit (push) Has been cancelled
chore(release): 0.12.0
2026-10-30 21:52:28 +08:00
.github/workflows feat: add release tooling and bring the repo in line with AGENTS.md 2026-10-05 13:35:26 +08:00
scripts fix: list each feature once in the changelog and normalise line endings 2026-10-05 13:37:41 +08:00
src feat: read the age key from SOPS's default key file before prompting 2026-10-29 12:39:10 +08:00
.gitattributes fix: list each feature once in the changelog and normalise line endings 2026-10-05 13:37:41 +08:00
.gitflow chore: configure git-flow-next and format AGENTS.md 2026-10-05 13:30:25 +08:00
.gitignore Initial commit. 2026-10-02 17:07:10 +08:00
.prettierignore Initial commit. 2026-10-02 17:07:10 +08:00
.prettierrc Initial commit. 2026-10-02 17:07:10 +08:00
AGENTS.md feat: publish as @freefoote/sops-wrapper on the public Forgejo npm registry 2026-10-30 21:44:18 +08:00
CHANGELOG.md chore(release): 0.12.0 2026-10-30 21:52:28 +08:00
cliff.toml fix: list each feature once in the changelog and normalise line endings 2026-10-05 13:37:41 +08:00
eslint.config.mjs Initial commit. 2026-10-02 17:07:10 +08:00
LICENSE chore: license under MIT 2026-10-30 21:52:25 +08:00
package.json chore(release): 0.12.0 2026-10-30 21:52:28 +08:00
pnpm-lock.yaml feat: publish as @freefoote/sops-wrapper on the public Forgejo npm registry 2026-10-30 21:44:18 +08:00
pnpm-workspace.yaml Initial commit. 2026-10-02 17:07:10 +08:00
README.md chore: license under MIT 2026-10-30 21:52:25 +08:00
tsconfig.json Initial commit. 2026-10-02 17:07:10 +08:00
vitest.config.mjs Initial commit. 2026-10-02 17:07:10 +08:00
vitest.integration.config.mjs Initial commit. 2026-10-02 17:07:10 +08:00

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:

  1. SOPS_AGE_KEY (the key itself) or SOPS_AGE_KEY_FILE (a path to a file holding it).
  2. SOPS's default key file, if it exists: $XDG_CONFIG_HOME/sops/age/keys.txt or ~/.config/sops/age/keys.txt on Linux, ~/Library/Application Support/sops/age/keys.txt on macOS (or the XDG_CONFIG_HOME one when that is set), and %APPDATA%\sops\age\keys.txt on Windows. It must contain an AGE-SECRET-KEY-1... line (the file age-keygen or secrets keygen prints is fine).
  3. 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 encrypt produces a new ciphertext even if nothing changed. Prefer secrets edit for changes to existing files to keep diffs small.
  • Only values are encrypted: keys and comments (for example in .env files) stay readable in the .enc file, so diffs and reviews remain useful. secrets init writes encrypted_regex: '.*' into .sops.yaml for this, and secrets encrypt adds it to existing .sops.yaml rules that don't already choose what to encrypt (rules with their own encrypted_regex, unencrypted_regex or suffix settings are left alone). Files encrypted earlier keep their encrypted comments until you decrypt and encrypt them again.
  • Encrypted files are read with CRLF line endings converted to LF, so git's core.autocrlf=true on Windows does not break decrypt, edit or rekey. To stop git converting them at all, add *.enc text eol=lf and *.enc.* text eol=lf to .gitattributes; secrets check warns 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_DIR to relocate the binary cache. Set SECRETS_DEBUG=1 for 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.