This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
fssh is a macOS SSH private key manager that provides Touch ID or OTP (One-Time Password) authentication for SSH connections. It acts as an SSH agent that securely stores and manages encrypted private keys, eliminating the need to enter passphrases repeatedly while maintaining security.
Core Purpose: Replace plaintext SSH keys with encrypted storage, unlock via biometric (Touch ID) or TOTP, and provide an interactive shell for managing SSH hosts.
# Build the binary
go build ./cmd/fssh
# Install to /usr/local/bin
go build -o /usr/local/bin/fssh ./cmd/fssh
# Run tests (if any)
go test ./...
# Initialize master key (Touch ID mode - default)
./fssh init
# Initialize with OTP mode (for devices without Touch ID)
./fssh init --mode otp --seed-unlock-ttl 3600
# Import SSH private key
./fssh import -alias mykey -file ~/.ssh/id_rsa --ask-passphrase
# List imported keys
./fssh list
# Start SSH agent (secure mode, Touch ID/OTP per signature)
./fssh agent --unlock-ttl-seconds 600
# Start SSH agent (convenience mode, decrypt all on startup)
./fssh agent --require-touch-id-per-sign=false
# Export private key
./fssh export -alias mykey -out /path/to/key.pem --ask-passphrase
# Interactive shell for SSH connections
./fssh shell
# or simply
./fssh
# Remove a key
./fssh remove -alias mykey
# Rotate master key and re-encrypt all keys
./fssh rekey
# Check system status
./fssh status
# Generate SSH config entries
./fssh config-gen
# Align sshd config for RSA-SHA2 support
./fssh sshd-alignThe project supports two authentication modes via a unified AuthProvider interface:
-
Touch ID Mode (default for macOS with Touch ID):
- Master key stored in macOS Keychain
- Biometric unlock via
internal/macos/touchid_darwin.go - Hardware-backed security
-
OTP Mode (for devices without Touch ID):
- Master key derived from password-encrypted OTP seed
- TOTP verification (RFC 6238) via authenticator apps
- Seed cached with TTL, supports recovery codes
- Configuration stored in
~/.fssh/otp/config.enc
Mode selection is stored in ~/.fssh/auth_mode.json and determined by internal/auth/auth.go:GetAuthProvider().
Master Key Flow:
User Auth (Touch ID/OTP) → Master Key → HKDF per-file key → AES-256-GCM → Encrypted Private Key
- Master Key: 32-byte key from Keychain (Touch ID) or password-derived (OTP)
- Per-file Keys: Derived using HKDF with unique salt per private key
- Encryption: AES-256-GCM with unique nonce, fingerprint as AAD
- Storage: JSON files in
~/.fssh/keys/*.enc
See internal/crypt/crypt.go for crypto primitives and internal/store/store.go for key storage.
Two Operating Modes:
-
Secure Mode (
require_touch_id_per_sign: true):- Implemented in
internal/agent/secure_agent.go - Decrypts keys on-demand per SSH signature
- Optional TTL cache for master key unlock
- Every signature triggers
AuthProvider.UnlockMasterKey()
- Implemented in
-
Convenience Mode (
require_touch_id_per_sign: false):- Uses standard
golang.org/x/crypto/ssh/agent.Keyring - Decrypts all keys once at agent startup
- Keys kept in memory until agent stops
- Uses standard
The agent implements the SSH agent protocol via golang.org/x/crypto/ssh/agent.Agent interface, supporting:
- RSA-SHA2-256/512 signatures (via
SignWithFlags) - Public key listing
- Agent extensions
cmd/fssh/shell.go provides an interactive prompt powered by github.com/peterh/liner:
- Parses
~/.ssh/configviainternal/sshconfig/sshconfig.go - Tab completion for host names, IPs, and numeric IDs
- Commands:
list,search <term>,connect <host>,help,exit - Non-command input defaults to SSH connection
User Configuration (~/.fssh/config.json):
{
"socket": "~/.fssh/agent.sock",
"require_touch_id_per_sign": true,
"unlock_ttl_seconds": 600,
"log_level": "info",
"log_format": "plain"
}Loaded by internal/config/config.go, used as defaults for CLI flags.
Auto-start: contrib/com.fssh.agent.plist for macOS LaunchAgent.
internal/auth/auth.go-AuthProviderinterface and mode selectioninternal/auth/touchid.go- Touch ID implementationinternal/auth/otp.go- OTP implementationinternal/otp/totp.go- TOTP generation/verificationinternal/otp/config.go- OTP seed encryption/storageinternal/otp/recovery.go- Recovery code generation/validation
internal/crypt/crypt.go- HKDF, AES-GCM encryption/decryptioninternal/store/store.go- Private key record storage and retrievalinternal/keychain/keychain.go- macOS Keychain integration
internal/agent/server.go- Agent server startup and mode dispatchinternal/agent/secure_agent.go- On-demand decryption agent
cmd/fssh/main.go- Command dispatch and flag parsingcmd/fssh/shell.go- Interactive shellcmd/fssh/otp_init.go- OTP initialization logiccmd/fssh/config_gen.go- SSH config generatorcmd/fssh/align.go- sshd configuration alignment
internal/sshconfig/sshconfig.go-~/.ssh/configparserinternal/log/log.go- Structured logginginternal/macos/touchid_darwin.go- macOS biometry CGo bindings
- All keys converted to PKCS#8 DER internally
- Supports RSA, ECDSA, Ed25519
- Import: Handles encrypted/unencrypted PEM via
ssh.ParseRawPrivateKey() - Export: PKCS#8 PEM with optional AES-256 encryption
~/.fssh/
├── agent.sock # SSH agent socket
├── config.json # User configuration
├── auth_mode.json # Authentication mode (touchid/otp)
├── keys/ # Encrypted private keys
│ ├── mykey.enc # Per-key encrypted file
│ └── server.enc
└── otp/ # OTP mode only
├── config.enc # Encrypted OTP seed + config
└── recovery_codes.enc # Encrypted recovery codes
- Password must be ≥12 characters
- TOTP uses standard algorithms (SHA1/SHA256/SHA512), 6 or 8 digits
- OTP seed encrypted with password-derived key (scrypt or similar)
- Seed cached in memory with configurable TTL
- Master key cached per signature with separate TTL
- 10 single-use recovery codes for password reset
- OTP Mode: Two-tier caching
seed-unlock-ttl: How long OTP seed stays in memory (password entry frequency)unlock-ttl-seconds: How long master key stays cached (TOTP entry frequency)
- Touch ID Mode: Single-tier caching
unlock-ttl-seconds: How long Touch ID unlock is cached
The agent advertises RSA-SHA2-256/512 support via:
SignWithFlags()implementation insecure_agent.goExtension("ext-info-c")returning signature flags bitmask- Uses
ssh.AlgorithmSignerinterface for algorithm selection
- Add case to
cmd/fssh/main.go:main()switch - Implement
cmdYourCommand()function inmain.goor separate file - Parse flags with
flag.NewFlagSet() - Call appropriate internal packages
- Both Touch ID and OTP implement
AuthProviderinterface UnlockMasterKey()is the primary entry point- Cache management happens inside provider implementations
- Test mode switching by modifying
~/.fssh/auth_mode.json
// Load and decrypt
mk, _ := keychain.LoadMasterKey() // or provider.UnlockMasterKey()
rec, _ := store.LoadDecryptedRecord(alias, mk)
// rec.PKCS8DER contains raw private key
// Parse and use
priv, _ := x509.ParsePKCS8PrivateKey(rec.PKCS8DER)
signer, _ := ssh.NewSignerFromKey(priv)Use structured logging via internal/log/log.go:
log.Info("message", map[string]interface{}{"key": "value"})
log.Debug("detail", fields)
log.Warn("warning", fields)
log.Error("error", fields)# Terminal 1: Start agent
./fssh agent --unlock-ttl-seconds 600
# Terminal 2: Set socket and test
export SSH_AUTH_SOCK=~/.fssh/agent.sock
ssh-add -l # List keys from agent
ssh user@host # Test actual connection- Never log master keys, seeds, passwords, or recovery codes
- Always use
0600permissions for encrypted files - Touch ID prompts use macOS LAContext with biometry policy
- OTP mode should recommend FileVault full-disk encryption
- Convenience mode keeps keys in memory - document security tradeoff
- Recovery codes are single-use and should be marked as used after consumption
README.md- English user guideREADME_CN.md- Chinese user guidedocs/OTP-QUICKSTART.md- OTP mode quick startdocs/otp-authentication.md- OTP design documentdocs/otp-sdd.md- OTP software design documentdocs/otp-implementation.md- OTP implementation details