key is a macOS secret manager for people who like what the venerable pass gets right:
- Secrets are stored as encrypted files, not in an opaque, app-specific database
- Flexible directory structure lets you organize and reason about secrets hierarchically
- Small, CLI-first command set with full flexibility from the shell
The difference is that, instead of pass’s GPG agent workflow, key handles authentication the native macOS way, using launchd, XPC, Mach services, userPresence, and Keychain.
- Each secret is stored as an individually encrypted file on disk, under
~/.keyby default. - All secret files are encrypted and decrypted using a single, randomly generated 256-bit symmetric vault key.
- That vault key is stored securely in your macOS Keychain (not the secrets themselves!).
- Access to the vault key in Keychain is protected by macOS local authentication—Touch ID, Apple Watch, or your system password—using
userPresence. - The CLI talks to an on-demand LaunchAgent helper over XPC using a Mach service.
- After a successful unlock, the helper keeps the vault key in memory for a short idle window and reuses it across separate CLI invocations without prompting again.
- When the helper has been idle long enough, it clears the in-memory key and exits.
Install via Homebrew from the tvanreenen/tap tap:
brew tap tvanreenen/tap
brew install --cask keyOpen Key.app once after install so it can register Key Agent with macOS before you use the key CLI.
The CLI is intentionally small:
key unlock # authenticate and warm the helper session
key lock # clear the helper session and stop the helper
key get <name> # print a secret or current TOTP code
key copy <name> # copy a secret or current TOTP code
key add <name> [--totp] # add a new secret or TOTP seed from stdin or prompt
key edit <name> [--totp] # update a secret or TOTP seed from stdin or prompt
key list # list stored secrets
key duplicate <src> <dst> [--force] # duplicate an entry
key rename <src> <dst> [--force] # rename an entry
key remove <name> [--force] # remove a secret
key config get <config-name> # print a config value
key config set <config-name> <value> # update a config value
key config list # list known config values
key version [--json] # print the CLI versionUnlike most password managers, key does not include a built-in password generator. Instead, it is designed to accept input via stdin, so you can add or edit secrets either by securely typing them in (using your terminal's secure input), or by piping in passwords generated by any tool or method you prefer:
openssl rand -base64 32 | key add aws/prod/token
openssl rand -hex 32 | key add api/key
pwgen -sy 24 1 | key edit github/personal
diceware -n 6 | key add personal/passphrase
xkcdpass -n 4 | key add outlook/work
uuidgen | key add app/token
head -c 32 /dev/urandom | base64 | key add backup/recoverykey also supports calculating time-based one-time passwords. When you store a provided Base32 seed using the --totp flag, key will know to calculate TOTP using RFC 6238 each time you use the get or copy methods on that secret.
key add github/mfa --totp
key edit github/mfa --totp
key get github/mfa
key copy github/mfaFor now, --totp accepts only bare Base32 seeds and not full otpauth://totp/Issuer:AccountName?secret=YourSecret&issuer=IssuerName URLs. If all you are given is the full URL, just copy and store the secret from that URL. That is the bare Base32 seed that key expects.
By default, key stores its config in ~/Library/Application Support/Key/config.toml and its encrypted secret files in ~/.key.
If you want to move the vault, move the files yourself and then update the configured path:
mv ~/.key ~/Secrets/key-vault
key config set vault-dir ~/Secrets/key-vaultIf ~/.key already exists and contains unrelated files, Key will refuse to adopt it as the default vault root. In that case, choose another vault directory with key config set vault-dir <path>.
One the things that make retrieving secret especially efficient is using key list with fzf to give you really strong fuzzy finding of your secrets:
key get "$(key list | fzf)"
key copy "$(key list | fzf)"
key edit "$(key list | fzf)"
key remove "$(key list | fzf)"key uses standard AES-256-GCM encryption with zero custom cryptography. If you have both the vault key and your .secret files, you're not locked in: you can decrypt your secrets using any tool that supports AES-GCM, letting you move your data without relying on the app.
Where the files live: Secrets are under ~/.key by default. An entry like github/personal is stored as ~/.key/github/personal.secret. The active vault path is configured in ~/Library/Application Support/Key/config.toml and can be inspected with key config get vault-dir.
Payload format: Each .secret file contains a JSON object:
{
"version": 2,
"type": "secret",
"alg": "AES.GCM",
"nonce": "<base64-encoded 96-bit nonce>",
"ciphertext": "<base64-encoded AES-GCM ciphertext + 16-byte auth tag>"
}For TOTP entries, the envelope is the same except type is totp; the decrypted plaintext is the normalized Base32 seed rather than a password.
Without the vault key (the 256-bit secret kept in your Keychain), the file contents are completely opaque.
How to decrypt: To unlock a secret yourself, parse the JSON and base64-decode both nonce and ciphertext. Split the decoded ciphertext into the payload (everything except the final 16 bytes) and the authentication tag (the last 16 bytes). Decrypt the payload using the vault key and nonce with AES-256-GCM—the result will be your UTF-8 plaintext.
key is not just a standalone CLI binary. To use the stronger macOS Keychain and user-presence path correctly, it is structured as three pieces:
Key.appkeyCLI client- LaunchAgent helper
The host app exists to give the project a proper macOS app identity, signing context, entitlements, and release shape, and to register the bundled LaunchAgent helper on first launch. It is not intended to be a full GUI password manager.
The CLI is the user-facing interface. It handles:
- command parsing
- stdin and secure prompt input
- stdout and stderr output
- clipboard writes for
key copy
The CLI does not directly access the protected vault key.
The helper is the privileged side of the system. It is managed by launchd, reachable through a Mach service, and owns:
- Keychain access
userPresence-gated vault key retrieval- encryption and decryption
- on-disk secret file access
- the short-lived in-memory unlock session
This split gives key a shape that is similar in spirit to ssh-agent or gpg-agent: a user-session helper keeps unlocked key material in memory so repeated CLI commands can reuse it. The difference is that key uses the native macOS service model instead of a Unix socket convention:
launchdstarts the helper on demand- the CLI talks to it over XPC using a Mach service
- the helper exits when it has been idle, so nothing is permanently running
That gives key a few nice properties:
- reliable unlock reuse across separate CLI invocations
- no long-lived decrypted secrets on disk
- no permanently running background process when idle
- native macOS process management, signing, and IPC
Conceptually, a get looks like this:
key get github/personal- if needed,
launchdstarts the helper when the CLI connects to its Mach service - the CLI sends a request to the helper over XPC
- if the helper is locked, it asks macOS for access to the vault key
- macOS enforces the Keychain item's
userPresencerequirement through its normal local-authentication path - the helper decrypts the secret file
- the CLI prints the result to stdout
Conceptually, an explicit unlock looks like this:
key unlock- the CLI connects to the helper's Mach service
- if needed,
launchdstarts the helper - the helper asks macOS for access to the vault key
- on success, the helper keeps the vault key in memory for a short idle window
- later
get,copy,add, oreditrequests can reuse that in-memory authorization without prompting again - after the helper has been idle long enough, it drops the key and exits
That is the tradeoff that makes the native macOS auth path possible while keeping the day-to-day interface CLI-first. This is intentionally macOS-specific and optimizes for native platform integration over cross-platform portability.
