A single-file shell secret manager backed by GPG symmetric (AES256) encryption.
Each secret is stored as ~/.shhh/<name>.gpg. Listing never decrypts; only get and edit ever touch plaintext. Values never appear in process arguments, shell history, or persistent temp files.
cp shhh ~/.local/bin/shhh
chmod +x ~/.local/bin/shhhMake sure ~/.local/bin is on your $PATH (add to ~/.zshrc or ~/.bashrc if needed):
export PATH="$HOME/.local/bin:$PATH"| Dependency | How to install |
|---|---|
bash ≥ 3.2 |
Pre-installed on Linux and macOS |
gpg (GnuPG) |
pacman -S gnupg · apt install gnupg · brew install gnupg |
sha256sum or shasum |
Pre-installed on Linux and macOS |
shred |
Optional — overwrites temp files before deletion; falls back to rm -f if absent (not available on macOS) |
| Variable | Default | Purpose |
|---|---|---|
SHHH_DIR |
~/.shhh |
Directory where <name>.gpg files are stored |
EDITOR |
nvim |
Editor opened by shhh edit |
TMPDIR |
/tmp |
Base directory for temp files used by shhh edit |
# Store a secret — prompted silently, confirmed
shhh set db_password
# Store from a pipe (one trailing newline is stripped)
echo "s3cr3t" | shhh set api_key
printf '%s' "s3cr3t" | shhh set api_key
# Retrieve — output is exactly the stored bytes, no newline added
shhh get db_password
shhh get api_key | pbcopy # copy to clipboard (macOS)
shhh get api_key | xclip -selection clipboard # copy to clipboard (Linux)
# Use inline in scripts
psql "postgres://user:$(shhh get db_password)@host/db"
# List all secret names (never decrypts)
shhh list
# Edit a secret in place
shhh edit api_key
EDITOR=nano shhh edit api_key # override editor for one invocation
# Rename or copy (operates on .gpg files directly — no decryption)
shhh rename api_key stripe_api_key
shhh cp stripe_api_key stripe_api_key_backup
# Delete (asks for confirmation)
shhh rm stripe_api_key_backup
# Generate a random secret from /dev/urandom (base64-encoded)
shhh gen session_secret # 32 bytes (default)
shhh gen signing_key 64 # 64 bytesGPG will prompt for your symmetric passphrase via whatever pinentry program is
configured in ~/.gnupg/gpg-agent.conf. On a desktop this is often a GUI dialog.
To keep everything in the terminal, install a terminal-based pinentry and point
the agent at it:
Arch / Debian / Ubuntu
# Arch
sudo pacman -S pinentry
# Debian / Ubuntu
sudo apt install pinentry-tty# ~/.gnupg/gpg-agent.conf
pinentry-program /usr/bin/pinentry-tty
macOS
brew install pinentry-mac# ~/.gnupg/gpg-agent.conf
pinentry-program /opt/homebrew/bin/pinentry-mac
Reload the agent after any change to gpg-agent.conf:
gpg-connect-agent reloadagent /byeBy default GPG prompts on every get or edit. To cache it:
# ~/.gnupg/gpg-agent.conf
default-cache-ttl 3600 # seconds since last use (1 hour)
max-cache-ttl 86400 # hard ceiling (24 hours)
Security tradeoff: while the passphrase is cached, any process running as your user can decrypt your secrets without a prompt. Set
default-cache-ttl 0to disable caching entirely.
$SHHH_DIRis createdchmod 700; every.gpgfile ischmod 600.- Plaintext values never appear in process arguments (visible via
ps) or shell history. - Encryption writes to a staging temp file first and only replaces the destination with
mvon success — a failed encryption cannot truncate an existing secret. - All temp files are registered on creation and removed by an
EXITtrap (viashred -uif available, otherwiserm -f), including on unexpected failure. shhh list,shhh rename, andshhh cpnever decrypt anything.
edit writes plaintext to disk |
The decrypted value lives in $TMPDIR during editing. On most Linux systems $TMPDIR is tmpfs (RAM-backed), but this is not guaranteed. For automated use, prefer set/get. |
| Shared passphrase | All secrets share one GPG passphrase. There is no per-secret key. Anyone with the passphrase can decrypt every secret in $SHHH_DIR. |
| No versioning | shhh set overwrites silently. There is no history or audit log. |
/dev/tty required |
The TTY prompt (set), delete confirmation (rm), and editor (edit) all open /dev/tty. This fails in fully detached environments such as some CI runners — use pipe mode for set and avoid rm/edit in those contexts. |
| Filesystem atomicity | mv is only atomic when source and destination share a filesystem. The staging file is created inside $SHHH_DIR to guarantee this. If you point SHHH_DIR at a separate mount point, verify this still holds. |
| Multi-line pipe input | Only one trailing newline is stripped. `printf 'a\nb\n\n' |