0993a4d3bb
Four things that each made keyman quietly less useful than it looked. **Clipboard.** `pbcopy` was spawned unconditionally, with a comment admitting it. Copy is now a list of commands per platform — pbcopy, clip, and wl-copy / xclip / xsel tried in order on everything else, because there is no single answer under Linux and trying them beats detecting the session type. Only an absent tool advances to the next candidate: one that ran and refused has an opinion. And if nothing is installed the key is printed, since "give me this public key" is answerable without a clipboard and used to be a dead end everywhere but macOS. Verified the round trip through real pbcopy/pbpaste. **Home directories.** `/home/<user>` was hardcoded — wrong on the platform this was written on. A named user is now looked for beside the current user's home first, which is right wherever homes live together whatever that directory is called, then in /home and /Users, and the failure names every path tried instead of feeding a nonexistent one to readdir. For the current user, `HOME` still wins, with `os.userInfo()` behind it: `process.env.HOME || ''` made an unset HOME fatal, which it is not in a cron job or a container. **Keys that are not named id_*.** A key called `deploy_ed25519` was absent from every menu with nothing said. It still is — the vault stores `<name minus id_>/id_<name>.age` and decrypt rebuilds the filename from the directory, so relaxing discovery means changing the on-disk layout, which the plan sizes as its largest single item and is not folded in here. What it does do is say so: any file whose first line carries a private key header and whose name lacks the prefix is now reported, per directory, with the reason. A bounded 64-byte read, because classifying a key is no reason to load one. **Plaintext hygiene.** A "Clear decrypted keys" entry, defaulting to no and listing what it would delete first, and a vault `.gitignore` written on first run covering the age identity and the tmp directory — which the README asked the user to do by hand. Never overwritten, and silent about a configured directory that sits outside the vault, since a .gitignore cannot speak for a path above itself and pretending otherwise reads as protection that is absent.
Keyman - SSH Key Management with Age Encryption
Keyman is a simple command line tool built around the age encryption tool. It allows you to manage SSH keys in public GitHub repositories securely by encrypting the private keys.
Features
- 🔐 Encrypt SSH private keys with age encryption
- 📁 Organized vault structure:
vault/keys/for encrypted keys,vault/tmp/for decrypted keys - ⚙️ Configurable via
.keymanrc.jsonwith sensible defaults - 🔍 Interactive CLI for encrypting, decrypting, and listing keys
- 🔄 Support for key rotation
Quick Start
1. Generate Age Encryption Key
# Create vault structure
mkdir -p vault/keys vault/tmp
# Generate age encryption key (keep this secret!)
age-keygen -o vault/age.key
# Add to .gitignore
echo "vault/age.key" >> .gitignore
echo "vault/tmp/" >> .gitignore
2. Generate SSH Keys
# Generate SSH key pair
ssh-keygen -t ed25519 -f vault/tmp/id_deploy -N "" -C "deploy@myapp.dev"
3. Run Keyman
# Run keyman interactively
VAULT_ROOT=./vault keyman
# Or if you have .keymanrc.json configured, just run:
keyman
Configuration
Keyman uses sensible defaults but can be customized via .keymanrc.json:
{
"vaultRoot": "./vault",
"keysDir": "keys",
"tmpDir": "tmp",
"ageKeyFile": "age.key"
}
Configuration Priority
- VAULT_ROOT environment variable (highest priority)
- .keymanrc.json file (searched from current directory upward)
- Default values (lowest priority)
Default Values
vaultRoot:"vault"keysDir:"keys"tmpDir:"tmp"ageKeyFile:"age.key"
Vault Structure
project/
├── vault/
│ ├── age.key # Master encryption key (NEVER commit!)
│ ├── keys/ # Encrypted keys (safe to commit)
│ │ └── deploy/ # Each key has its own folder
│ │ ├── id_deploy.pub # Public key
│ │ └── id_deploy.age # Encrypted private key
│ └── tmp/ # Decrypted keys (NEVER commit!)
│ ├── id_deploy # Decrypted private key
│ └── id_deploy.pub # Public key
└── .keymanrc.json # Configuration (optional)
Operations
Keyman provides an interactive menu-driven interface with the following operations:
- 📋 List keys - Compact view showing all keys with checkbox indicators for their locations
- 🔒 Encrypt keys - Encrypt SSH keys from
vault/tmp/and store invault/keys/ - 🔓 Decrypt keys - Decrypt keys from
vault/keys/tovault/tmp/or~/.ssh/ - ❌ Quit - Exit the program
After completing any operation, keyman automatically returns to the main menu, allowing you to perform multiple operations in a single session without restarting the tool.
List Keys Output
The list command shows a compact, unified view of all SSH keys with their locations:
🔑 SSH Keys:
Key Name [Vault] [Tmp] [.ssh]
──────────────────────────────────────────────────────────
✅ id_deploy (.pub) [✓] [ ] [✓]
🔓 id_github (.pub) [✓] [✓] [ ]
🔒 id_backup (.pub) [✓] [ ] [ ]
⚠️ id_local (.pub) [ ] [ ] [✓]
Legend:
✅ = Managed (encrypted in vault + active in .ssh)
🔓 = Decrypted (in vault + decrypted to tmp)
🔒 = Encrypted only (in vault, not decrypted)
⚠️ = Unmanaged (in .ssh or tmp, not encrypted in vault)
Features:
- Public keys are indicated with
(.pub)suffix instead of separate entries - Status emoji shows management state at a glance
- Checkboxes
[✓]show presence in three locations:- [Vault] - Encrypted in vault/keys/
- [Tmp] - Decrypted in vault/tmp/
- [.ssh] - Active in ~/.ssh/
- Alphabetically sorted for easy scanning
- New 🔓 status for keys decrypted to tmp but not yet in .ssh
Example Usage
# Using environment variable
VAULT_ROOT=../../vault keyman
# Using default configuration
keyman
# Keyman will show:
# 📁 Vault Root: /path/to/vault
# 🔑 Keys Directory: /path/to/vault/keys
# 📂 Temp Directory: /path/to/vault/tmp
# 🔐 Age Key: /path/to/vault/age.key
Best Practices
- Never commit
vault/age.keyorvault/tmp/to version control - Always backup your
age.keysecurely (password manager, encrypted USB) - Commit
vault/keys/- encrypted keys are safe to share - Use environment variables for CI/CD:
VAULT_ROOT=/path/to/vault keyman - Keep .keymanrc.json in your project root for team consistency