Skip to content

tvanreenen/key

Repository files navigation

Key

Key

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

key uses the same model, but instead of pass’s GPG agent workflow, it relies on native macOS encryption and authentication. Under the hood it works a bit like ssh-agent or gpg-agent: a short-lived helper caches unlocked key material in memory. The difference is that key does it the native macOS way, using launchd, XPC, Mach services, and Keychain userPresence.

How it works

  • Each secret is stored as an individually encrypted file on disk, under ~/Library/Application Support/key/vault.
  • 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.

The result: your secrets stay encrypted on the filesystem, protected by a single key, and only you can access that key thanks to native macOS authentication.

Install

Warning

key is still in early development. There is not a public release yet.

Install via Homebrew from the tvanreenen/tap tap:

brew tap tvanreenen/tap
brew install --cask key

Open Key.app once after install so it can register Key Agent with macOS before you use the key CLI.

CLI

The CLI is intentionally small:

key unlock                      # authenticate and warm the helper session
key add <name>                  # add a new secret from stdin or prompt
key edit <name>                 # update a secret from stdin or prompt
key list                        # list stored secrets
key get <name>                  # print a secret
key copy <name>                 # copy a secret to the clipboard
key duplicate <src> <dst> [--force]  # duplicate an entry
key rename <src> <dst> [--force]     # rename an entry
key remove <name> [--force]     # remove a secret

Generating passwords

Unlike 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/recovery

Fuzzy picking with fzf

If you use fzf, key list composes cleanly with it:

key get "$(key list | fzf)"
key copy "$(key list | fzf)"
key edit "$(key list | fzf)"
key remove "$(key list | fzf)"

This stays fully optional. key does not depend on fzf, but the combination works well when you want fuzzy selection across a larger store.

Security without the lock-in

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 or audit it without relying on the app.

Where the files live: Secrets are under ~/Library/Application Support/key/vault. An entry like github/personal is stored as ~/Library/Application Support/key/vault/github/personal.secret.

Payload format: Each .secret file contains a JSON object:

{
  "version": 1,
  "alg": "AES.GCM",
  "nonce": "<base64-encoded 96-bit nonce>",
  "ciphertext": "<base64-encoded AES-GCM ciphertext + 16-byte auth tag>"
}

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.

Nerdy details about the macOS integration

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:

  1. Key.app
  2. key CLI client
  3. LaunchAgent helper

Key.app

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.

key CLI client

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.

LaunchAgent helper

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:

  • launchd starts 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:

  1. key get github/personal
  2. if needed, launchd starts the helper when the CLI connects to its Mach service
  3. the CLI sends a request to the helper over XPC
  4. if the helper is locked, it asks macOS for access to the vault key
  5. macOS enforces the Keychain item's userPresence requirement through its normal local-authentication path
  6. the helper decrypts the secret file
  7. the CLI prints the result to stdout

Conceptually, an explicit unlock looks like this:

  1. key unlock
  2. the CLI connects to the helper's Mach service
  3. if needed, launchd starts the helper
  4. the helper asks macOS for access to the vault key
  5. on success, the helper keeps the vault key in memory for a short idle window
  6. later get, copy, add, or edit requests can reuse that in-memory authorization without prompting again
  7. 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.

About

A file-based CLI secret manager with native macOS auth

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project