https://github.com/ajf1016/sshelf
Manage SSH profiles, keys, and identities from a single CLI.
https://github.com/ajf1016/sshelf
cli developer-tools git go golang ssh ssh-keys typer
Last synced: 4 days ago
JSON representation
Manage SSH profiles, keys, and identities from a single CLI.
- Host: GitHub
- URL: https://github.com/ajf1016/sshelf
- Owner: ajf1016
- License: mit
- Created: 2026-04-25T16:36:19.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-17T18:44:19.000Z (about 1 month ago)
- Last Synced: 2026-05-17T19:43:42.069Z (about 1 month ago)
- Topics: cli, developer-tools, git, go, golang, ssh, ssh-keys, typer
- Language: Go
- Homepage:
- Size: 130 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# sshelf
> One CLI to manage all your SSH identities, keys, and server connections — no more hand-editing `~/.ssh/config`.
[](https://github.com/ajf1016/sshelf/actions/workflows/ci.yml)
[](https://go.dev)
[](#installation)
[](LICENSE)
---
## The problem
You are a developer on a single machine. You have:
- A **personal GitHub** account for side projects
- A **work GitHub** account that your company owns
- A **test server** your team gave you access to via a `.pem` key
- Maybe a **freelance client** with their own GitLab
Every time you sit down to work you're either:
- Googling *"how to use two GitHub accounts SSH"* again
- Digging through `~/Downloads` for a `.pem` file you got in Slack three months ago
- Breaking your `~/.ssh/config` with a stray edit
- Forgetting which email address commits to which repo
**sshelf fixes all of this.** It manages SSH keys, host aliases, and agent sessions behind one clean CLI. You never touch `~/.ssh/config` by hand again.
---
## Table of Contents
- [Installation](#installation)
- [Real-world scenarios](#real-world-scenarios)
- [Two GitHub accounts on one machine](#scenario-1-two-github-accounts-on-one-machine)
- [Your team gave you a .pem key for a server](#scenario-2-your-team-gave-you-a-pem-key-for-a-server)
- [New laptop — restore everything from backup](#scenario-3-new-laptop--restore-everything-from-backup)
- [Already have SSH keys — migrate without losing anything](#scenario-4-already-have-ssh-keys--migrate-without-losing-anything)
- [Commands](#commands)
- [profile](#sshelf-profile)
- [key](#sshelf-key)
- [host](#sshelf-host)
- [agent](#sshelf-agent)
- [doctor](#sshelf-doctor)
- [import](#sshelf-import)
- [whoami](#sshelf-whoami)
- [completion](#sshelf-completion)
- [shell](#sshelf-shell)
- [Shell integration](#shell-integration)
- [How sshelf stores data](#how-sshelf-stores-data)
- [Key rotation](#key-rotation)
- [Jump hosts (bastion servers)](#jump-hosts-bastion-servers)
- [Doctor checks reference](#doctor-checks-reference)
- [Security](#security)
- [Building from source](#building-from-source)
- [Contributing](#contributing)
---
## Installation
### macOS / Linux — pre-built binary
```bash
# macOS (Apple Silicon)
curl -Lo sshelf https://github.com/ajf1016/sshelf/releases/latest/download/sshelf-darwin-arm64
chmod +x sshelf && sudo mv sshelf /usr/local/bin/
# macOS (Intel)
curl -Lo sshelf https://github.com/ajf1016/sshelf/releases/latest/download/sshelf-darwin-amd64
chmod +x sshelf && sudo mv sshelf /usr/local/bin/
# Linux (x86_64)
curl -Lo sshelf https://github.com/ajf1016/sshelf/releases/latest/download/sshelf-linux-amd64
chmod +x sshelf && sudo mv sshelf /usr/local/bin/
```
### go install
```bash
go install github.com/ajf1016/sshelf/cmd/sshelf@latest
```
### Homebrew
```bash
brew install ajf1016/tap/sshelf
```
> The Homebrew tap is coming soon. Use the binary install above in the meantime.
**Prerequisites:** `ssh-keygen` and `ssh-agent` ship with macOS and every major Linux distro by default.
---
## Real-world scenarios
### Scenario 1: Two GitHub accounts on one machine
You have `alex@gmail.com` on personal GitHub and `alex@acme.io` on work GitHub. Both repos live on the same laptop.
**One-time setup:**
```bash
# Create your personal identity — generates a key pair
sshelf profile init
# Profile name: personal
# Type: git → GitHub
# Email: alex@gmail.com
# Username: alex-personal
# Create your work identity — generates a separate key pair
sshelf profile init
# Profile name: work
# Type: git → GitHub
# Email: alex@acme.io
# Username: alex-acme
```
sshelf generates two separate key pairs and writes this automatically to `~/.ssh/config`:
```
Host github-personal
HostName github.com
User git
IdentityFile ~/.sshelf/keys/personal_ed25519
IdentitiesOnly yes
Host github-work
HostName github.com
User git
IdentityFile ~/.sshelf/keys/work_ed25519
IdentitiesOnly yes
```
**Copy the public keys to GitHub** (you only do this once):
```bash
sshelf key copy personal_ed25519 # copies to clipboard → paste into GitHub personal settings
sshelf key copy work_ed25519 # copies to clipboard → paste into GitHub work settings
```
**Day-to-day use:**
```bash
# Clone a personal repo — use the alias, not github.com directly
git clone git@github-personal:alex/my-side-project.git
# Clone a work repo
git clone git@github-work:acme/backend-service.git
# Switch your active git identity in the current terminal
sshelf-switch work # requires one-time: sshelf shell setup
# Verify who you are right now
sshelf whoami
# Active profile: work
# Git identity: alex
# SSH key: ~/.sshelf/keys/work_ed25519 (created 12 days ago)
# Agent: running · PID 34521 · 2 keys loaded
# Host aliases: github-work
```
---
### Scenario 2: Your team gave you a .pem key for a server
Your senior Slacked you:
> "Here's the staging server key 👇
> `ssh -i ~/Downloads/staging.pem ubuntu@203.0.113.42`"
You don't want to type that full command every time. You don't want that `.pem` rotting in `~/Downloads` forever.
**One-time setup:**
```bash
# Step 1 — bring the key into sshelf management
sshelf key add ~/Downloads/staging.pem
# → Imported key: staging (ed25519)
# → Key stored at ~/.sshelf/keys/staging.pem
# → Permissions set to 0600
# Step 2 — create a short alias for the server
sshelf host add
# Host alias → staging
# Hostname or IP → 203.0.113.42
# SSH user → ubuntu
# Identity key → staging.pem ← tab-completes from your imported keys
```
**From now on:**
```bash
ssh staging
# or
sshelf host connect staging
```
Both do exactly the same thing as the original long command — but you never have to remember the IP, the username, or which `.pem` to use.
**Verify the connection works:**
```bash
sshelf host test staging
# → Testing connection to staging (203.0.113.42)...
# → Connected successfully as ubuntu
```
**Team grows — more servers:**
```bash
sshelf host add --name prod --hostname 203.0.113.100 --user ubuntu --key staging.pem
sshelf host add --name dev-box --hostname 203.0.113.55 --user ec2-user --key staging.pem
sshelf host list
# ALIAS HOSTNAME USER KEY
# staging 203.0.113.42 ubuntu staging.pem
# prod 203.0.113.100 ubuntu staging.pem
# dev-box 203.0.113.55 ec2-user staging.pem
```
---
### Scenario 3: New laptop — restore everything from backup
Before you wiped your old machine, you backed up all your sshelf keys:
```bash
sshelf key backup
# Passphrase: ••••••••••••
# Confirm: ••••••••••••
# ✓ 6 key files backed up to ~/.sshelf/keys-backup-2026-05-17.vault
```
You copied that `.vault` file to a USB drive (or cloud storage). Now on the new machine:
```bash
# Install sshelf, then:
sshelf key restore /Volumes/USB/keys-backup-2026-05-17.vault
# Passphrase: ••••••••••••
# ✓ 6 key files restored to ~/.sshelf/keys/
sshelf doctor
# → All checks pass
```
Your profiles, host aliases, and every key are back exactly as they were. Nothing to reconfigure.
---
### Scenario 4: Already have SSH keys — migrate without losing anything
You already have `~/.ssh/id_ed25519`, `~/.ssh/work_rsa`, and a hand-crafted `~/.ssh/config`. You want sshelf to take over without losing anything.
```bash
sshelf import
```
sshelf scans your existing setup and lets you pick what to bring in:
```
→ Scanning ~/.ssh/config... found 3 Host blocks
→ Scanning ~/.ssh/... found 4 key files
Select keys to import:
◉ id_ed25519
◉ work_rsa
○ id_rsa_old ← left this one out
Select hosts to import:
◉ github.com
◉ gitlab.acme.io
○ 192.168.0.5 ← skipped this one too
✓ Imported 2 keys, 2 hosts
✓ SSH config updated
```
Your hand-written entries outside the sshelf block are **never modified**. Safe to run at any time.
After importing:
```bash
sshelf doctor --fix # fixes any permission issues on imported keys
```
---
## Commands
### `sshelf profile`
A **profile** is your identity: name, email, git username, and an SSH key pair. Create one per account you own.
---
#### `sshelf profile init`
Start the interactive wizard to create a new profile and generate its key pair.
```bash
sshelf profile init
```
Walks you through:
```
? Profile name: work
? Profile type: git (GitHub / GitLab / Bitbucket)
? Platform: GitHub
? Email: alex@acme.io
? Username: alex-acme
? Key name: work_ed25519 ← pre-filled, press Enter to accept
? Key type: ed25519 (recommended)
✓ Profile "work" created!
Public key (paste into GitHub → Settings → SSH keys):
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... alex@acme.io
```
---
#### `sshelf profile list`
Show all profiles. The active one is marked with `●`.
```bash
sshelf profile list
NAME TYPE PLATFORM EMAIL KEY AGE
● work git github alex@acme.io work_ed25519 12d
personal git github alex@gmail.com personal_ed25519 3mo
freelance git gitlab alex@client.com freelance_rsa 8mo
```
---
#### `sshelf profile switch `
Activate a profile. Updates the SSH config block and exports git identity env vars to your shell.
After running [`sshelf shell setup`](#sshelf-shell) once, you use the short form:
```bash
sshelf-switch work
# Now git commits go out as alex@acme.io with the work key
sshelf-switch personal
# Now git commits go out as alex@gmail.com with the personal key
```
If shell integration is not installed, use the raw form:
```bash
eval $(sshelf profile switch work)
```
> **Why `eval`?** A subprocess cannot export env vars to its parent shell. `switch` prints `export GIT_AUTHOR_NAME=...` lines; `eval` applies them. `sshelf shell setup` installs a wrapper so you never type `eval $(...)` again.
---
#### `sshelf profile show `
Print full details of a profile including its public key.
```bash
sshelf profile show work
Profile: work
Type: git
Platform: github
Email: alex@acme.io
Username: alex-acme
Key: work_ed25519
Created: 2026-05-05
Public key:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... alex@acme.io
```
Useful when you need to give your public key to a server admin or add it to a new platform.
---
#### `sshelf profile edit `
Update profile fields interactively. Pre-filled with current values — press Enter to keep any field unchanged.
```bash
sshelf profile edit work
# Change email from alex@acme.io → alex@newcompany.io
```
---
#### `sshelf profile remove `
Delete a profile and optionally its key pair.
```bash
sshelf profile remove freelance # prompts for confirmation
sshelf profile remove freelance --yes # skip confirmation
sshelf profile remove freelance --keep-key # delete profile, keep key files
```
Also works as:
```bash
sshelf profile delete freelance
```
---
#### `sshelf profile clone `
Duplicate a profile as a starting point. Useful when onboarding to a new project that shares the same platform.
```bash
sshelf profile clone work work-clientx
# Now edit the clone to set the right email
sshelf profile edit work-clientx
```
---
#### `sshelf profile rename `
Rename a profile and update all host aliases that reference it automatically.
```bash
sshelf profile rename work acme-work
```
---
### `sshelf key`
Keys are stored under `~/.sshelf/keys/` with `0600` permissions enforced automatically.
---
#### `sshelf key generate`
Generate a standalone key pair without creating a full profile. Useful for server keys or temporary access.
```bash
sshelf key generate --name deploy_ed25519
sshelf key generate --name legacy_server --type rsa --comment "old server needs RSA"
```
| Flag | Default | Description |
|---|---|---|
| `--name` | *(required)* | Base filename for the key pair |
| `--type` | `ed25519` | `ed25519` or `rsa` |
| `--comment` | `@sshelf` | Comment embedded in the public key |
---
#### `sshelf key list`
List all managed keys with age and which profile they belong to.
```bash
sshelf key list
NAME TYPE AGE PROFILE
work_ed25519 ed25519 12d work
personal_ed25519 ed25519 3mo personal
freelance_rsa rsa 8mo freelance
staging.pem rsa 47d — ← key without a profile (server key)
```
Keys with no profile are server keys imported via `key add`. They still work fine — they just aren't tied to a git identity.
---
#### `sshelf key add `
Import an existing key into sshelf management. Copies it to `~/.sshelf/keys/` and sets correct permissions.
```bash
# Your senior gave you a server key
sshelf key add ~/Downloads/staging.pem
# Bring in a key you already had in ~/.ssh/
sshelf key add ~/.ssh/id_ed25519
```
After importing, the original file is no longer needed — sshelf owns a copy.
---
#### `sshelf key remove `
Delete a managed key pair.
```bash
sshelf key remove old_rsa
sshelf key remove old_rsa --yes # skip confirmation
```
**Note:** if a profile references this key, the profile's `key_name` field will be blank until you update it with `sshelf profile edit`.
---
#### `sshelf key rotate `
Generate a new key pair for an existing name. The old key is archived as `.bak` — not deleted — giving you time to update GitHub/GitLab before removing it.
```bash
sshelf key rotate work_ed25519
→ Generating new ed25519 key...
→ Old key backed up to ~/.sshelf/keys/work_ed25519.bak
New public key (add this to GitHub, then revoke the old one):
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...newkey alex@acme.io
Remember to update your platforms, then run:
sshelf key remove work_ed25519.bak
```
`sshelf doctor` will warn you when any key is older than 1 year.
---
#### `sshelf key copy `
Copy a key's public key content to your clipboard. Falls back to printing if no clipboard tool is available.
```bash
sshelf key copy work_ed25519
# → Public key of "work_ed25519" copied to clipboard.
```
Paste directly into GitHub → Settings → SSH keys, or into a server's `authorized_keys`.
---
#### `sshelf key backup`
Encrypt all managed keys into a single vault file. You'll be prompted for a passphrase (min 8 characters).
```bash
sshelf key backup
# Passphrase: ••••••••••••
# Confirm: ••••••••••••
# ✓ 4 key file(s) backed up to ~/.sshelf/keys-backup-2026-05-17.vault
# Custom output location — e.g. USB drive
sshelf key backup --out /Volumes/USB/my-ssh-keys.vault
```
The vault is AES-256-GCM encrypted. Without the passphrase it cannot be read. Store it on a USB drive, cloud storage, or your password manager.
---
#### `sshelf key restore `
Decrypt a vault file and restore keys into `~/.sshelf/keys/`. Existing files are skipped unless `--force` is passed.
```bash
sshelf key restore ~/.sshelf/keys-backup-2026-05-17.vault
# Passphrase: ••••••••••••
# ✓ 4 key file(s) restored to ~/.sshelf/keys/
# Overwrite existing keys (e.g. after key rotation gone wrong)
sshelf key restore ~/backup/my-ssh-keys.vault --force
```
After restoring, run `sshelf doctor` to verify permissions.
---
### `sshelf host`
A **host** is a saved SSH destination: an alias name, hostname or IP, login user, and which key to use. Once added, `ssh ` works anywhere on your machine.
---
#### `sshelf host add`
Add a host alias interactively, or pass flags to skip the prompts.
```bash
# Interactive — shows autocomplete suggestions as you type:
sshelf host add
# Host alias → staging
# Hostname or IP → 203.0.113.42
# SSH user → ubuntu ← suggests: ubuntu, root, ec2-user, admin...
# Identity key name → staging.pem ← tab-completes from your imported keys
# Link to profile → ← tab-completes from your profiles
# ProxyJump / bastion →
# Non-interactive (good for scripts):
sshelf host add --name staging --hostname 203.0.113.42 --user ubuntu --key staging.pem
sshelf host add --name prod --hostname 203.0.113.100 --user ubuntu --profile work
```
| Flag | Description |
|---|---|
| `--name` | Short alias used in `ssh ` |
| `--hostname` | Real hostname or IP address |
| `--user` | SSH login username |
| `--port` | SSH port (default 22) |
| `--key` | Key name to use (overrides profile key) |
| `--profile` | Link to a profile — inherits its key automatically |
| `--jump` | ProxyJump (bastion) alias |
---
#### `sshelf host list`
List all host aliases, grouped by profile.
```bash
sshelf host list
Profile: work
ALIAS HOSTNAME USER KEY
github-work github.com git work_ed25519
staging 203.0.113.42 ubuntu work_ed25519
prod 203.0.113.100 ubuntu work_ed25519
Profile: personal
github-personal github.com git personal_ed25519
(no profile)
dev-box 203.0.113.55 ec2-user staging.pem
```
Filter by profile:
```bash
sshelf host list --profile work
```
---
#### `sshelf host remove `
Remove a host alias and regenerate the SSH config block.
```bash
sshelf host remove old-staging
sshelf host remove old-staging --yes
```
---
#### `sshelf host test `
Run a quick connectivity check using `ssh -T`. Shows whether the connection and key are working.
```bash
sshelf host test github-work
# Hi alex-acme! You've successfully authenticated, but GitHub does not provide shell access.
sshelf host test staging
# Connected to 203.0.113.42 (OpenSSH_9.0)
sshelf host test broken-server
# ssh: connect to host 203.0.113.99 port 22: Connection refused
```
Run this after adding a new host or rotating a key to confirm everything still works.
---
#### `sshelf host connect `
Open a live SSH session to a host alias. Identical to `ssh ` but explicitly goes through sshelf's config resolution.
```bash
sshelf host connect staging
# → opens a terminal session on the server
sshelf host connect prod
```
---
#### `sshelf host copy-id `
Push the linked profile's public key to a remote server's `authorized_keys` using `ssh-copy-id`. Use this to provision a new server before you have key-based auth set up — you'll be prompted for the password once.
```bash
sshelf host copy-id new-server
# → /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed.
# → alex@203.0.113.88's password: ••••••••
# → ✓ Key installed. You can now ssh in without a password.
```
---
#### `sshelf host jump `
Set or update a ProxyJump (bastion) for an alias — for reaching servers that aren't directly accessible from the internet.
```bash
sshelf host jump prod-app --via bastion
# → Set ProxyJump for "prod-app" → bastion
```
See [Jump hosts](#jump-hosts-bastion-servers) for the full bastion server workflow.
---
### `sshelf agent`
Manages the ssh-agent lifecycle. sshelf stores the agent's socket path and PID so keys stay loaded across terminal sessions on the same login.
---
#### `sshelf agent start`
Start a new ssh-agent and load all managed keys.
```bash
eval $(sshelf agent start)
# ssh-agent started.
# PID: 34521
```
#### `sshelf agent stop`
Kill the running agent.
```bash
sshelf agent stop
```
#### `sshelf agent status`
Show agent state, PID, and which keys are loaded.
```bash
sshelf agent status
Agent: running
PID: 34521
Socket: /tmp/ssh-XxYyZz/agent.34521
Loaded keys:
256 SHA256:abc... alex@acme.io (ED25519)
256 SHA256:xyz... alex@gmail.com (ED25519)
```
#### `sshelf agent reload`
Restart the agent and reload all keys. Run this after a profile switch or key rotation.
```bash
sshelf agent reload
```
---
### `sshelf doctor`
Run a suite of health checks and print a colour-coded table of results.
```bash
sshelf doctor
CHECK STATUS MESSAGE
config-dir PASS ~/.sshelf exists (0700)
permissions WARN staging.pem has permissions 0644, expected 0600 [fixable]
agent WARN ssh-agent is not running [fixable]
active-profile PASS active profile: work
key-age WARN freelance_rsa is 14 months old — run: sshelf key rotate freelance_rsa
orphaned-keys PASS no orphaned keys
3 issue(s). Run `sshelf doctor --fix` to auto-fix fixable items.
```
Auto-fix everything fixable in one shot:
```bash
sshelf doctor --fix
```
Run a single check by name:
```bash
sshelf doctor --check permissions
sshelf doctor --check key-age
sshelf doctor --check agent
```
**Fixable checks:** `config-dir`, `permissions`, `orphaned-keys`
**Informational only:** `key-age`, `active-profile`, `agent` (agent restart requires `eval`)
---
### `sshelf import`
Already have SSH keys and a `~/.ssh/config`? This command scans your existing setup and lets you migrate everything into sshelf without losing anything. Entries outside the managed block are **never touched**.
```bash
sshelf import
→ Scanning ~/.ssh/config... found 3 Host blocks
→ Scanning ~/.ssh/... found 4 key files
Select keys to import:
◉ id_ed25519 ← will be managed by sshelf
◉ work_rsa
○ id_rsa_old ← leaving this one out
Select hosts to import:
◉ github.com
◉ gitlab.acme.io
○ 192.168.0.5
Link imported items to a profile?
❯ work
✓ Imported key: id_ed25519
✓ Imported key: work_rsa
✓ Imported host: github.com
✓ Imported host: gitlab.acme.io
4 item(s) imported. SSH config updated. Keys stored in ~/.sshelf/keys/.
Run `sshelf doctor` to verify everything looks good.
```
---
### `sshelf whoami`
Print a snapshot of your current identity at a glance. Useful at the start of a work session.
```bash
sshelf whoami
Active profile: work
Git identity: alex
SSH key: ~/.sshelf/keys/work_ed25519 (created 12 days ago)
Agent: running · PID 34521 · 2 keys loaded
Host aliases: github-work, staging, prod
```
---
### `sshelf completion`
Generate shell completion scripts. After sourcing, `` completes profile names, host aliases, and key names live from your store.
```bash
# Zsh
sshelf completion zsh >> ~/.zshrc && source ~/.zshrc
# Bash
sshelf completion bash >> ~/.bashrc && source ~/.bashrc
# Fish
sshelf completion fish > ~/.config/fish/completions/sshelf.fish
```
```bash
sshelf profile switch
# work personal freelance
sshelf host connect
# staging prod dev-box github-work
sshelf key rotate
# work_ed25519 personal_ed25519 freelance_rsa
```
---
### `sshelf shell`
Shell integration utilities.
#### `sshelf shell setup`
Install the `sshelf-switch` function into your shell config file. Run once after installing sshelf — then switch profiles without ever typing `eval $(...)` again.
```bash
sshelf shell setup
# ✓ Shell integration installed in ~/.zshrc
# Apply now: source ~/.zshrc
# Then use: sshelf-switch work
```
Detects your shell automatically (`$SHELL`). Supports zsh, bash, and fish. Safe to re-run — checks for existing installation and skips if already done.
---
## Shell integration
### `sshelf shell setup`
Run this **once** after installing sshelf. It detects your shell (zsh, bash, or fish) and appends the `sshelf-switch` function to your config file automatically.
```bash
sshelf shell setup
# ✓ Shell integration installed in ~/.zshrc
#
# Apply now (or open a new terminal):
# source ~/.zshrc
#
# Then switch profiles with:
# sshelf-switch work
# sshelf-switch personal
```
From then on, switching is a single short command:
```bash
sshelf-switch work # activates work profile + sets git identity
sshelf-switch personal # activates personal profile
```
**What it installs** (you can also add this manually if you prefer):
Zsh / Bash (`~/.zshrc` or `~/.bashrc`):
```bash
# sshelf shell integration
sshelf-switch() {
eval $(sshelf profile switch "$1")
}
```
Fish (`~/.config/fish/config.fish`):
```fish
# sshelf shell integration
function sshelf-switch
eval (sshelf profile switch $argv)
end
```
### Auto-start the agent on login
```bash
# ~/.zshrc or ~/.bashrc
if ! sshelf agent status &>/dev/null; then
eval $(sshelf agent start)
fi
```
---
## How sshelf stores data
sshelf keeps all state under `~/.sshelf/`. The only thing it touches in `~/.ssh/` is a clearly delimited block inside `~/.ssh/config`.
| Path | Contents | Permissions |
|---|---|---|
| `~/.sshelf/` | Root config directory | `0700` |
| `~/.sshelf/profiles.toml` | All profile definitions | `0600` |
| `~/.sshelf/hosts.toml` | Host alias definitions | `0600` |
| `~/.sshelf/keys/` | Managed key pairs | `0700` |
| `~/.sshelf/keys/` | Private key | `0600` |
| `~/.sshelf/keys/.pub` | Public key | `0644` |
| `~/.sshelf/keys-backup-.vault` | AES-256-GCM encrypted key backup | `0600` |
| `~/.sshelf/agent.env` | Agent PID + socket path (runtime) | `0600` |
| `~/.ssh/config` | sshelf block only — everything else untouched | system |
### The managed SSH config block
```
# (your own entries above — sshelf never touches these)
# BEGIN sshelf-managed — do not edit this block manually
Host github-work
HostName github.com
User git
IdentityFile ~/.sshelf/keys/work_ed25519
IdentitiesOnly yes
Host github-personal
HostName github.com
User git
IdentityFile ~/.sshelf/keys/personal_ed25519
IdentitiesOnly yes
Host staging
HostName 203.0.113.42
User ubuntu
IdentityFile ~/.sshelf/keys/staging.pem
# END sshelf-managed
# (your own entries below — also untouched)
```
`IdentitiesOnly yes` is set on every profile-linked entry to prevent SSH from accidentally offering the wrong key when you have multiple accounts on the same service (e.g. two GitHub accounts).
---
## Key rotation
SSH keys age. `sshelf key rotate` makes rotation painless:
```bash
sshelf key rotate work_ed25519
```
What happens:
1. A fresh ed25519 key pair is generated at the same path.
2. The old key is moved to `work_ed25519.bak` — not deleted yet.
3. The SSH config block updates immediately to the new key.
4. The new public key is printed for you to add to GitHub/GitLab.
Once you've added the new key to all platforms:
```bash
sshelf key remove work_ed25519.bak
```
`sshelf doctor` warns you when any managed key is older than 1 year.
---
## Jump hosts (bastion servers)
Many companies route internal server access through a bastion host. sshelf handles this natively.
**Setup:**
```bash
# Add the bastion first
sshelf host add --name bastion --hostname bastion.acme.io --user alex --profile work
# Add the internal app server
sshelf host add --name prod-app --hostname 10.0.1.50 --user ubuntu --profile work
# Link prod-app through bastion
sshelf host jump prod-app --via bastion
```
sshelf generates this in `~/.ssh/config`:
```
Host bastion
HostName bastion.acme.io
User alex
IdentityFile ~/.sshelf/keys/work_ed25519
Host prod-app
HostName 10.0.1.50
User ubuntu
IdentityFile ~/.sshelf/keys/work_ed25519
ProxyJump bastion
```
Connect through the bastion in one command:
```bash
ssh prod-app
# or
sshelf host connect prod-app
```
---
## Doctor checks reference
| Check | ID | Severity | Auto-fixable | What it checks |
|---|---|---|---|---|
| Config directory | `config-dir` | warn | yes | `~/.sshelf/` exists with `0700` permissions |
| Key permissions | `permissions` | fail | yes | Every private key is `0600`, public keys `0644` |
| Agent running | `agent` | warn | no | ssh-agent process is alive and socket reachable |
| Active profile | `active-profile` | warn | no | At least one profile is marked active |
| Key age | `key-age` | warn | no | No managed key older than 1 year |
| Orphaned keys | `orphaned-keys` | warn | yes | No key file exists without a profile referencing it |
---
## Security
- **Private keys** are stored at `0600`. `sshelf doctor` flags any drift and can fix it automatically.
- **No network calls.** sshelf never transmits your keys or config to any remote service.
- **Atomic writes.** Every config and key write uses temp-file → `os.Rename`. A crash mid-write cannot corrupt your config.
- **`IdentitiesOnly yes`** on every generated entry prevents SSH from offering the wrong key to hosts where you have multiple accounts.
- **Key generation** delegates to the system `ssh-keygen` binary. sshelf does not implement cryptography for key generation.
- **Vault encryption** uses AES-256-GCM with a key derived from 100,000 rounds of iterated SHA-256 + a random 16-byte salt. Pure stdlib — no external crypto dependencies.
- **Destructive operations require confirmation.** Pass `--yes` only in scripts where you control the input.
- **sshelf never reads private key content.** It manages paths, metadata, and permissions only.
---
## Building from source
Requires **Go 1.23+** and the system `ssh-keygen` / `ssh-agent` binaries.
```bash
git clone https://github.com/ajf1016/sshelf.git
cd sshelf
# Run tests with race detector
go test -race ./...
# Development build
go build -o sshelf ./cmd/sshelf
sudo mv sshelf /usr/local/bin/
# Release build — strip debug info, inject version
go build \
-trimpath \
-ldflags="-s -w -X github.com/ajf1016/sshelf/internal/config.AppVersion=v0.1.0" \
-o sshelf \
./cmd/sshelf
```
### Cross-compile
```bash
# Linux amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
go build -trimpath -o dist/sshelf-linux-amd64 ./cmd/sshelf
# macOS arm64
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 \
go build -trimpath -o dist/sshelf-darwin-arm64 ./cmd/sshelf
```
---
## Contributing
Contributions are welcome. Before opening a PR:
1. **Run the test suite** with the race detector:
```bash
go test -race ./...
```
2. **Run the linter:**
```bash
go vet ./...
golangci-lint run
```
3. **Keep commands thin.** Business logic belongs in `internal/core/`, not `internal/commands/`. Commands parse flags, call core services, and format output — nothing more.
4. **Tests for core logic are required.** Packages under `internal/core/` need test coverage. Command files don't.
5. **No real credentials in tests.** Use `t.TempDir()` for all paths; never commit hostnames, IPs, emails, or key material.
### Project layout
```
sshelf/
├── cmd/sshelf/ # binary entry point — main.go only
├── internal/
│ ├── commands/ # cobra command wrappers — thin, no business logic
│ ├── config/ # constants, permission bits, default paths
│ ├── core/ # all business logic: profiles, keys, hosts, agent, vault, doctor
│ ├── platform/ # OS-specific helpers (clipboard)
│ ├── ui/ # lipgloss styles, table renderer, formatting helpers
│ ├── utils/ # atomic file I/O, typed errors
│ └── wizard/ # interactive huh forms for init and import flows
```
---
## License
MIT — see [LICENSE](LICENSE).
---
*Built with [cobra](https://github.com/spf13/cobra), [huh](https://github.com/charmbracelet/huh), [lipgloss](https://github.com/charmbracelet/lipgloss), and [BurntSushi/toml](https://github.com/BurntSushi/toml).*