KEYCHAIN(1)

πŸ’… KEYCHAIN(1) πŸ’…

okay bestie, buckle up β€” this is KEYCHAIN(1), straight out of the User Commands section of the manual, and it's about to change your whole terminal life. no cap.


🌸 NAME 🌸

keychain β€” Manager for ssh-agent and private keys, with native GnuPG support


✨ SYNOPSIS ✨

keychain [global-options] [action] [action-options] [keys...]

πŸ¦‹ DESCRIPTION πŸ¦‹

Keychain orchestrates ssh-agent and gives you one coordinated, long-running SSH agent per user and host. It's a frontend to ssh-agent and ssh-add that lets you keep one long-running ssh-agent process per user per host β€” instead of the default of one per login session β€” so you only need to enter each key's passphrase once per boot. literally once. iconic behavior πŸ’…

πŸŽ€ The Keychain difference πŸŽ€

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Without Keychain:                                          β”‚
β”‚                                                             β”‚
β”‚  Terminal 1 β†’ ssh-agent (prompt)                            β”‚
β”‚  Terminal 2 β†’ ssh-agent (prompt again)                      β”‚
β”‚  Terminal 3 β†’ ssh-agent (prompt again)                      β”‚
β”‚  Cron job β†’ no agent (fails)                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  With Keychain:                                             β”‚
β”‚                                                             β”‚
β”‚  Terminal 1 ─┐                                              β”‚
β”‚  Terminal 2 ─┼→ single ssh-agent (prompt once)              β”‚
β”‚  Terminal 3 ──    ↓                                         β”‚
β”‚  Cron job β”€β”€β”€β”˜    all terminals reconnect automatically     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This dramatically reduces the number of times you need to enter your passphrase. With keychain, you only need to enter a passphrase once every time your local machine is rebooted. πŸŽ‰

Keychain also makes it easy for remote cron jobs to securely "hook in" to a long running ssh-agent process, allowing your scripts to take advantage of key-based logins. fr fr, your cron jobs eat with this one πŸ”₯

🧚 GnuPG, but make it convenient 🧚

Keychain also supports the ability to auto-warm GnuPG signing and encryption keys so they are ready for use, and allows you to easily wipe cached GnuPG secrets using the keychain wipe --gpg command. 🫢

heads up bestie: GnuPG features require GnuPG 2.1 or later. no exceptions, the vibes demand it.

When a GPG key is requested, Keychain asks GnuPG to prove the requested signing or decryption capability so any required passphrase is cached in memory. While Keychain manages the lifecycle of ssh-agent for you, GnuPG retains complete ownership of gpg-agent startup, configuration, and lifecycle. like, keychain is not stepping on gpg-agent's turf β€” that's its main character era, we respect it πŸ‘‘

⭐ the multi-terminal glow-up ⭐

Keychain 3.0.0_beta2+ introduces coordinated multi-terminal initialization, eliminating the could not acquire lock errors that occurred when multiple shells started simultaneously. πŸ™ˆ

When Visual Studio Code reconnects to WSL and restores several terminals at once, all terminals now coordinate automatically β€” Enter in any one terminal to initialize keys, and all others are notified when complete. the way that just works now 😭

See keychain man topic:coordination. πŸ’–

🌈 INTRODUCTION 🌈

okay so before we get into the actual key-loading chaos, here's the whole vibe check for Keychain 3, bestie ✨

πŸ’– Keychain 3, Requirements and Backward Compatibility (topic:requirements) πŸ’–

Keychain supports most UNIX-like operating systems and intentionally limits its scope to modern, widely deployed implementations of OpenSSH and GnuPG. no dusty legacy nonsense, just the stuff people actually run πŸ’…

From shell script to Python zipapp. Keychain was conceived as a single POSIX Bourne shell script that grew steadily more sophisticated since its inception in 2002. In 2026, Keychain 3 moved to a new, portable technology: a single Python zipapp β€” a single executable <code>.pyz</code> file that runs on any system with Python 3.9 or newer, without requiring external Python dependencies. It's giving modern runtime while maintaining the self-contained, script-based nature of Keychain, periodt 🫢

Action-driven interface. Keychain 3 introduces a new action-driven interface (<code>keychain add id_rsa</code>, <code>keychain agent stop --mine</code>, <code>keychain wipe --ssh</code>, etc.) and is now the recommended way to drive Keychain. A full embedded documentation engine provides help via <code>--help &lt;action&gt;</code> or <code>--explain</code> for context-sensitive documentation. The complete man page is available via <code>keychain man</code>, with <code>keychain man --list</code> showing all available sections. documentation slay πŸ’–

Backward compatibility. Maintaining <code>keychain 2.x</code> CLI compatibility was a primary goal. The legacy flat-flag command line (for example <code>keychain --eval --quiet id_rsa</code>) continues to work exactly as before via an explicit compatibility shim. no cap, your old scripts are safe 🫑

Shell support. Keychain 3 continues to be compatible with Bourne sh, csh/tcsh and fish shells. bash, ksh and zsh are compatible with the Bourne sh built-in to Keychain. Keychain also supports integration with systemd. shells stay fed, bestie πŸ“


🌸 General Usage (topic:usage) 🌸

Typically, you configure Keychain to run from your shell startup files. Use the shell-specific recipes in <code>keychain man topic:env</code> so interactive terminals can load keys while non-interactive login shells establish the agent environment without prompting. To load a key from an interactive Bourne-compatible shell, run:

eval "$(keychain add id_rsa --eval)"

Keychain will start <code>ssh-agent</code> if one isn't already running, then checks to make sure your private keys (in this example, <code>id_rsa</code>) are loaded into the agent. If they are not, you are prompted for any passphrase necessary to decrypt them, so that they are cached in memory and available for use. lowkey iconic behavior πŸ’…

In addition to printing user-friendly output to your terminal, <code>keychain</code> outputs important <code>ssh-agent</code> environment variables. Command substitution captures this output, and the shell built-in <code>eval</code> evaluates it, causing these variables to persist in your current session. it's giving "the shell remembers so you don't have to" energy ✨

These <code>ssh-agent</code> environment variables are also written to <code>~/.keychain/${HOSTNAME}-sh</code>, so that subsequent logins and non-interactive shells such as cron jobs can source this file to access the running <code>ssh-agent</code> and make passwordless ssh connections using the cached private keys β€” even when you are logged out. These files are collectively called pidfiles. yes bestie, your cron jobs can vibe with your agent too 😭

Multi-terminal coordination (3.0.0_beta2+). When multiple shells start simultaneously, all terminals coordinate automatically. Each displays <code>Press Enter to run ssh-add in this terminal</code>. Pressing Enter in any one terminal runs <code>ssh-add</code>; other terminals wait and are notified when complete. This eliminates the <code>could not acquire lock</code> errors from earlier versions. See <code>keychain man topic:coordination</code>. no more lockfile scavenger hunt, we love to see it πŸŽ€

The key files specified on the command-line will be searched for in the <code>~/.ssh/</code> directory, and <code>keychain</code> will expect to find the private key file with the same name, as well as a <code>.pub</code> public key. Keychain will also see if any GnuPG keys are specified and, if so, ask GnuPG to exercise the requested operation so any required passphrase is prompted for and cached. bestie, it checks both sides of the key situation πŸ¦‹

Typically, private SSH key files are specified by filename only, without path, although it is possible to specify an absolute or relative path to the private key file as well. Private key files can be symlinks to the actual key. PKCS#11-backed SSH tokens can be requested explicitly via <code>pkcs11:/path/to/provider.so</code>; Keychain will compare the token's exported public keys against the current agent and, when needed, load the provider via <code>ssh-add -s</code>. fancy token girlies, this one's for you πŸ’œ

For GPG keys, Keychain exercises the requested capability so <code>gpg-agent</code> has the required passphrase cached and ready for use. Use <code>gpgs:KEYID</code> for signing-oriented workflows, <code>gpge:KEYID</code> for decrypt-oriented workflows such as <code>pass</code>, and <code>gpga:KEYID</code> when both capabilities should be warmed. The <code>gpge:</code> prefix performs an explicit encrypt-then-decrypt verification cycle β€” a tiny temporary payload is encrypted to the key and immediately decrypted through <code>gpg-agent</code>. If this verification cannot be completed, <code>add</code> fails rather than reporting success. This is more reliable than relying on signing warm-up side effects, which may not populate the decryption passphrase cache on all GnuPG versions and key configurations. The legacy <code>gpgk:KEYID</code> alias is equivalent to <code>gpgs:KEYID</code> (signing warm-up only). gpg bestie said "verify, don't guess" πŸ”₯


πŸ¦‹ Coordinated key initialization (3.0.0_beta2+) (topic:coordination) πŸ¦‹

The problem. When multiple terminal shells start simultaneously, each shell may run <code>keychain add --eval</code> to initialize SSH keys. In Keychain <code>2.x</code> and <code>3.0.0_beta1</code> and earlier, all terminals would race to acquire a single lock. One terminal would win and prompt for the passphrase; all others would wait for the lock timeout (default 5 seconds) and then fail with <code>could not acquire lock</code>. Users had to manually delete the lockfile or retry. it was giving "everyone fighting over one bathroom stall" energy and nobody was having a good time πŸ’€

This was especially problematic in Visual Studio Code: only one terminal is visible at a time, so the terminal running <code>ssh-add</code> might be hidden behind other terminal tabs. Users had to click through each terminal tab to find the one prompting for the passphrase, turning what should be a simple workflow into a frustrating scavenger hunt. not the scavenger hunt, bestie 😭

The solution (3.0.0_beta2+). Keychain now uses a coordinated unlock protocol designed specifically for multi-terminal scenarios:

  1. State lock (short-lived). A brief lock protects agent setup and coordination updates, including waiter registration. Agent queries and passphrase entry run outside this lock, so a slow or unanswered prompt does not block those updates. short lock, no drama 🫢
  2. Loading and clearing. Each loading attempt has its own lock. Loading attempts share the activation lock, while SSH clearing requires exclusive access. This permits independent immediate prompts without allowing <code>wipe</code> to clear the agent during loading. boundaries, bestie πŸ’…
  3. FIFO notification. Waiting terminals create private named pipes (FIFOs) and sleep until keyboard input, a notification, or the end of a loading operation wakes them. A loading terminal also listens for cancellation and other terminals' results; it checks the agent before canceling its own redundant prompt. they're literally passing notes in class, but secure πŸŽ€
  4. Moving the request. Pressing Enter in a waiting terminal asks existing loaders to cancel their requests. Keychain waits for those loading children to exit before starting a replacement and displays a short notice. The canceled terminals wait for the replacement operation. okay so we're passing the aux cord, but for passphrases 🎧

User experience. All terminals that detect missing SSH keys display:

β–Έ Press Enter to run ssh-add in this terminal

Pressing Enter in any one terminal starts <code>ssh-add</code>. Other terminals show:

β–Έ Press Enter to move the passphrase request to this terminal

When initialization completes, waiting terminals wake automatically and print:

Keys initialized by another terminal.

that "wake automatically" moment? highkey satisfying 🌈

Automatic activation. <code>--immediate</code> skips Keychain's initial Enter prompt. Several terminals can run <code>ssh-add</code> at the same time, but you only need to complete a prompt that loads your requested keys. Other invocations check the actual agent after completion notifications and cancel their own redundant prompts. A success message for another terminal's different key request is not enough: each invocation checks its own keys. Failed attempts do not cancel useful prompts elsewhere. The normal Enter prompt remains the default so users can choose where passphrase entry begins. main character energy stays with the user, as it should πŸ‘‘

Implementation details. The latest completion record is stored in <code>~/.keychain/&lt;host&gt;.state.json</code>; it is not proof that an operation is alive or that keys are loaded. Per-attempt locks, lifetime and cancellation FIFOs, and notification FIFOs live in <code>~/.keychain/&lt;host&gt;-waiters/</code>. The shared loading/exclusive clearing lock is <code>~/.keychain/&lt;host&gt;.activation.lock</code>. Runtime files are private to the current user and are checked before use. security bestie checked the vibes and the permissions βœ…

Scope. The coordinated unlock flow applies to SSH key loading only. GPG keys use explicit warm-up paths (<code>gpgs:</code>, <code>gpge:</code>, <code>gpga:</code>) and do not participate in multi-terminal coordination. SSH only, no crossover episode πŸ¦‹

Legacy behavior. Users upgrading from Keychain <code>2.x</code> or <code>3.0.0_beta1</code> will notice the absence of <code>Waiting N seconds for lock...</code> messages during interactive key loading. This is intentional: internal coordination is quiet, and the state lock is held only briefly for metadata updates. so yeah, the silence is a feature, not a bug πŸ’œ

✨ GLOBAL OPTIONS ✨

okay bestie, these are the global flags β€” the main-character options that apply no matter which action you're doing. all the deets, no cap πŸ’…

  • --help, -h β€” prints the top-level help cheat sheet, or action-specific help when it's followed by an action path. -h is literally the alias 🫢
  • --version, -V β€” prints the version banner and exits. -V is the alias, periodt.
  • --explain β€” this global one tells keychain to not actually act on the command line. instead it evaluates the command-line arguments (minus the --explain option) and prints documentation boxes for the action, every recognised option and positional argument to standard output. essentially, it tells Keychain to explain the command invocation it would have performed to you by printing all relevant documentation snippets. it's giving "read me the recipe before we cook" energy ✨
  • --allow-env, -E β€” by default, Keychain silently ignores all KEYCHAIN_* environment variables to prevent accidental or malicious injection in multi-user environments. pass -E or --allow-env to allow $KEYCHAIN_CONFIG to override the config file path and $KEYCHAIN_SSH_AGENT_ARGS to inject extra flags into a spawned ssh-agent.

this flag is intended for trusted, single-user environments or CI pipelines where the shell environment is controlled. it is not needed for normal interactive use; all KEYCHAIN_* behavior can be achieved via ~/.keychainrc configuration. so like… don't just leave this on for funsies, bestie πŸ”₯

  • --quiet, -q β€” silences routine Keychain messages and successful ssh-add reports. required interactive prompts, explicit results from actions such as list, warnings, and errors are still printed. she's quiet, not gone πŸ’€
  • --debug, -D β€” emits extra details about the agent-discovery decisions keychain makes: which pidfile candidates were considered, which sockets were probed, why one was selected, what ssh -G returned for each host: extkey, etc. detective-mode keychain πŸ”
  • --debug-log β€” use --debug-log FILE to collect Keychain diagnostics during shell or graphical-login startup without redirecting terminal output. the file receives diagnostic details even without --debug, including routine messages hidden by --quiet. normal console output and the shell assignments produced by --eval are unchanged.

records are plain text and include a UTC timestamp, process ID, and terminal name (none when unavailable). existing records are preserved. the parent directory must already exist; the log must be a regular file owned by you, not a symlink or hard link. Keychain creates the file with private permissions and restricts an existing file to owner read/write access.

eval "$(keychain add --eval --debug-log ~/.keychain/debug.log id_ed25519)"

for shell startup, add --debug-log FILE to both branches of the Bash or fish recipe in keychain man topic:env, retaining --noask in the non-interactive branch.

the log records Keychain's decisions, not terminal input, passphrases, or private-key contents. it does not capture prompts that ssh-add writes directly to its terminal. review paths and configuration details before sharing a log in a bug report. remove the option when troubleshooting is complete; Keychain does not rotate the log. do not forget the cleanup step, okay? 🫢

  • --nocolor, --no-color β€” disables ANSI color output. useful when piping to a tool that doesn't strip escape codes, or in log capture where color bytes muddy the file. the conventional $NO_COLOR environment variable (https://no-color.org) is also honored; the flag wins when both are set. --no-color is an accepted alias. bye-bye rainbow, hello clean bytes πŸŒˆβž‘οΈπŸ“„
  • --theme β€” selects the color / glyph theme. legacy is the keychain 2.x output style; modern is the 3.0 default. a once-forever lifestyle preference; usually set in ~/.keychainrc. choose your aesthetic and commit, bestie πŸŽ€
  • --no-gui, --nogui β€” don't use GUI passphrase prompts (DISPLAY / SSH_ASKPASS). force a TTY prompt instead. a once-forever lifestyle preference for headless or screen-shared environments. no pop-up jumpscares, just terminal honesty πŸ’…
  • --absolute β€” treats --dir (or [paths] dir) as the literal pidfile directory; don't append /.keychain. literal queen behavior πŸ‘‘
  • --dir β€” overrides the per-user state directory. by default keychain uses ~/.keychain/ for pidfiles, env-files, and lock files. --absolute treats the value verbatim instead of appending /.keychain.
  • --host, --hostname β€” --host HOSTNAME (or the more explicit --hostname HOSTNAME) overrides the hostname used in pidfile names. pidfiles are named HOST-sh / HOST-csh / HOST-fish etc. so multiple hosts sharing a homedir over NFS each get their own state. by default keychain probes hostname(1), $HOSTNAME, then uname -n. identity crisis? avoided πŸ¦‹

πŸ’… ACTIONS πŸ’…

Okay bestie, this is the main character section β€” every action Keychain can do, one by one. Buckle up. πŸŽ€

🌟 keychain add

Syntax:

keychain add [OPTIONS] [KEYS...]

add is the workhorse of Keychain 3 β€” no cap, it carries the whole team. Back in Keychain 2.x, this was the implied action when you didn't specify any other action. It tells Keychain to first make sure an ssh-agent is currently running (which is literally equivalent to an automatic keychain agent start). If there isn't one? Boom, one gets started. Then, for each KEY argument:

  • For each SSH key: do nothing if ssh-agent already holds it; otherwise, prompt for its passphrase and load it. ✨
  • For each GPG key: ask GnuPG to prove the requested signing or decryption capability. GnuPG owns any gpg-agent and pinentry interaction β€” that's its lane, not Keychain's.
  • Write the SSH agent's environment to a per-user, per-host pidfile under ~/.keychain/ that scripts can source to gain access to the agent.

Coordinated multi-terminal initialization (3.0.0_beta2+). When multiple shells start simultaneously, Keychain 3.0.0_beta2+ uses a coordinated unlock protocol instead of the classic lock-timeout approach. It's giving teamwork, honestly:

  1. All terminals that detect missing SSH keys display: Press Enter to run ssh-add in this terminal.
  2. Pressing Enter in any terminal designates it to run ssh-add.
  3. Other terminals wait automatically and are notified when initialization completes.
  4. When notified, waiting terminals print Keys initialized by another terminal. and configure their environment without prompting.

Moving a passphrase request. If the terminal that started ssh-add becomes inaccessible, press Enter at Keychain's waiting prompt in your visible terminal. Keychain cancels the existing request before prompting there and prints a short notice. The original terminal waits for the replacement operation to finish. 🫢

Transitioning from classic lock behavior. In earlier versions, simultaneous terminal startups would race for a lock, with losers timing out after 5 seconds (or the --lockwait value). The new coordination model is fundamentally different: terminals cooperate rather than compete, and all terminals succeed together once any one completes the passphrase entry. Iconic behavior, honestly.

Bare key names are looked up under ~/.ssh/ and as GPG key IDs; absolute paths are loaded directly. Symbolic links are supported. Extended-key syntax β€” sshk:PATH, gpgk:KEYID, gpgs:KEYID, gpge:KEYID, gpga:KEYID, pkcs11:PATH, host:HOSTNAME β€” is automatically detected and can be used to explicitly specify the lookup method.

When one or more KEY arguments are supplied and none of them resolve to a real SSH key or known GPG identity, add exits before starting or attaching to an ssh-agent. This is an intentional compatibility accommodation: keychain 2.x bare-token invocation is still accepted, but a mistyped modern action should not accidentally spawn a fresh SSH agent as a side effect. (We love a safety net. πŸ’…) GnuPG may perform its normal gpg-agent autostart while resolving a bare token against local GPG key material. A token unknown to the local GPG keyring does not normally trigger agent autostart.

Note: The coordinated unlock flow applies to SSH key loading only. Due to differences in how GPG operates, GPG keys use Keychain's explicit GnuPG warm-up paths (gpgs:, gpge:, gpga:) and do not participate in the multi-terminal coordination protocol. GPG said "not me participating in your group project," and honestly valid. 😭

See also: topic:extkeys, topic:coordination.


🌟 keychain agent

Syntax:

keychain agent <start|stop> [OPTIONS]

Domain command for managing agent processes without loading keys. agent start ensures an agent exists and can emit its environment; agent stop terminates agents recorded in the pidfile.

keychain agent start

Syntax:

keychain agent start [OPTIONS]

Start an ssh-agent if none is running, then optionally emit its environment for the current shell. By default, Keychain pins a spawned ssh-agent to a host-specific socket path under ~/.keychain/ so the agent endpoint stays stable across WSL and other early startup environments that may clean /tmp. This action does not load any keys; use add when you want prompt-and-load semantics.

keychain agent stop

Syntax:

keychain agent stop [--mine|--others]

Terminate the ssh-agents recorded in the pidfile. Bare agent stop targets every recorded agent; --mine and --others narrow the kill list by owner UID.


🌟 keychain env

Syntax:

keychain env [--shell FORMAT] [--json]

Print SSH_AUTH_SOCK and SSH_AGENT_PID from the best available source (running pidfile, then inherited env) in the requested format. Output targets, bestie:

  • env β€” bare KEY=value lines (default)
  • sh β€” Bourne shell KEY=value; export KEY
  • csh β€” C-shell setenv KEY value
  • fish β€” set -x KEY value
  • json β€” JSON object on one line
  • eval β€” same as add --eval: shell-detected dialect
  • systemd β€” bare KEY=value (suitable for EnvironmentFile=)

--target is an alias for --shell; --json is shorthand for --shell json.


🌟 keychain forget

Syntax:

keychain forget [KEYS...]

For each KEY argument, evict the matching SSH key from ssh-agent. Bare key names are looked up under ~/.ssh/; absolute paths are used directly. Extended-key syntax supports sshk:PATH and host:HOSTNAME. gpgk:KEYID and pkcs11:PATH are not supported by forget. Sorry bestie, them's the rules. πŸ™ˆ

Without arguments, forget is a no-op (use wipe to remove all keys).


🌟 keychain help

Syntax:

keychain help [<action>]

Print the top-level cheat sheet. With an action name, print the per-action cheat sheet (same as keychain <action> --help). Identical to keychain --help.

For full documentation, use keychain man, keychain man --list for a man index, and keychain man <doc-atom> for specific documentation on an action, option or topic. To display all documentation relevant to your current command-line, append --explain.


🌟 keychain inspect

Syntax:

keychain inspect [KEYS...] [--json]

Run every state probe keychain would run during a real add (host detection, paths, perms, env-file, agent reachability, key list, extkey expansion, ...) and present the internal runtime in easy-to-read topical boxes. Runtime identity, the parsed ~/.keychainrc, effective preferences and their sources, and the presence of Keychain-relevant environment variables are included. Environment reporting is restricted to a diagnostic allowlist; unrelated variables and literal configuration-file contents are never exposed. Use with --json to generate a complete machine-readable state dump suitable for bug reports. The versioned JSON schema groups diagnostics under runtime, configuration, keychain_state, agent_state, and keys. Perfect for when you need to spill the tea to a maintainer. πŸ“


🌟 keychain list

Syntax:

keychain list [--json]

Print the type, bit-length, fingerprint, and comment of every key held by the reachable ssh-agent, in the same shape as ssh-add -l. When the modern color theme is active, the output is rendered as a clean four-column table. Aesthetic and functional, we love to see it. ✨

--json emits a JSON array suitable for scripting.


🌟 keychain man

Syntax:

keychain man [<action>|<topic>] [--list] [--no-pager] [--width N]

Print the full embedded documentation in a pager-friendly form. Targets:

  • keychain man β€” whole Keychain manual
  • keychain man <action> β€” one action (incl. all flags)
  • keychain man <topic> β€” named topical section
  • keychain man topic:<name> β€” same, with explicit prefix
  • keychain man --list β€” index every doc atom
  • keychain man --no-pager β€” skip $PAGER even on TTY
  • keychain man --width 100 β€” explicit wrap width

On a terminal, Keychain uses $PAGER, then less, then more. Color is retained only when the selected executable is verified as less, including aliases such as macOS more; when $LESS is unset or empty, Keychain supplies LESS=-R to that child process without changing the caller's environment. Other pagers receive plain text. No glow-up for them, sadly. πŸ’€


🌟 keychain version

Syntax:

keychain version [--json]

Print the version string, copyright, and license summary.


🌟 keychain wipe

Syntax:

keychain wipe [--ssh] [--gpg]

Clear cached credentials while leaving the agent processes running. By default, wipe removes identities from ssh-agent only (this behavior is new as of version 3.0.1 β€” previous versions wiped GnuPG keys as well). GnuPG is affected only when --gpg is specified. Keychain then connects to an already-running gpg-agent and sends RELOADAGENT. GnuPG flushes the agent's entire in-memory secret cache, not just passphrases warmed by Keychain. This makes protected signing and decryption capabilities cold again and may clear other cache modes maintained by the same agent. No key files or other persistent key material are deleted.

  • wipe β€” SSH identities only
  • wipe --ssh β€” SSH identities only
  • wipe --gpg β€” gpg-agent's in-memory secret cache only
  • wipe --ssh --gpg β€” both

As part of RELOADAGENT, GnuPG also rereads its agent configuration and trust list. Clearing an absent gpg-agent is a successful no-op. If gpg-connect-agent is unavailable, times out, or cannot confirm the cache flush, Keychain reports an error. 🫑


⚑ ACTION OPTIONS ⚑

--eval

Syntax:

keychain add --eval [KEYS...]

This is a foundational keychain feature β€” like, genuinely foundational, bestie. If you're using Keychain on a POSIX system, you'll likely want to use this flag in your shell startup files (e.g. ~/.bash_profile) to ensure that every new shell session can find and use ssh-agent without manual intervention.

Here's how it works. When you pass --eval, Keychain prints shell commands that set the environment variables needed by ssh and scp to find the running ssh-agent. Your shell's built-in eval executes those commands in the current shell. Keychain emits the appropriate environment syntax for the selected shell, but the surrounding command substitution must also use that shell's syntax. From an interactive Bash or Zsh terminal, run:

eval "$(keychain add --eval id_ed25519)"

The shell runs Keychain before eval processes its output, so --eval does not prevent a key-loading prompt. For automatic startup, use the guarded recipes in keychain man topic:env.

If you just want to retrieve the agent environment without adding any keys, use the keychain env --target eval action instead, or source the appropriate pidfile. iykyk πŸ’…

--systemd

After loading keys, push SSH_AUTH_SOCK and SSH_AGENT_PID into the calling user's systemd user environment block using systemctl --user set-environment. This makes the agent environment available to all child systemd units (services, timers) without requiring them to source a pidfile.

Use this option when your user session is managed by systemd and you need background services to access the SSH agent. From an interactive Bash or Zsh terminal, run:

eval "$(keychain add --eval --systemd id_ed25519)"

Systemd services getting the agent env without lifting a finger? That's main character energy for your background processes. πŸ‘‘

For automatic startup, add --systemd to both Keychain invocations in the Bash or fish recipe in keychain man topic:env. Keep --noask in the non-interactive branch so publishing the agent environment does not pop a prompt for keys. 🫢

Real talk: --systemd is a side-effecting operation that runs systemctl directly β€” it does not print any shell code. If all you want is to update the systemd environment without loading keys, use keychain env --target systemd instead; that one prints the agent environment in systemd EnvironmentFile= format (bare KEY=value lines). ✨

  • --quick, -Q β€” if a reachable ssh-agent already holds at least one key, just leave. Exit immediately, no prompting, no loading anything new. It's built for shell-startup snippets where the agent gets reused across a million shells, so the shell only pays the cost of a single ssh-add -l on subsequent invocations. πŸ’… Heads up: this is intentionally an SSH-only compatibility option. GPG key arguments get ignored and Keychain does not invoke GnuPG at all when --quick is present. For GPG workflows, use normal keychain add. 🐚
  • --immediate β€” when requested SSH keys are missing, start ssh-add without displaying Press Enter to run ssh-add in this terminal. Immediate invocations can prompt independently, so a hidden terminal's passphrase request doesn't block a visible terminal from loading keys. And after another terminal finishes loading, each invocation checks its own requested keys and cancels its unfinished ssh-add if those keys are now available. Coordination, but make it fast. πŸ”₯ This option does not suppress an ssh-add passphrase prompt, hide errors, disable coordination, or imply --quiet. For quiet automatic activation, add --quiet --immediate to the interactive branch of your startup recipe (see keychain man topic:env). The corresponding command in an interactive Bash or Zsh terminal is:
eval "$(keychain add --eval --quiet --immediate id_ed25519)"

A failed invocation does not cancel other attempts β€” terminals requesting different keys can keep going independently. Pressing Enter in a regular waiting terminal moves outstanding requests there; canceled invocations wait for that operation instead of immediately starting another prompt. Keychain reports the confirmation and lifetime settings used by a successful load, but it does not reconcile differing settings: if the same identity gets added again, OpenSSH applies that addition's settings. To make automatic activation persistent:

[agent]
activation = immediate

The --immediate flag overrides the configured activation mode for this invocation. See keychain man config:agent.activation for package defaults and the legacy immediate config key. πŸŽ€

  • --no-passphrase, --noask β€” complete SSH-agent setup and any requested environment output, then exit without inspecting, loading, or warming the specified keys. Keychain does not invoke GnuPG in this mode. Which means the command can succeed even when a requested key isn't available β€” this option does not verify key availability. πŸ™ˆ For a modern agent-only invocation, prefer keychain agent start. The --noask spelling and its --no-passphrase alias are here to preserve the traditional Keychain 2.x behavior. πŸ’œ
  • --confirm β€” passes -c to ssh-add: every subsequent use of the loaded key (say, by ssh, scp, or Git over SSH) requires explicit user confirmation through OpenSSH's askpass mechanism. This is normally a persistent preference:
[agent]
confirm = true

On macOS, --confirm implicitly applies --no-inherit: Keychain does not adopt the $SSH_AUTH_SOCK / $SSH_AGENT_PID inherited from the parent shell. This ensures that when no valid Keychain pidfile agent exists, Keychain starts an agent with the native confirmation environment described below. Other platforms retain normal inherited-agent discovery. πŸ“

macOS. When Keychain starts a new managed ssh-agent with confirmation enabled, it installs a private helper at ~/.keychain/ssh-askpass-macos (or the selected Keychain directory) and starts the agent with the environment OpenSSH needs to invoke it. The helper accepts confirmation requests only and displays a native graphical Keychain SSH Confirmation dialog. Only choosing Allow approves the operation; choosing Deny, closing or cancelling the dialog, an unavailable desktop session, or any helper failure refuses it. No vibes-based approval here, bestie. πŸ’…

If $SSH_ASKPASS already names a custom helper when Keychain starts the managed agent, Keychain preserves it and enables OpenSSH's forced askpass mode. On other platforms, provide a suitable askpass helper in the agent's startup environment when the platform doesn't supply one.

An agent's askpass environment is fixed when the agent starts, and a valid pidfile agent is considered before inherited-agent discovery. After upgrading, run keychain agent stop once if Keychain's pidfile refers to an agent created without confirmation support; the next confirmed invocation will start it with the native helper. See issue #227. πŸ›

--confirm cannot be combined with --no-gui. Keychain reports an error instead of silently weakening the request or loading a key whose confirmation constraint can't be satisfied. Safety first, always. πŸ‘‘
  • --timeout β€” passes a timeout straight through to ssh-add: the key is forgotten by the agent after the requested number of minutes. Common settings include 480 (8h, a workday) or 60 (higher security). Also a common config-file preference. ⏰
  • --ignore-missing β€” for each KEY positional that doesn't resolve to an existing file (or known GPG ID), skip it silently instead of erroring. Perfect for dotfiles shared across machines where not every key exists everywhere. And if all requested keys are missing, add still exits successfully without spawning an agent. No drama. πŸ¦‹
  • --clear β€” remove every identity from the selected ssh-agent, then process the requested keys normally. Equivalent to calling:
keychain wipe && keychain add KEYS

Important: --clear does not flush GnuPG's passphrase cache, even when GPG keys are also requested. Use keychain wipe --gpg when that separate operation is what you actually mean. 🧹

  • --confallhosts β€” iterate every Host block in ~/.ssh/config (excluding pure-wildcard patterns and negations) and load its IdentityFile. Equivalent to --extended host:NAME for every host. Per-host expansion is delegated to ssh -G NAME, so include files, Match blocks, and %d/%u/%h tokens work exactly as OpenSSH does. iykyk. 🌈
  • --ssh-allow-forwarded β€” adopt the inherited $SSH_AUTH_SOCK if it points to a forwarded agent (typical of ssh -A from a workstation into a server). Heads up, this has security implications: the forwarded socket is controllable by the source machine. See keychain man topic:security. ⚠️
  • --no-inherit, --noinherit β€” don't trust $SSH_AUTH_SOCK / $SSH_AGENT_PID from the parent environment; always start fresh from the pidfile or by spawning. The inverse of --ssh-allow-forwarded. 🫑
  • --ssh-agent-socket β€” use PATH as the ssh-agent socket explicitly. When this option isn't supplied, Keychain now chooses a host-specific managed socket path under ~/.keychain/ instead of letting ssh-agent pick a transient location under /tmp. Override it only when integrating with another tool that expects a specific socket path. πŸ”Œ
  • --lockwait β€” maximum time (in seconds) to wait for keychain's state lock when another keychain process is mutating shared coordination state. Default 5 seconds. ⏳ Historical note, gather round: in Keychain 2.x and 3.0.0_beta1, --lockwait controlled the timeout for the single global lock that protected the entire key-loading operation. That was the source of the could not acquire lock errors when multiple terminals started simultaneously. Starting in 3.0.0_beta2, the lock scope is much narrower (protecting only small coordination state updates) and the interactive unlock flow uses a separate activation lock with FIFO-based notification. So --lockwait is now an internal tuning parameter that most users will never need to adjust. Set it to 0 to fail immediately if the state lock is held (rare in normal operation). πŸ’€
  • --mine β€” restrict the kill list to agents whose owner UID matches the calling user. Useful in shared accounts or root-cron contexts where you must not touch other users' agents. 🫢
  • --others β€” inverse of --mine: terminate only agents whose owner UID does not match the calling user. Rare; typically a maintenance tool. 🧚
  • --all β€” terminate every candidate agent recorded in the pidfile, regardless of owner. This is the default target for keychain agent stop. πŸ’…
  • --shell, --target β€” specify the output format for keychain env. Supported targets:
    • env β€” bare KEY=value lines (default, suitable for EnvironmentFile=)
    • sh β€” Bourne shell KEY=value; export KEY
    • csh β€” C-shell setenv KEY value
    • fish β€” fish shell set -x KEY value
    • json β€” JSON object on one line
    • eval β€” shell-detected dialect (same as add --eval)
    • systemd β€” bare KEY=value (alias for env, suitable for systemd)

Use --target systemd or --shell systemd when you want to redirect output to a file for systemd's EnvironmentFile= directive. The --systemd flag on add performs the same function but pushes directly to systemd via systemctl --user set-environment instead of printing to stdout. ✨

  • --json β€” equivalent to --shell json. πŸŽ€
  • --json β€” emit the inspection payload as JSON. (yes, again β€” she contains multitudes) πŸ’–
  • --json β€” emit a JSON array of key records on stdout. πŸ”₯
  • --list β€” print slug -- one-liner for every action, global flag, config key, and topic. Useful as a discovery surface. 🌸
  • --no-pager β€” skip the automatic $PAGER invocation even when stdout is a terminal. 🚫
  • --width β€” explicit wrap width. Defaults to $COLUMNS then 80. πŸ“
  • --roff β€” write the manual as UTF-8 roff man(7) source, for installation as keychain.1. Never paged, and --width does not apply. $SOURCE_DATE_EPOCH pins the header date.
keychain man --roff > keychain.1
  • --json β€” emit {"version": "X.Y.Z"}. 🏷️
  • --ssh β€” remove all identities held by ssh-agent using ssh-add -D. This does not delete the corresponding private key files. πŸ—‘οΈ
  • --gpg β€” send RELOADAGENT to an already-running gpg-agent. GnuPG flushes the agent's entire in-memory secret cache, not only entries warmed by Keychain, and rereads its configuration. Protected signing and decryption operations will need to unlock their keys again. No private keys or other persistent key material are deleted, and the agent process remains running. πŸ”

πŸ“š REFERENCE TOPICS

πŸͺŸ Windows-specific notes (topic:windows)

Keychain runs on Windows Subsystem for Linux (WSL) and Git Bash on Windows via keychain.pyz plus the Python launcher (py.exe). What you get:

  • The full action surface (add, agent start, list, inspect, env, ...). The embedded documentation system (--help, man, --explain) works identically to the Linux build. πŸ’…
  • Coordinated multi-terminal initialization (3.0.0_beta2+) works on WSL and Git Bash 🫢. When several terminals start at the exact same moment (super common when Visual Studio Code reconnects to WSL), they coordinate with each other automatically so you don't get those could not acquire lock errors. iconic behavior honestly.
  • WSL needs the standard Linux OpenSSH tools (ssh-agent, ssh-add) installed inside your WSL distribution, no cap.
  • Git Bash lives in the MinGW environment and may need Git for Windows' SSH tools.

πŸ” How keychain discovers, attaches, and spawns ssh-agent (topic:agents)

Discovery order. When add or agent start runs, keychain goes probing for an existing agent, and it checks in this order:

  1. The pidfile in $KEYCHAIN_DIR/HOST-pid β€” its writer left SSH_AUTH_SOCK and SSH_AGENT_PID sitting in there. Both socket presence on disk and reachability via ssh-add -l get verified, bestie. No vibes-based trust here.
  2. Inherited $SSH_AUTH_SOCK / $SSH_AGENT_PID from the parent shell's environment β€” unless --no-inherit is set. Forwarded agents (ssh -A) are only accepted when --ssh-allow-forwarded is set. And an inherited gpg-agent SSH socket? Deliberately ignored. not invited to the party πŸ’….
  3. None of the above β€” so a brand new ssh-agent gets spawned ✨. By default its socket is pinned to ~/.keychain/HOST-agent.sock instead of some chaotic ephemeral /tmp/ssh-* directory.

Pidfile is the source of truth. Per-host (so NFS-shared homes actually work) and per-user (mode 700). Older pidfiles get replaced atomically β€” there are no in-place edits, ever. The default managed ssh-agent socket lives right beside these files as HOST-agent.sock.

Coordinated key loading. A short-lived state lock protects metadata updates. Per-attempt locks identify live loading operations; the activation lock allows concurrent loading but excludes SSH clearing. Waiting terminals get notified through FIFO endpoints and then verify the requested keys in the agent. See keychain man topic:coordination for the whole saga.

Agent responsibilities. Keychain owns ssh-agent discovery, startup, and coordination for SSH keys, periodt. For native GPG signing and decryption, Keychain invokes gpg and leaves gpg-agent startup, configuration, pinentry, and lifecycle entirely to GnuPG (this is new as of 3.0.1). It deliberately does not use GnuPG's SSH-agent emulation, because adding an SSH key there imports the private key into GnuPG's persistent key store β€” and that behavior differs from the in-memory model you expect from ssh-agent. gross, we don't want that πŸ™ˆ.

🌈 Environment emission and shell integration (topic:env)

Keychain sets SSH_AUTH_SOCK and SSH_AGENT_PID so ssh, scp, and all their little friends can find the running agent. The --eval flag emits environment assignments for your shell to actually execute. Interactive terminals load your keys; non-interactive login shells establish the agent environment without prompting. That distinction is the whole point β€” it lets graphical-session startup finish even when it's running a login shell whose terminal you can't even reach 😭.

Choose the recipe for your shell and replace the filenames in KEYCHAIN_KEYS with your keys. Keep one key per line, and quote filenames that contain spaces. The key list is a shell variable and does not need to be exported. Keep your unrelated existing config, and replace any older Keychain startup invocation rather than adding a second one πŸŽ€.

Bash startup. Add this to ~/.bash_profile, keeping any existing configuration:

KEYCHAIN_KEYS=(
    ~/.ssh/id_ed25519
    ~/.ssh/id_rsa
)

if [[ $- == *i* ]]; then
    eval "$(keychain add --eval "${KEYCHAIN_KEYS[@]}")"
else
    eval "$(keychain add --eval --noask)"
fi

Bash reads .bash_profile for login shells, including non-interactive ones. The i in $- identifies an interactive shell; --noask skips key loading in the other branch πŸ¦‹.

Zsh startup. Add this to ~/.zshrc, which is read only by interactive shells:

KEYCHAIN_KEYS=(
    ~/.ssh/id_ed25519
    ~/.ssh/id_rsa
)
eval "$(keychain add --eval "${KEYCHAIN_KEYS[@]}")"

If your graphical session also needs the agent environment, add eval "$(keychain add --eval --noask)" to ~/.zprofile. That login-shell file can run non-interactively; key loading belongs in .zshrc ✨.

Fish startup. Add this to ~/.config/fish/config.fish, keeping any existing configuration:

set KEYCHAIN_KEYS \
    ~/.ssh/id_ed25519 \
    ~/.ssh/id_rsa

if status is-interactive
    eval "$(keychain add --eval $KEYCHAIN_KEYS)"
else if status is-login
    eval "$(keychain add --eval --noask)"
end

Fish reads config.fish for every shell. This recipe loads keys only in interactive shells, uses --noask for non-interactive login shells, and skips Keychain in other non-interactive shells. This avoids the inaccessible startup prompt that was reported with SDDM graphical login πŸ’€.

Bash alternative: load keys from .bashrc. Use this arrangement if you want both interactive login shells and interactive non-login terminals to run Keychain. In ~/.bash_profile, source .bashrc for interactive logins and establish the agent environment for non-interactive logins:

if [[ $- == *i* ]]; then
    if [[ -f ~/.bashrc ]]; then
        source ~/.bashrc
    fi
else
    eval "$(keychain add --eval --noask)"
fi

In ~/.bashrc, keep the key list and interactive key loading together:

KEYCHAIN_KEYS=(
    ~/.ssh/id_ed25519
    ~/.ssh/id_rsa
)

if [[ $- == *i* ]]; then
    eval "$(keychain add --eval "${KEYCHAIN_KEYS[@]}")"
fi

Place the .bashrc block after any existing early return for non-interactive shells. If .bash_profile already sources .bashrc, use the conditional source above in place of that existing source command. Keychain then runs once per interactive shell, while non-interactive logins still receive the agent environment without loading keys πŸ‘‘.

Alternate Option 1: Source the per-host pidfile directly. If you'd rather not invoke Keychain in every shell, just source the pidfile that Keychain writes after the first run. For Bourne-style shells, run . ~/.keychain/$(hostname)-sh; for fish, run source ~/.keychain/$(hostname)-fish. This is faster because it avoids a Python invocation β€” but it requires that Keychain has already run at least once to create the pidfile, so like, no shortcuts before the first run bestie πŸ“.

Alternate Option 2: Push to systemd user environment. If your session uses systemd, you can push the agent environment to all child units via keychain add --systemd id_rsa. This runs systemctl --user set-environment to set SSH_AUTH_SOCK and SSH_AGENT_PID for all systemd services and timers. To update the systemd environment without loading keys, use keychain env --target systemd and redirect to an environment file 🧚.

Alternate Option 3: Emit bare KEY=value for tools. For non-shell tools that consume EnvironmentFile= or similar formats, use keychain env > /run/user/$UID/keychain.env to generate a bare definition file to be sourced by another tool. The default env target outputs bare KEY=value lines suitable for systemd's EnvironmentFile= directive or other configuration parsers πŸ”₯.

πŸŽ€ Extended key syntax (topic:extkeys)

SSH and GPG keys can be specified as positional arguments for the add action. keychain add NAME will look for ~/.ssh/NAME if it exists and treat it as an SSH key; otherwise it's looked up via gpg --list-secret-keys, and if found, the GnuPG signing key gets loaded and warmed. This may also warm an associated encryption key, but that's not guaranteed β€” see below, bestie.

In addition to explicitly passing keys by name or path, positional key arguments may accept seven extended key prefixes to avoid ambiguity and give you more fine-grained control. The following prefixed formats can be mixed freely with regular positional key names:

sshk:PATH       an SSH key file path
sshk:NAME       an SSH key name under `~/.ssh/`
gpgk:KEYID      a GnuPG key ID (legacy alias for `gpgs:`)
gpgs:KEYID      warm and prove signing capability using GnuPG's signing path
gpge:KEYID      warm and prove encryption/decryption capability
gpga:KEYID      warm and prove signing + encryption/decryption capability
pkcs11:PATH     a PKCS#11 provider library loaded via `ssh-add -s`
host:HOSTNAME   every IdentityFile for HOSTNAME
                (resolved via `ssh -G HOSTNAME`)

Using the extended key syntax opens up several important features to help make sure your specified GnuPG signing or encryption key is actually loaded and ready to go. This matters because GnuPG's keys and subkeys are tricky little things β€” OpenPGP encryption capability may live on the primary key or on a separate encryption subkey, and GnuPG's passphrase cache behavior can vary by version, platform, and key layout. A signing operation may happen to unlock later decryption on some systems, but Keychain does not rely on that as a portable guarantee 🫢.

Extended key syntax also covers PKCS#11-backed SSH tokens. pkcs11:PATH asks OpenSSH to enumerate public keys from the provider with ssh-keygen -D and compare their fingerprints with the identities already held by the running agent. If the token's identities aren't already present, Keychain loads the provider with ssh-add -s PATH so the usual PIN or touch prompt can happen in the foreground ✨.

For GPG keys, gpgk:KEYID is equivalent to gpgs:KEYID. Both perform a signing-oriented warm-up: Keychain asks GnuPG to perform a signing operation with the requested key, which causes gpg-agent to cache the passphrase needed for signing. This proves signing capability, but it does not prove that the same key can decrypt data. important distinction, don't skip it πŸ’….

Using gpge:KEYID performs a similar explicit check to ensure functionality. When used, Keychain will encrypt a tiny temporary payload with the requested key and then decrypt it to ensure it's properly loaded and functional β€” and it will fail if it doesn't succeed. If your workflow decrypts data (for example, pass show ...), use gpge:KEYID πŸ¦‹.

If your workflow needs both signing and decryption, use gpga:KEYID. Keychain will first warm the signing path, then require the same explicit decryption check performed by gpge:. If GnuPG cannot encrypt to and decrypt with the requested key, gpga:KEYID fails rather than treating the signing warm-up as enough. no half-vibes here πŸ”₯.

host:HOSTNAME can be used to refer to a specific hostname defined in your local OpenSSH config. host: expansion is delegated to ssh -G, so the full OpenSSH semantics (Match, Include, %d/%u/%h substitution, quoted args) apply properly πŸ‘‘.

πŸ’œ Pidfile location and contents (topic:pidfile)

Per user, per host, mode 700 directory:

~/.keychain/

(overridable by --dir / [paths] dir).

  • HOST-sh β€” Bourne-style KEY=value; export KEY
  • HOST-csh β€” C-shell setenv KEY value
  • HOST-fish β€” fish set -x KEY value
  • HOST-envfile β€” bare KEY=value export for non-shell tools
  • HOST-agent.sock β€” managed default ssh-agent socket path

Every single one of these files is owner-only (mode 600) and gets re-written atomically, so no half-written mess, no cap. 🫢

One pidfile per host is the whole point, bestie: homedirs shared over NFS across many machines each get their own agent state β€” which means no ssh-agent confusion when you log in from multiple hosts. It's giving organized. πŸ’…

Coordination state (3.0.0_beta2+). For coordinated multi-terminal initialization:

  • HOST.state.json β€” latest attempt identifier and recorded outcome
  • HOST.state.lock β€” protects state file and waiter registration
  • HOST.activation.lock β€” shared for loading, exclusive for clearing
  • HOST-waiters/ β€” per-attempt locks and private FIFO endpoints

The coordination state file is a result record, not a list of live processes or registered waiters. Held per-attempt locks identify live operations, and each terminal's notification FIFO is its registration. Coordination files are created with mode 0o600 inside the private keychain directory. Normal cleanup removes the attempt's temporary resources; files left by a terminated process are cleaned up after its lock is no longer held. The way that works is honestly kind of iconic. ✨


🌸 CONFIG KEYS (~/.keychainrc) 🌸

πŸŽ€ ~/.keychainrc preference file (topic:config) πŸŽ€

Keychain 3 introduces an optional configuration file, ~/.keychainrc, for setting persistent defaults. The config file lets you customize Keychain's behavior across all invocations. Keychain reads ~/.keychainrc at startup. The file uses INI-style syntax with [section] headers and simple key/value pairs:

[agent]
timeout = 1h

[lock]
lock = true

[agent.env]
ssh_args = -a ~/.ssh/agent.sock

πŸ’« Evaluation order πŸ’«

Keychain evaluates settings in this order, with successive steps overriding previous steps, okay so pay attention:

  1. Built-in defaults provide the base settings, including any package-selected activation default.
  2. If --allow-env is specified, $KEYCHAIN_CONFIG can point to an alternate .keychainrc path.
  3. The selected .keychainrc file is parsed.
  4. If --allow-env is specified, supported KEYCHAIN_* variables from the environment are applied.
  5. Explicit command-line flags are applied last.

This means command-line flags always override environment and config file settings, giving you flexibility to customize individual invocations while maintaining sensible defaults. Last one wins, periodt. πŸ‘‘

πŸ” Environment variables πŸ”

Keychain does not support arbitrary KEYCHAIN_* environment variables. The supported environment surface is intentionally small:

  • KEYCHAIN_CONFIG β€” alternate path to the config file (instead of ~/.keychainrc)
  • KEYCHAIN_SSH_AGENT_ARGS β€” extra arguments for spawned ssh-agent

By default, Keychain ignores these variables for security reasons. To enable them, pass --allow-env (or -E) on the command line. This prevents untrusted shell environments from silently altering Keychain's behavior β€” which, honestly, thank you. Normal interactive users should prefer ~/.keychainrc for persistent settings. πŸ™ˆ

🧚 Config key correspondence 🧚

Most config keys mirror command-line options. For example, --timeout 1h on the command line is equivalent to timeout = 1h in the [agent] section. When a config key corresponds to a CLI option, the documentation references that option for behavioral details.

Some boolean config keys use positive phrasing even when the historical CLI flag is negative for compatibility. For instance, [lock] lock = false produces the same behavior as --no-lock. No cap, that's just how it is. πŸ’€

  • agent.activation β€” choose prompted or immediate SSH key activation
  • agent.clear β€” wipe SSH identities before adding
  • agent.confirm β€” ssh-add -c: prompt per use
  • agent.inherit β€” ignore inherited SSH_AUTH_SOCK / PID
  • agent.ssh_agent_socket β€” use PATH as the ssh-agent endpoint
  • agent.ssh_allow_forwarded β€” adopt a forwarded SSH agent
  • agent.systemd β€” push agent env to systemctl --user
  • agent.timeout β€” auto-expire keys after MIN minutes
  • agent.env.ssh_args β€” extra args passed to spawned ssh-agent
  • keys.confallhosts β€” load every IdentityFile in ~/.ssh/config
  • keys.ignore_missing β€” skip keys that don't exist
  • output.animate β€” animate the initial activation prompt
  • output.color β€” disable ANSI color output
  • output.gui β€” don't use GUI passphrase prompts
  • output.quiet β€” suppress non-error output on stderr
  • output.theme β€” color theme: legacy or modern
  • paths.absolute β€” treat --dir as the literal pidfile dir
  • paths.dir β€” pidfile directory (default: ~/.keychain)
  • paths.pid_formats β€” pidfile output formats

🌈 Detailed config entries 🌈

⭐ agent.activation ⭐

Config key: [agent] activation

--immediate selects activation = immediate for this invocation.

choose prompted or immediate SSH key activation

Set [agent] activation = prompt to wait for Enter before attempting coordinated SSH key initialization, or activation = immediate to begin automatically. This controls the initial activation step, not passphrase entry: ssh-add may still prompt, and immediate terminals may wait for another terminal to finish initialization. iykyk 🫢

Upstream builds default to prompt. A package may be built with immediate as its default; both behaviors remain available. Your ~/.keychainrc overrides the package default, and --immediate overrides the config for one invocation. To restore the Enter prompt on a package that defaults to immediate activation:

[agent]
activation = prompt

The legacy [agent] immediate = true and immediate = false settings remain accepted and mean activation = immediate and activation = prompt, respectively. If both keys are present, activation wins regardless of their order, and Keychain warns that immediate was ignored. Invalid activation values are errors. See keychain man --immediate for coordination and failure behavior.

πŸ¦‹ agent.env.ssh_args πŸ¦‹

Config key: [agent.env] ssh_args

extra args passed to spawned ssh-agent

Append extra arguments when keychain spawns ssh-agent. This is an advanced escape hatch for cases Keychain does not model directly, such as pinning the agent socket to a specific path or enabling agent-level debugging. The value is split into shell-like tokens and appended after Keychain's own managed flags. For example, this pins the ssh-agent socket to a fixed location:

[agent.env]
ssh_args = -a ~/.ssh/agent.sock

If $KEYCHAIN_SSH_AGENT_ARGS is set in the environment and --allow-env is active, it overrides this config key. πŸ’…

πŸŽ€ output.animate πŸŽ€

Config key: [output] animate

animate the initial activation prompt

The default is true. On a suitable color terminal, the complete activation prompt appears immediately, then changes from subdued text to its final colors from left to right over about half a second. This happens once; it does not repeat while Keychain waits. Enter works throughout, and the animation does not delay coordination events. Obsessed with her, honestly. ✨

Set animate = false under [output] in ~/.keychainrc for a static prompt with the same final colors. There is no corresponding command-line option. Disabled color, redirected output, an unsuitable terminal, or insufficient or unknown terminal width also produce a static prompt. Required prompts remain visible with --quiet.

πŸ’œ paths.pid_formats πŸ’œ

Config key: [paths] pid_formats

pidfile output formats

Comma-separated pidfile formats to write after agent setup. The default is sh; supported values are sh, csh, fish and envfile.

πŸ“ Advanced config settings πŸ“

A few settings exist only in the config file, with no command-line equivalent. These include:

  • pid_formats in [paths] β€” comma-separated pidfile formats to write after agent setup.
  • ssh_args in [agent.env] β€” extra arguments passed when spawning ssh-agent. This is injected into Keychain's internal environment as $KEYCHAIN_SSH_AGENT_ARGS, which the agent startup code reads. If this environment variable is set in the outer environment and --allow-env is active, it takes precedence over the config file value.
  • Advanced tuning parameters for coordination and locking behavior

🧡 Value formats 🧡

Boolean values accept: true, false, yes, no, on, off, 1, 0. Integers are decimal. Strings are passed verbatim (quote values containing spaces, though unquoted usually works). Fr fr, quote them if you're being fancy. 🌸

πŸ”₯ Editing and discovery πŸ”₯

Edit ~/.keychainrc directly with your preferred text editor. Use keychain man --list to see all available config keys, or keychain man config:SECTION.KEY for detailed documentation on a specific key. The config key list is generated from Keychain's option metadata, so it stays synchronized with the implementation. Main character metadata behavior, no notes. πŸ‘‘

πŸŽ€ APPENDIX πŸŽ€

🧚 keychain 2.x compatibility (topic:compat) 🧚

Okay bestie, this is the part where old Keychain energy gets translated into new Keychain energy β€” every single keychain 2.x flat-flag invocation is translated to the new-style action verbs transparently. No drama, just vibes. ✨

Old long-form spellings of options that have been renamed β€” --noask, --noinherit, --nogui, --nolock, --no-color β€” all remain accepted. No deprecation is planned for the legacy flag spellings. πŸ’…

Old options that are now hidden but still accepted for compatibility: --extended, --gpg2, --absolute, --clear, --theme, --ssh-allow-forwarded, --no-inherit, --ssh-agent-socket, --confirm. These are now lifestyle preferences set in ~/.keychainrc; the CLI flags work but no longer appear in --help. --clear follows Keychain 3's SSH-only default wipe policy; clearing GnuPG remains an explicit wipe --gpg operation. 🫢

Deprecated no-ops retained for command-line compatibility: --ssh-allow-gpg and --ssh-spawn-gpg. Each emits a warning and is ignored; Keychain uses ssh-agent exclusively for SSH keys. 😭

Removed with migration guidance: --confhost; use host:HOSTNAME instead.

Other deprecated no-ops retained with a warning: --agents, --inherit, and --attempts.


Coordination model change (3.0.0_beta2+). The coordinated unlock protocol introduced in 3.0.0_beta2 fundamentally changes how multiple terminals initialize keys simultaneously. In Keychain 2.x and 3.0.0_beta1, terminals raced for a single lock with losers timing out after 5 seconds. In 3.0.0_beta2+, terminals cooperate via FIFO notification and all succeed together. Legacy invocations still work, but the behavior is now cooperative rather than competitive. Users will notice the absence of Waiting N seconds for lock... messages during interactive key loading. It's giving β€œwe're all in this together” instead of β€œfight to the death for the lock.” πŸ’€βœ¨

Compatibility is therefore intentionally high but not perfect. In particular, bare tokens still default to add for 2.x compatibility, but if none of the requested keys resolve, keychain refuses to start an agent and exits with an error instead of preserving the old side effect. That's the fine print, bestie β€” read it and weep, or read it and slay. πŸ’–


πŸ›‘οΈ Security model (topic:security) πŸ›‘οΈ

Keychain runs with the authority of the invoking user. It assumes the operating system, the current user account, the user's executable search path, OpenSSH, and GnuPG are trusted. Within that boundary, Keychain protects its runtime state from other local users, validates agent endpoints before use, coordinates concurrent invocations, and quotes generated shell output. 🫑

Runtime storage. The default ~/.keychain/ directory is created mode 0700. Before runtime state is used on POSIX systems, Keychain checks the directory and existing pidfiles, state files, lock files, and waiter directory. It refuses paths owned by another user, paths with any group or other permissions, and paths that cannot be inspected. Pidfiles and coordination state are replaced through private temporary files. πŸ”’

Agent endpoints. An SSH endpoint must be a non-symlink UNIX-domain socket owned by the current user. Pidfile agents must also name a live process, and the selected endpoint is checked again immediately before ssh-add runs. Forwarded agents require the explicit --ssh-allow-forwarded opt-in. A socket identified as GnuPG's SSH endpoint is always rejected; Keychain uses ssh-agent exclusively for SSH identities. 🧚

Configuration and environment. On POSIX systems, .keychainrc must be owned by the current user and must not be writable by group or others. KEYCHAIN_* environment variables are ignored unless --allow-env (-E) is supplied. External programs receive argument vectors rather than shell command strings. ✨

Shell output. Sourceable environment values are quoted for the selected shell. NUL and line-break characters are rejected rather than emitted into a pidfile or --eval output. πŸ’…

--ssh-allow-forwarded. Adopting a forwarded agent socket on a remote host means the source machine β€” the laptop you forwarded from β€” can sign authentication requests on this host's behalf. That is by design (it's the point of agent forwarding) but it widens the trust boundary. Don't enable on hosts you don't fully control. Seriously. This is not the moment for main character energy. 😭

Coordination security. The coordinated unlock protocol uses private FIFO endpoints for notification and operating-system advisory locks for ownership. Each attempt's held lock establishes that it is active; PID, hostname, heartbeat, and saved file contents do not establish ownership. The shared activation lock prevents SSH clearing during loading. A FIFO wakeup only signals a possible state change, so terminals check the agent before reporting that another attempt supplied their keys. --no-lock deliberately disables these guarantees. Iconic for debugging, lowkey terrifying as a permanent lifestyle. πŸ’€

What Keychain does not protect against. Root, the current user, or a process already able to act as that user can reach or replace user-owned resources. A malicious program selected through PATH, an intentionally unsafe command-line or preference-file setting, and a compromised agent are also outside the trust boundary. Any process that can reach an unlocked agent socket may ask it to sign; use agent timeouts, confirmation constraints, screen locking, and careful forwarding policy as appropriate. No cap β€” the socket is the VIP section, and you gotta check the guest list. πŸŽ€


And that's the whole manual, bestie. This page is from keychain 3.0.8, dated 2026-10-23, and it's the KEYCHAIN(1) page. Now go forth, keep your agent cozy, and don't let randos touch your sockets. πŸ’–βœ¨πŸ”

πŸ“¦ where this page comes from