Keymaster is a small binary written in Swift that allows scripts to access the Mac Keychain guarded by TouchID.
Macs come with the security command which can get and set secrets to the
Keychain:
# Save a key/value to the default "login" keychain, with key "MyKeyName",
# update if exists (-U), allow no app to access without a prompt (-T ""),
# and prompt for secret to store (-w)
security add-generic-password -a login -s "MyKeyName" -T "" -U -w
# Get the secret value from a key
security find-generic-password -s "MyKeyName" -wYou can use security in a script, but (AFAIK) you can't tell it to use
biometrics to guard secrets, you have to enter the password each time, or
"always allow" the security binary to access the secret.
Keymaster fixes this.
Compile keymaster.swift into a binary:
swiftc -O -o keymaster keymaster.swift -framework LocalAuthentication -framework SecurityPut the keymaster binary somewhere in your $PATH, or run it directly from
the project directory.
keymaster [options] get <key> # Retrieve a secret (printed to stdout)
echo <secret> | keymaster [options] set <key> # Store a secret (read from stdin)
keymaster [options] delete <key> # Delete a secret
Options:
-v Enable debug logging (stderr)
-s, --session <name> Use a named session shared across keys and processes (see Sessions)
On first use, macOS will show two Keychain Access prompts asking whether to
allow keymaster to access keychain items. Select Always Allow for both:
- HMAC session key (
keymaster_session_hmac_key) — used internally to validate sessions. This is read on every invocation, before TouchID, so it must be accessible without a prompt. - Your secret — the actual keychain item you're storing or retrieving.
Keymaster handles authentication itself via TouchID. The keychain is a passive store — "Always Allow" makes it transparent, leaving TouchID as the sole authentication gate. If you don't select "Always Allow", you'll get a keychain dialog on every invocation in addition to TouchID.
To change a secret, delete and re-set it, or edit it directly in
Keychain Access.app.
After a successful TouchID authentication, keymaster caches the auth so that
subsequent calls within a TTL window can skip the prompt. The default TTL is
5 minutes (300 seconds); configure with KEYMASTER_TTL:
# Extend to 10 minutes
export KEYMASTER_TTL=600
# Require TouchID every time
export KEYMASTER_TTL=0The session file lives in $TMPDIR (a per-user directory on macOS, mode 700),
HMAC-SHA256 signed with a key stored in the keychain. Key and session names are
hashed before being written, so the file does not reveal which entries have
been accessed. It does not persist across reboots; expired entries are pruned
automatically.
Each key has its own session, bound to the calling POSIX session
(getsid(0)). The cache only satisfies subsequent calls from the same terminal
or process tree, and authenticating for one key does not affect another.
Pass -s <name> or set KEYMASTER_SESSION=<name> to share auth across
multiple keys and across unrelated processes:
KEYMASTER_SESSION=aws keymaster get aws_access_key_id
KEYMASTER_SESSION=aws keymaster get aws_secret_access_keyA named session is not bound to the POSIX session ID — any process running
as the same user can use the cache within the TTL window as long as it
provides the same name. This is the right escape hatch for agentic shells
(each command may run under a fresh setsid()) and long-running daemons.
The flag takes precedence over the environment variable.
Per-key sessions' SID binding prevents an unrelated same-UID process from racing the TTL window to piggyback on a recent auth. Named sessions opt out of that binding by design — any same-UID process that knows the name can use the cache. The keychain ACL boundary (same-UID, post-TouchID) is the underlying security control; the SID binding is defense-in-depth that named sessions trade for cross-process sharing.
Keymaster can act as an SSH_ASKPASS provider, supplying SSH key passphrases
via TouchID instead of typing them. Every SSH connection triggers a TouchID
prompt (or reuses the cached session within the TTL window).
This means you don't need ssh-add or a running ssh-agent — SSH itself
calls keymaster directly each time it needs a passphrase.
# 1. Import an existing SSH key's passphrase
bin/keymaster-ssh import ~/.ssh/id_ed25519
# 2. Configure your shell (add output to ~/.zshrc)
bin/keymaster-ssh setup >> ~/.zshrc
source ~/.zshrc
# 3. SSH now uses TouchID for passphrases
ssh user@hostGenerate a new SSH key with a random passphrase, automatically stored in keymaster.
keymaster-ssh generate [-t type] [-b bits] [-C comment] [-f file]
| Flag | Default | Description |
|---|---|---|
-t |
ed25519 |
Key type (ed25519, rsa, ecdsa) |
-b |
(none) | Key size in bits (only applies to rsa) |
-C |
$USER@$(hostname -s) |
Key comment |
-f |
~/.ssh/id_<type> |
Output file path |
The passphrase is a random 32-character string generated from /dev/urandom.
You never need to see or type it — keymaster handles retrieval via TouchID.
The command refuses to overwrite an existing key file. If keymaster fails to store the passphrase after key generation, the passphrase is printed to stderr so you can store it manually.
# Generate an ed25519 key with defaults
keymaster-ssh generate
# Generate an RSA-4096 key for a specific purpose
keymaster-ssh generate -t rsa -b 4096 -f ~/.ssh/id_work -C "work@example.com"Store an existing key's passphrase in keymaster.
keymaster-ssh import <key_path>
The command prompts you to enter the passphrase, then verifies it by attempting
to extract the public key with ssh-keygen -y. If the key has no passphrase,
it exits with a message and stores nothing.
keymaster-ssh import ~/.ssh/id_ed25519
keymaster-ssh import ~/.ssh/id_rsaPrint shell configuration lines to stdout. Pipe or copy these into your
~/.zshrc or ~/.bashrc.
# Preview what will be added
keymaster-ssh setup
# Apply directly
keymaster-ssh setup >> ~/.zshrc
source ~/.zshrcThis sets two environment variables:
SSH_ASKPASS— points to thekeymaster-askpassscriptSSH_ASKPASS_REQUIRE=force— tells SSH to always use the askpass program, even when running in a terminal (without this, SSH only uses askpass when there is no TTY)
If you're using a custom prefix, it also includes the KEYMASTER_SSH_PREFIX
export.
Show which SSH keys in ~/.ssh/ have passphrases stored in keymaster.
keymaster-ssh listOutput:
KEY STORED
id_ed25519 yes
id_rsa no
Scans ~/.ssh/id_* (excluding .pub files). The first invocation may trigger
a TouchID prompt; subsequent checks within the TTL window are cached.
Remove a stored passphrase from keymaster. Takes the key basename (not the full path).
keymaster-ssh remove id_ed25519This only removes the passphrase from keymaster's keychain store. It does not delete or modify the SSH key file itself.
By default, passphrases are stored under the keychain key
ssh_key_passphrase:<key_basename> (e.g., ssh_key_passphrase:id_ed25519).
You can change the prefix to namespace keys differently:
# Via environment variable (affects both keymaster-ssh and keymaster-askpass)
export KEYMASTER_SSH_PREFIX="work_ssh"
# Resulting keychain key: work_ssh:id_ed25519
# Via flag (keymaster-ssh only, overrides the env var)
keymaster-ssh --prefix work_ssh import ~/.ssh/id_ed25519If you use a custom prefix, make sure the same KEYMASTER_SSH_PREFIX is set in
your shell profile so that keymaster-askpass uses it when SSH invokes it. The
setup command includes this export automatically when a custom prefix is
active.
SSH supports an SSH_ASKPASS environment variable pointing to a program that
returns the passphrase on stdout. The bin/keymaster-askpass script:
- Receives the SSH prompt as
$1(e.g.,"Enter passphrase for /path/to/key: ") - Checks
SSH_ASKPASS_PROMPT— only handles passphrase prompts (ignores confirmation and notification prompts) - Parses the key path from the prompt and extracts its basename
- Calls
keymaster get <prefix>:<basename>which triggers TouchID - Returns the passphrase to SSH on stdout
Both keymaster-askpass and keymaster-ssh resolve the keymaster binary by
first checking relative to the script's location (the project root), then
falling back to $PATH.
- Passphrases are stored in the macOS Keychain, protected by TouchID via keymaster. They are never written to disk in plaintext.
- The session file is stored in
$TMPDIR(per-user, mode 700 on macOS) and HMAC-signed to prevent forgery. The HMAC key is stored in the keychain and generated automatically on first use. - Passphrases are never exposed in process arguments.
keymaster setreads from stdin, andkeymaster-sshpasses passphrases tossh-keygenviaSSH_ASKPASSrather than command-line flags. - The
KEYMASTER_TTLsetting controls how often TouchID is required. Set it to0for maximum security (TouchID on every SSH connection).