shh
Warning: Experimental — not yet ready for production use.
🤫 Commit your secrets. Yes, really.
- Encrypted secrets live in your repo, safe to push, easy to share.
- Add teammates by GitHub username —
shh users add alice fetches their SSH key automatically.
- No GitHub? Pass an age key directly.
- One binary, one dependency (
gh). Private keys stay in your OS keyring.
Install
brew install stefanpenner/tap/shh
Or from source:
go install github.com/stefanpenner/shh@latest
Quick Start
shh init # one-time setup (requires gh auth login)
shh set DATABASE_URL postgres://localhost/mydb
shh set API_KEY sk-secret123
shh shell # launch a shell with secrets loaded
No plaintext .env file ever touches disk.
Commands
shh set KEY value # add or update a secret
shh rm KEY # remove a secret
shh edit # edit all secrets in $EDITOR
shh list # list secret names
shh env # print export statements
shh shell # open a shell with secrets loaded
shh run -- <cmd> [args...] # run a command with secrets injected
shh template <file> # render a template with secrets substituted
shh doctor # check your setup for common issues
shh whoami # show your key and identity
All commands default to .env.enc. Use -e to pick an environment:
shh shell -e staging # opens shell with staging.env.enc
shh run -e production -- node app.js # runs with production secrets
Or pass a filename directly:
shh set KEY value staging.env.enc
Already have a .env file? Encrypt it:
shh encrypt .env # creates .env.enc (then delete .env)
Team Workflow
# You (project owner)
shh init
shh set SECRET supersecret
git add .env.enc && git push
# Add a teammate
shh users add alice
git add .env.enc && git push
# Alice (joining the project)
shh login # auto-detects SSH key via GitHub
shh shell # works immediately
shh users list # show who has access
shh users add <username-or-key> # add by GitHub username or age key
shh users remove <user|#> # revoke access (rotates data key)
CI / Production
Create a deploy key for environments that don't have a GitHub identity:
shh users add --name production-deploy
# Prints a secret key — store it as SHH_AGE_KEY in your CI/deploy platform
git add .env.enc && git push
Then in your CI pipeline or Dockerfile:
shh run -- node app.js # secrets injected, SHH_AGE_KEY auto-filtered
# or
eval $(shh env -q) # export secrets into the current shell
You manage one platform secret (SHH_AGE_KEY); everything else lives in .env.enc.
If you already have an age public key, pass it directly:
shh users add --name staging --key age1xyzt...
How It Works
shh uses envelope encryption: a random 32-byte AES-256 data key encrypts all secrets, and that data key is wrapped (age-encrypted) individually to each recipient.
- Adding a user — re-wraps the existing data key for the new recipient. Secrets don't change.
- Removing a user — generates a new data key, re-encrypts all secrets, wraps only for remaining recipients. The old key becomes useless.
Private keys stay in your OS keyring (macOS Keychain, GNOME/KDE Secret Service, Windows Credential Manager). Set SHH_AGE_KEY to override for CI/Docker. Set SHH_PLAINTEXT to point at a plain .env file to skip decryption entirely.
.env.enc is TOML:
version = 2
mac = "hmac-sha256-hex"
[recipients]
"https://github.com/alice" = "age1..."
"https://github.com/bob" = "age1..."
[wrapped_keys]
"https://github.com/alice" = "base64-data-key-wrapped-to-alice"
"https://github.com/bob" = "base64-data-key-wrapped-to-bob"
[secrets]
DATABASE_URL = "base64-aes-256-gcm-ciphertext"
API_KEY = "base64-aes-256-gcm-ciphertext"
| Section |
Purpose |
recipients |
Maps identities to age public keys |
wrapped_keys |
The data key, individually wrapped to each recipient — one entry per recipient so branch additions don't conflict |
secrets |
Values encrypted with AES-256-GCM using the data key; key name is authenticated data |
mac |
HMAC-SHA256 over all fields, verified on every decrypt |
Git Merge Support
Per-recipient wrapped keys mean adding teammates on different branches won't conflict. If a merge conflict does occur, any shh command auto-detects it, performs a semantic 3-way merge, and stages the result. True conflicts (same key modified both sides) are reported for manual resolution.
For proactive conflict prevention:
# .gitattributes (commit this)
*.env.enc merge=shh
# ~/.gitconfig (each developer)
[merge "shh"]
name = shh encrypted env merge
driver = shh merge %O %A %B
License
MIT