fssh keeps your SSH private keys encrypted and acts as your SSH agent. Instead of plaintext key files or passphrases, every signature is unlocked with Touch ID (or your macOS login password), or with a password plus an authenticator code (OTP mode).
- No plaintext keys on disk — keys are encrypted with AES-256-GCM;
fssh keygencreates new keys that never touch the disk unencrypted - Know who is asking — the unlock prompt shows which program wants which key for which server, and warns about forwarded (
ssh -A) requests - The right key for each host —
fssh bind github.com workmakes a host use exactly one key - Interactive shell — browse, search and connect to the hosts in
~/.ssh/config, with Tab completion - Self-check —
fssh doctorfinds setup problems and tells you how to fix them
┌─────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ ssh / git │ ──▶ │ fssh agent │ ──▶ │ remote server│
└─────────────┘ │ Touch ID / OTP prompt│ └──────────────┘
│ decrypts one key │
└──────────────────────┘
Interactive shell:
fssh supports macOS on Apple Silicon and Intel.
Homebrew (builds from source):
brew tap Mister-leo/fssh
brew install fsshInstall script (release binary; verifies the checksum and removes the download quarantine):
curl -fsSL https://raw.githubusercontent.com/Mister-leo/fssh/main/contrib/install.sh | shFrom source (Go 1.21+, Xcode command line tools):
go build -o fssh ./cmd/fssh
sudo install -m 755 fssh /usr/local/bin/fsshThe release binary is not signed by Apple. The install script and Homebrew avoid Gatekeeper; for a manual download run xattr -d com.apple.quarantine fssh first.
fssh init # setup wizard
fssh doctor # check everything is connected
ssh my-server # Touch ID / OTP prompt appearsThe wizard walks through:
- Authentication — Touch ID (recommended; Macs without a usable sensor get the login-password prompt instead) or OTP
- Install fssh to
/usr/local/bin(skipped for Homebrew installs) - Import keys from
~/.ssh, then archive the plaintext originals to~/.ssh/fssh-archive/(or delete / keep them) - Auto-start the agent at login (LaunchAgent)
- Start the agent
- Configure ssh — a marked block at the top of
~/.ssh/configmakes every host use the fssh agent - Shell environment (optional) — exports
SSH_AUTH_SOCKforssh-addand tools that ignore~/.ssh/config
Running fssh init again offers to keep the setup, switch the auth mode (keeping all keys), or start over.
Use ssh, git, scp as usual. After you authenticate, the agent caches the unlock for unlock_ttl_seconds (default 600), so a burst of connections — or Ansible against 50 hosts — needs only one prompt. Set it to 0 to authenticate every signature.
The prompt says who is asking, e.g. "git → ssh wants to use key "work" to log in to github.com". Requests arriving through agent forwarding (ssh -A) are flagged and always ask, ignoring the cache; set "forwarded_sign": "deny" to refuse them outright. fssh log lists recent signature requests.
Without a binding, ssh offers every key in the agent to every server: you may push with the wrong GitHub account, and servers reject you after ~6 keys ("Too many authentication failures"). Bind hosts to keys:
fssh bind github.com work # adds IdentityFile ~/.fssh/pubkeys/work.pub + IdentitiesOnly yes
fssh bind gitlab.com personal
fssh list # shows which hosts use which key
fssh unbind gitlab.comfssh keygen deploy # ed25519; --type ecdsa|rsa, --bits 4096
fssh pubkey deploy --copy # public key to the clipboardRun fssh (or fssh shell):
fssh> list # all hosts (follows Include, merges aliases)
fssh> list -r # with resolved IPs (proxy fake-ip addresses are hidden)
fssh> search prod
fssh> web1 # connect by name, alias, number, hostname or IP
fssh> connect me@10.0.0.9 -p 2222 # anything ssh accepts
fssh> show web1 # details, including the bound fssh key
fssh> add / edit web1 / delete web1
fssh> global show # the Host * block
edit keeps every option it does not manage (LocalForward, SetEnv, comments, extra aliases). Hosts defined in Included files are shown but not edited.
| Command | Description |
|---|---|
fssh init |
Setup wizard (--mode touchid|otp for non-interactive, --force to start over) |
fssh import --alias A --file F |
Import a private key; asks for its passphrase; --original archive|delete|keep |
fssh keygen A |
Generate a key directly into encrypted storage |
fssh list [--json] |
List keys with type, fingerprint and bound hosts |
fssh pubkey A [--copy] |
Print a public key |
fssh bind HOST A / fssh unbind HOST |
Pin a host to one key |
fssh export --alias A --out F |
Export in OpenSSH format (--ask-passphrase to encrypt; --format pkcs8 for unencrypted PKCS#8) |
fssh remove --alias A |
Delete a key (asks for confirmation; --yes in scripts) |
fssh backup [--out F] / fssh restore F |
Encrypted backup of all keys, restorable on another Mac or auth mode |
fssh rekey |
Rotate the master key (keys are re-encrypted atomically) |
fssh mode switch touchid|otp |
Change the auth mode, keeping every key |
fssh status |
Overview: mode, keys, agent, auto-start, ssh config |
fssh doctor |
Find problems and print the fix |
fssh agent |
Run the agent in the foreground |
fssh log [-n N] |
Recent signature requests (who, which key, which host, result) |
fssh otp recover|change-password|regen-recovery|show-secret |
OTP maintenance |
fssh shell |
Interactive host manager (default when run without arguments) |
fssh uninstall [--purge] |
Remove fssh from ssh config, auto-start and shell files |
fssh help [command], fssh version |
Help and version |
~/.fssh/config.json (all fields optional):
{
"socket": "~/.fssh/agent.sock",
"require_touch_id_per_sign": true,
"unlock_ttl_seconds": 600,
"forwarded_sign": "prompt",
"language": "",
"log_level": "info",
"log_format": "plain"
}| Field | Default | Meaning |
|---|---|---|
require_touch_id_per_sign |
true |
false = convenience mode: decrypt all keys when the agent starts and never prompt again |
unlock_ttl_seconds |
600 |
How long one unlock is reused; 0 = authenticate every signature |
forwarded_sign |
prompt |
Requests via ssh -A: prompt (always ask) or deny |
language |
system | zh or en; also FSSH_LANG, then LANG, then the macOS language |
A random 32-byte master key lives in the macOS Keychain; fssh shows the Touch ID prompt before using it. Macs without a usable sensor (no Touch ID, no enrolled finger, lid closed) get the macOS login-password prompt instead.
For people who prefer a password plus authenticator codes:
fssh init --mode otpYou choose a password (12+ characters), add the secret to an authenticator app (QR code shown if qrencode is installed), and confirm with a code before anything is saved. You also get 10 single-use recovery codes.
- Forgot the password:
fssh otp recover(uses one recovery code) - New phone:
fssh otp show-secret - Change password / new recovery codes:
fssh otp change-password,fssh otp regen-recovery - When the agent runs in the background, password and code are asked in macOS dialogs.
- A code is accepted only once; wait for the next one if you just used it.
Security boundary: the OTP secret is stored on this Mac, encrypted with your password. Someone who has both your password and a copy of ~/.fssh can derive the master key without the authenticator. The code check stops someone who only knows the password from using fssh on this Mac, but it is not a hardware second factor. Use FileVault.
fssh mode switch otp # or touchidEvery key is re-encrypted for the new mode and verified before the switch is committed; on OTP you confirm a code first.
| ssh-add | fssh agent |
|---|---|
ssh-add -l |
lists imported keys |
ssh-add -x / -X |
locks / unlocks the agent (nothing is listed or signed while locked) |
ssh-add -D |
prints "All identities removed" but keeps the keys (they live in fssh's encrypted store); it forgets the cached unlock, so the next signature asks again |
ssh-add <file> |
refused: use fssh import |
fssh backup # ~/fssh-backup-<time>.fsshbak, protected by a backup passphrase
fssh restore fssh-backup-....fsshbak # on the new Mac, after fssh init
fssh uninstall # keeps ~/.fssh; --purge deletes keys and the master key- OTP users: older versions showed the TOTP secret in a format authenticator apps cannot use. Run
fssh otp show-secretand add the secret again. Old recovery codes cannot recover anything; runfssh otp regen-recovery. - OTP users: older versions also stored the master key in the Keychain, which bypassed OTP. It is removed automatically after your next OTP unlock.
- After every upgrade macOS may ask whether fssh may use the Keychain; click "Always Allow".
- Run
fssh doctor: it also creates the public key filesfssh bindneeds for keys imported earlier.
Start with fssh doctor. Common cases:
- ssh does not ask for Touch ID / OTP — the agent is not running, or ssh is not configured to use it.
fssh doctorprints the exact fix. - ssh asks for a key file passphrase — the agent refused (locked, cancelled, not running) and ssh fell back to a plaintext key in
~/.ssh. Archive imported originals:fssh import ... --original archive, or followfssh doctor. - "Too many authentication failures" — too many keys offered; bind the host:
fssh bind <host> <alias>. - "incorrect signature type" / "no mutual signature supported" — old RSA server;
fssh sshd-align --host <server>enables RSA-SHA2 on it. - OTP code rejected — check the phone clock; a code works only once.
- Agent logs —
~/Library/Logs/fssh/agent.log.
- Keys are encrypted with AES-256-GCM using per-key HKDF-SHA256 keys; the fingerprint is authenticated data.
- Touch ID mode: the master key is in the Keychain; the Touch ID check is enforced by fssh (an app-level check, not a Keychain access-control flag).
- Convenience mode keeps decrypted keys in agent memory until it stops.
- Agent forwarding exposes your keys to the remote host while connected; fssh asks for every forwarded signature.
- Enable FileVault.
go build ./cmd/fssh
go test -race ./...Tests that touch the Keychain are opt-in (FSSH_KEYCHAIN_TESTS=1) and use an isolated item name (FSSH_KEYCHAIN_SERVICE). Do not point HOME at a temporary directory while calling the Keychain: macOS looks for the login keychain under $HOME and shows a "Keychain Not Found" dialog.
This project was developed with assistance from TRAE AI software.

