shh

command module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 6, 2026 License: MIT Imports: 1 Imported by: 0

README

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 get KEY                           # print a single secret value
shh rm KEY                            # remove a secret
shh edit                              # edit all secrets in $EDITOR
shh list                              # list secret names
shh env --stdout                      # print export statements (requires --stdout)
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 --stdout)          # 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.

File Format

.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

Claude Code Integration

If your team uses Claude Code, copy CLAUDE.md.example into your project's CLAUDE.md (or append it to an existing one). This teaches Claude how to manage secrets with shh — it will use the right commands, avoid leaking values, and follow best practices automatically.

# In your project directory:
curl -sL https://raw.githubusercontent.com/stefanpenner/shh/main/CLAUDE.md.example >> CLAUDE.md

License

MIT

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
cli

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL