{"id":51142727,"url":"https://github.com/ajf1016/sshelf","last_synced_at":"2026-06-26T00:30:44.747Z","repository":{"id":358510148,"uuid":"1221016568","full_name":"ajf1016/sshelf","owner":"ajf1016","description":"Manage SSH profiles, keys, and identities from a single CLI.","archived":false,"fork":false,"pushed_at":"2026-05-17T18:44:19.000Z","size":133,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-17T19:43:42.069Z","etag":null,"topics":["cli","developer-tools","git","go","golang","ssh","ssh-keys","typer"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ajf1016.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-25T16:36:19.000Z","updated_at":"2026-05-17T18:55:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ajf1016/sshelf","commit_stats":null,"previous_names":["ajf1016/sshelf"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ajf1016/sshelf","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajf1016%2Fsshelf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajf1016%2Fsshelf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajf1016%2Fsshelf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajf1016%2Fsshelf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ajf1016","download_url":"https://codeload.github.com/ajf1016/sshelf/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajf1016%2Fsshelf/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34798182,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","developer-tools","git","go","golang","ssh","ssh-keys","typer"],"created_at":"2026-06-26T00:30:43.976Z","updated_at":"2026-06-26T00:30:44.728Z","avatar_url":"https://github.com/ajf1016.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sshelf\n\n\u003e One CLI to manage all your SSH identities, keys, and server connections — no more hand-editing `~/.ssh/config`.\n\n[![CI](https://github.com/ajf1016/sshelf/actions/workflows/ci.yml/badge.svg)](https://github.com/ajf1016/sshelf/actions/workflows/ci.yml)\n[![Go Version](https://img.shields.io/badge/go-1.23-00ADD8?logo=go)](https://go.dev)\n[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey)](#installation)\n[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)\n\n---\n\n## The problem\n\nYou are a developer on a single machine. You have:\n\n- A **personal GitHub** account for side projects\n- A **work GitHub** account that your company owns\n- A **test server** your team gave you access to via a `.pem` key\n- Maybe a **freelance client** with their own GitLab\n\nEvery time you sit down to work you're either:\n\n- Googling *\"how to use two GitHub accounts SSH\"* again\n- Digging through `~/Downloads` for a `.pem` file you got in Slack three months ago\n- Breaking your `~/.ssh/config` with a stray edit\n- Forgetting which email address commits to which repo\n\n**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.\n\n---\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Real-world scenarios](#real-world-scenarios)\n  - [Two GitHub accounts on one machine](#scenario-1-two-github-accounts-on-one-machine)\n  - [Your team gave you a .pem key for a server](#scenario-2-your-team-gave-you-a-pem-key-for-a-server)\n  - [New laptop — restore everything from backup](#scenario-3-new-laptop--restore-everything-from-backup)\n  - [Already have SSH keys — migrate without losing anything](#scenario-4-already-have-ssh-keys--migrate-without-losing-anything)\n- [Commands](#commands)\n  - [profile](#sshelf-profile)\n  - [key](#sshelf-key)\n  - [host](#sshelf-host)\n  - [agent](#sshelf-agent)\n  - [doctor](#sshelf-doctor)\n  - [import](#sshelf-import)\n  - [whoami](#sshelf-whoami)\n  - [completion](#sshelf-completion)\n  - [shell](#sshelf-shell)\n- [Shell integration](#shell-integration)\n- [How sshelf stores data](#how-sshelf-stores-data)\n- [Key rotation](#key-rotation)\n- [Jump hosts (bastion servers)](#jump-hosts-bastion-servers)\n- [Doctor checks reference](#doctor-checks-reference)\n- [Security](#security)\n- [Building from source](#building-from-source)\n- [Contributing](#contributing)\n\n---\n\n## Installation\n\n### macOS / Linux — pre-built binary\n\n```bash\n# macOS (Apple Silicon)\ncurl -Lo sshelf https://github.com/ajf1016/sshelf/releases/latest/download/sshelf-darwin-arm64\nchmod +x sshelf \u0026\u0026 sudo mv sshelf /usr/local/bin/\n\n# macOS (Intel)\ncurl -Lo sshelf https://github.com/ajf1016/sshelf/releases/latest/download/sshelf-darwin-amd64\nchmod +x sshelf \u0026\u0026 sudo mv sshelf /usr/local/bin/\n\n# Linux (x86_64)\ncurl -Lo sshelf https://github.com/ajf1016/sshelf/releases/latest/download/sshelf-linux-amd64\nchmod +x sshelf \u0026\u0026 sudo mv sshelf /usr/local/bin/\n```\n\n### go install\n\n```bash\ngo install github.com/ajf1016/sshelf/cmd/sshelf@latest\n```\n\n### Homebrew\n\n```bash\nbrew install ajf1016/tap/sshelf\n```\n\n\u003e The Homebrew tap is coming soon. Use the binary install above in the meantime.\n\n**Prerequisites:** `ssh-keygen` and `ssh-agent` ship with macOS and every major Linux distro by default.\n\n---\n\n## Real-world scenarios\n\n### Scenario 1: Two GitHub accounts on one machine\n\nYou have `alex@gmail.com` on personal GitHub and `alex@acme.io` on work GitHub. Both repos live on the same laptop.\n\n**One-time setup:**\n\n```bash\n# Create your personal identity — generates a key pair\nsshelf profile init\n# Profile name: personal\n# Type: git → GitHub\n# Email: alex@gmail.com\n# Username: alex-personal\n\n# Create your work identity — generates a separate key pair\nsshelf profile init\n# Profile name: work\n# Type: git → GitHub\n# Email: alex@acme.io\n# Username: alex-acme\n```\n\nsshelf generates two separate key pairs and writes this automatically to `~/.ssh/config`:\n\n```\nHost github-personal\n  HostName github.com\n  User git\n  IdentityFile ~/.sshelf/keys/personal_ed25519\n  IdentitiesOnly yes\n\nHost github-work\n  HostName github.com\n  User git\n  IdentityFile ~/.sshelf/keys/work_ed25519\n  IdentitiesOnly yes\n```\n\n**Copy the public keys to GitHub** (you only do this once):\n\n```bash\nsshelf key copy personal_ed25519   # copies to clipboard → paste into GitHub personal settings\nsshelf key copy work_ed25519       # copies to clipboard → paste into GitHub work settings\n```\n\n**Day-to-day use:**\n\n```bash\n# Clone a personal repo — use the alias, not github.com directly\ngit clone git@github-personal:alex/my-side-project.git\n\n# Clone a work repo\ngit clone git@github-work:acme/backend-service.git\n\n# Switch your active git identity in the current terminal\nsshelf-switch work   # requires one-time: sshelf shell setup\n\n# Verify who you are right now\nsshelf whoami\n#   Active profile:  work\n#   Git identity:    alex \u003calex@acme.io\u003e\n#   SSH key:         ~/.sshelf/keys/work_ed25519  (created 12 days ago)\n#   Agent:           running · PID 34521 · 2 keys loaded\n#   Host aliases:    github-work\n```\n\n---\n\n### Scenario 2: Your team gave you a .pem key for a server\n\nYour senior Slacked you:\n\n\u003e \"Here's the staging server key 👇  \n\u003e `ssh -i ~/Downloads/staging.pem ubuntu@203.0.113.42`\"\n\nYou don't want to type that full command every time. You don't want that `.pem` rotting in `~/Downloads` forever.\n\n**One-time setup:**\n\n```bash\n# Step 1 — bring the key into sshelf management\nsshelf key add ~/Downloads/staging.pem\n# → Imported key: staging (ed25519)\n# → Key stored at ~/.sshelf/keys/staging.pem\n# → Permissions set to 0600\n\n# Step 2 — create a short alias for the server\nsshelf host add\n#   Host alias       → staging\n#   Hostname or IP   → 203.0.113.42\n#   SSH user         → ubuntu\n#   Identity key     → staging.pem      ← tab-completes from your imported keys\n```\n\n**From now on:**\n\n```bash\nssh staging\n# or\nsshelf host connect staging\n```\n\nBoth 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.\n\n**Verify the connection works:**\n\n```bash\nsshelf host test staging\n# → Testing connection to staging (203.0.113.42)...\n# → Connected successfully as ubuntu\n```\n\n**Team grows — more servers:**\n\n```bash\nsshelf host add --name prod     --hostname 203.0.113.100 --user ubuntu --key staging.pem\nsshelf host add --name dev-box  --hostname 203.0.113.55  --user ec2-user --key staging.pem\n\nsshelf host list\n#   ALIAS      HOSTNAME          USER       KEY\n#   staging    203.0.113.42      ubuntu     staging.pem\n#   prod       203.0.113.100     ubuntu     staging.pem\n#   dev-box    203.0.113.55      ec2-user   staging.pem\n```\n\n---\n\n### Scenario 3: New laptop — restore everything from backup\n\nBefore you wiped your old machine, you backed up all your sshelf keys:\n\n```bash\nsshelf key backup\n# Passphrase: ••••••••••••\n# Confirm:    ••••••••••••\n# ✓ 6 key files backed up to ~/.sshelf/keys-backup-2026-05-17.vault\n```\n\nYou copied that `.vault` file to a USB drive (or cloud storage). Now on the new machine:\n\n```bash\n# Install sshelf, then:\nsshelf key restore /Volumes/USB/keys-backup-2026-05-17.vault\n# Passphrase: ••••••••••••\n# ✓ 6 key files restored to ~/.sshelf/keys/\n\nsshelf doctor\n# → All checks pass\n```\n\nYour profiles, host aliases, and every key are back exactly as they were. Nothing to reconfigure.\n\n---\n\n### Scenario 4: Already have SSH keys — migrate without losing anything\n\nYou already have `~/.ssh/id_ed25519`, `~/.ssh/work_rsa`, and a hand-crafted `~/.ssh/config`. You want sshelf to take over without losing anything.\n\n```bash\nsshelf import\n```\n\nsshelf scans your existing setup and lets you pick what to bring in:\n\n```\n→ Scanning ~/.ssh/config... found 3 Host blocks\n→ Scanning ~/.ssh/... found 4 key files\n\n  Select keys to import:\n  ◉ id_ed25519\n  ◉ work_rsa\n  ○ id_rsa_old          ← left this one out\n\n  Select hosts to import:\n  ◉ github.com\n  ◉ gitlab.acme.io\n  ○ 192.168.0.5         ← skipped this one too\n\n  ✓ Imported 2 keys, 2 hosts\n  ✓ SSH config updated\n```\n\nYour hand-written entries outside the sshelf block are **never modified**. Safe to run at any time.\n\nAfter importing:\n\n```bash\nsshelf doctor --fix   # fixes any permission issues on imported keys\n```\n\n---\n\n## Commands\n\n### `sshelf profile`\n\nA **profile** is your identity: name, email, git username, and an SSH key pair. Create one per account you own.\n\n---\n\n#### `sshelf profile init`\n\nStart the interactive wizard to create a new profile and generate its key pair.\n\n```bash\nsshelf profile init\n```\n\nWalks you through:\n\n```\n? Profile name:      work\n? Profile type:      git (GitHub / GitLab / Bitbucket)\n? Platform:          GitHub\n? Email:             alex@acme.io\n? Username:          alex-acme\n? Key name:          work_ed25519      ← pre-filled, press Enter to accept\n? Key type:          ed25519 (recommended)\n\n✓ Profile \"work\" created!\n\nPublic key (paste into GitHub → Settings → SSH keys):\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... alex@acme.io\n```\n\n---\n\n#### `sshelf profile list`\n\nShow all profiles. The active one is marked with `●`.\n\n```bash\nsshelf profile list\n\n    NAME       TYPE    PLATFORM   EMAIL               KEY               AGE\n  ● work       git     github     alex@acme.io        work_ed25519      12d\n    personal   git     github     alex@gmail.com      personal_ed25519  3mo\n    freelance  git     gitlab     alex@client.com     freelance_rsa     8mo\n```\n\n---\n\n#### `sshelf profile switch \u003cname\u003e`\n\nActivate a profile. Updates the SSH config block and exports git identity env vars to your shell.\n\nAfter running [`sshelf shell setup`](#sshelf-shell) once, you use the short form:\n\n```bash\nsshelf-switch work\n# Now git commits go out as alex@acme.io with the work key\n\nsshelf-switch personal\n# Now git commits go out as alex@gmail.com with the personal key\n```\n\nIf shell integration is not installed, use the raw form:\n\n```bash\neval $(sshelf profile switch work)\n```\n\n\u003e **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.\n\n---\n\n#### `sshelf profile show \u003cname\u003e`\n\nPrint full details of a profile including its public key.\n\n```bash\nsshelf profile show work\n\n  Profile:    work\n  Type:       git\n  Platform:   github\n  Email:      alex@acme.io\n  Username:   alex-acme\n  Key:        work_ed25519\n  Created:    2026-05-05\n\n  Public key:\n  ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... alex@acme.io\n```\n\nUseful when you need to give your public key to a server admin or add it to a new platform.\n\n---\n\n#### `sshelf profile edit \u003cname\u003e`\n\nUpdate profile fields interactively. Pre-filled with current values — press Enter to keep any field unchanged.\n\n```bash\nsshelf profile edit work\n# Change email from alex@acme.io → alex@newcompany.io\n```\n\n---\n\n#### `sshelf profile remove \u003cname\u003e`\n\nDelete a profile and optionally its key pair.\n\n```bash\nsshelf profile remove freelance          # prompts for confirmation\nsshelf profile remove freelance --yes    # skip confirmation\nsshelf profile remove freelance --keep-key  # delete profile, keep key files\n```\n\nAlso works as:\n\n```bash\nsshelf profile delete freelance\n```\n\n---\n\n#### `sshelf profile clone \u003cname\u003e \u003cnew-name\u003e`\n\nDuplicate a profile as a starting point. Useful when onboarding to a new project that shares the same platform.\n\n```bash\nsshelf profile clone work work-clientx\n# Now edit the clone to set the right email\nsshelf profile edit work-clientx\n```\n\n---\n\n#### `sshelf profile rename \u003cold\u003e \u003cnew\u003e`\n\nRename a profile and update all host aliases that reference it automatically.\n\n```bash\nsshelf profile rename work acme-work\n```\n\n---\n\n### `sshelf key`\n\nKeys are stored under `~/.sshelf/keys/` with `0600` permissions enforced automatically.\n\n---\n\n#### `sshelf key generate`\n\nGenerate a standalone key pair without creating a full profile. Useful for server keys or temporary access.\n\n```bash\nsshelf key generate --name deploy_ed25519\nsshelf key generate --name legacy_server --type rsa --comment \"old server needs RSA\"\n```\n\n| Flag | Default | Description |\n|---|---|---|\n| `--name` | *(required)* | Base filename for the key pair |\n| `--type` | `ed25519` | `ed25519` or `rsa` |\n| `--comment` | `\u003cname\u003e@sshelf` | Comment embedded in the public key |\n\n---\n\n#### `sshelf key list`\n\nList all managed keys with age and which profile they belong to.\n\n```bash\nsshelf key list\n\n  NAME                 TYPE      AGE    PROFILE\n  work_ed25519         ed25519   12d    work\n  personal_ed25519     ed25519   3mo    personal\n  freelance_rsa        rsa       8mo    freelance\n  staging.pem          rsa       47d    —           ← key without a profile (server key)\n```\n\nKeys with no profile are server keys imported via `key add`. They still work fine — they just aren't tied to a git identity.\n\n---\n\n#### `sshelf key add \u003cpath\u003e`\n\nImport an existing key into sshelf management. Copies it to `~/.sshelf/keys/` and sets correct permissions.\n\n```bash\n# Your senior gave you a server key\nsshelf key add ~/Downloads/staging.pem\n\n# Bring in a key you already had in ~/.ssh/\nsshelf key add ~/.ssh/id_ed25519\n```\n\nAfter importing, the original file is no longer needed — sshelf owns a copy.\n\n---\n\n#### `sshelf key remove \u003cname\u003e`\n\nDelete a managed key pair.\n\n```bash\nsshelf key remove old_rsa\nsshelf key remove old_rsa --yes   # skip confirmation\n```\n\n**Note:** if a profile references this key, the profile's `key_name` field will be blank until you update it with `sshelf profile edit`.\n\n---\n\n#### `sshelf key rotate \u003cname\u003e`\n\nGenerate a new key pair for an existing name. The old key is archived as `\u003cname\u003e.bak` — not deleted — giving you time to update GitHub/GitLab before removing it.\n\n```bash\nsshelf key rotate work_ed25519\n\n  → Generating new ed25519 key...\n  → Old key backed up to ~/.sshelf/keys/work_ed25519.bak\n\n  New public key (add this to GitHub, then revoke the old one):\n  ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...newkey alex@acme.io\n\n  Remember to update your platforms, then run:\n    sshelf key remove work_ed25519.bak\n```\n\n`sshelf doctor` will warn you when any key is older than 1 year.\n\n---\n\n#### `sshelf key copy \u003cname\u003e`\n\nCopy a key's public key content to your clipboard. Falls back to printing if no clipboard tool is available.\n\n```bash\nsshelf key copy work_ed25519\n# → Public key of \"work_ed25519\" copied to clipboard.\n```\n\nPaste directly into GitHub → Settings → SSH keys, or into a server's `authorized_keys`.\n\n---\n\n#### `sshelf key backup`\n\nEncrypt all managed keys into a single vault file. You'll be prompted for a passphrase (min 8 characters).\n\n```bash\nsshelf key backup\n# Passphrase: ••••••••••••\n# Confirm:    ••••••••••••\n# ✓ 4 key file(s) backed up to ~/.sshelf/keys-backup-2026-05-17.vault\n\n# Custom output location — e.g. USB drive\nsshelf key backup --out /Volumes/USB/my-ssh-keys.vault\n```\n\nThe 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.\n\n---\n\n#### `sshelf key restore \u003cvault-file\u003e`\n\nDecrypt a vault file and restore keys into `~/.sshelf/keys/`. Existing files are skipped unless `--force` is passed.\n\n```bash\nsshelf key restore ~/.sshelf/keys-backup-2026-05-17.vault\n# Passphrase: ••••••••••••\n# ✓ 4 key file(s) restored to ~/.sshelf/keys/\n\n# Overwrite existing keys (e.g. after key rotation gone wrong)\nsshelf key restore ~/backup/my-ssh-keys.vault --force\n```\n\nAfter restoring, run `sshelf doctor` to verify permissions.\n\n---\n\n### `sshelf host`\n\nA **host** is a saved SSH destination: an alias name, hostname or IP, login user, and which key to use. Once added, `ssh \u003calias\u003e` works anywhere on your machine.\n\n---\n\n#### `sshelf host add`\n\nAdd a host alias interactively, or pass flags to skip the prompts.\n\n```bash\n# Interactive — shows autocomplete suggestions as you type:\nsshelf host add\n#   Host alias           → staging\n#   Hostname or IP       → 203.0.113.42\n#   SSH user             → ubuntu        ← suggests: ubuntu, root, ec2-user, admin...\n#   Identity key name    → staging.pem   ← tab-completes from your imported keys\n#   Link to profile      →               ← tab-completes from your profiles\n#   ProxyJump / bastion  →\n\n# Non-interactive (good for scripts):\nsshelf host add --name staging --hostname 203.0.113.42 --user ubuntu --key staging.pem\nsshelf host add --name prod    --hostname 203.0.113.100 --user ubuntu --profile work\n```\n\n| Flag | Description |\n|---|---|\n| `--name` | Short alias used in `ssh \u003calias\u003e` |\n| `--hostname` | Real hostname or IP address |\n| `--user` | SSH login username |\n| `--port` | SSH port (default 22) |\n| `--key` | Key name to use (overrides profile key) |\n| `--profile` | Link to a profile — inherits its key automatically |\n| `--jump` | ProxyJump (bastion) alias |\n\n---\n\n#### `sshelf host list`\n\nList all host aliases, grouped by profile.\n\n```bash\nsshelf host list\n\n  Profile: work\n    ALIAS           HOSTNAME            USER       KEY\n    github-work     github.com          git        work_ed25519\n    staging         203.0.113.42        ubuntu     work_ed25519\n    prod            203.0.113.100       ubuntu     work_ed25519\n\n  Profile: personal\n    github-personal github.com          git        personal_ed25519\n\n  (no profile)\n    dev-box         203.0.113.55        ec2-user   staging.pem\n```\n\nFilter by profile:\n\n```bash\nsshelf host list --profile work\n```\n\n---\n\n#### `sshelf host remove \u003calias\u003e`\n\nRemove a host alias and regenerate the SSH config block.\n\n```bash\nsshelf host remove old-staging\nsshelf host remove old-staging --yes\n```\n\n---\n\n#### `sshelf host test \u003calias\u003e`\n\nRun a quick connectivity check using `ssh -T`. Shows whether the connection and key are working.\n\n```bash\nsshelf host test github-work\n# Hi alex-acme! You've successfully authenticated, but GitHub does not provide shell access.\n\nsshelf host test staging\n# Connected to 203.0.113.42 (OpenSSH_9.0)\n\nsshelf host test broken-server\n# ssh: connect to host 203.0.113.99 port 22: Connection refused\n```\n\nRun this after adding a new host or rotating a key to confirm everything still works.\n\n---\n\n#### `sshelf host connect \u003calias\u003e`\n\nOpen a live SSH session to a host alias. Identical to `ssh \u003calias\u003e` but explicitly goes through sshelf's config resolution.\n\n```bash\nsshelf host connect staging\n# → opens a terminal session on the server\n\nsshelf host connect prod\n```\n\n---\n\n#### `sshelf host copy-id \u003calias\u003e`\n\nPush 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.\n\n```bash\nsshelf host copy-id new-server\n# → /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed.\n# → alex@203.0.113.88's password: ••••••••\n# → ✓ Key installed. You can now ssh in without a password.\n```\n\n---\n\n#### `sshelf host jump \u003calias\u003e`\n\nSet or update a ProxyJump (bastion) for an alias — for reaching servers that aren't directly accessible from the internet.\n\n```bash\nsshelf host jump prod-app --via bastion\n# → Set ProxyJump for \"prod-app\" → bastion\n```\n\nSee [Jump hosts](#jump-hosts-bastion-servers) for the full bastion server workflow.\n\n---\n\n### `sshelf agent`\n\nManages the ssh-agent lifecycle. sshelf stores the agent's socket path and PID so keys stay loaded across terminal sessions on the same login.\n\n---\n\n#### `sshelf agent start`\n\nStart a new ssh-agent and load all managed keys.\n\n```bash\neval $(sshelf agent start)\n# ssh-agent started.\n# PID: 34521\n```\n\n#### `sshelf agent stop`\n\nKill the running agent.\n\n```bash\nsshelf agent stop\n```\n\n#### `sshelf agent status`\n\nShow agent state, PID, and which keys are loaded.\n\n```bash\nsshelf agent status\n\n  Agent:    running\n  PID:      34521\n  Socket:   /tmp/ssh-XxYyZz/agent.34521\n\n  Loaded keys:\n    256 SHA256:abc... alex@acme.io (ED25519)\n    256 SHA256:xyz... alex@gmail.com (ED25519)\n```\n\n#### `sshelf agent reload`\n\nRestart the agent and reload all keys. Run this after a profile switch or key rotation.\n\n```bash\nsshelf agent reload\n```\n\n---\n\n### `sshelf doctor`\n\nRun a suite of health checks and print a colour-coded table of results.\n\n```bash\nsshelf doctor\n\n  CHECK           STATUS   MESSAGE\n  config-dir      PASS     ~/.sshelf exists (0700)\n  permissions     WARN     staging.pem has permissions 0644, expected 0600  [fixable]\n  agent           WARN     ssh-agent is not running  [fixable]\n  active-profile  PASS     active profile: work\n  key-age         WARN     freelance_rsa is 14 months old — run: sshelf key rotate freelance_rsa\n  orphaned-keys   PASS     no orphaned keys\n\n  3 issue(s). Run `sshelf doctor --fix` to auto-fix fixable items.\n```\n\nAuto-fix everything fixable in one shot:\n\n```bash\nsshelf doctor --fix\n```\n\nRun a single check by name:\n\n```bash\nsshelf doctor --check permissions\nsshelf doctor --check key-age\nsshelf doctor --check agent\n```\n\n**Fixable checks:** `config-dir`, `permissions`, `orphaned-keys`\n**Informational only:** `key-age`, `active-profile`, `agent` (agent restart requires `eval`)\n\n---\n\n### `sshelf import`\n\nAlready 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**.\n\n```bash\nsshelf import\n\n  → Scanning ~/.ssh/config... found 3 Host blocks\n  → Scanning ~/.ssh/... found 4 key files\n\n  Select keys to import:\n  ◉ id_ed25519          ← will be managed by sshelf\n  ◉ work_rsa\n  ○ id_rsa_old          ← leaving this one out\n\n  Select hosts to import:\n  ◉ github.com\n  ◉ gitlab.acme.io\n  ○ 192.168.0.5\n\n  Link imported items to a profile?\n  ❯ work\n\n  ✓ Imported key: id_ed25519\n  ✓ Imported key: work_rsa\n  ✓ Imported host: github.com\n  ✓ Imported host: gitlab.acme.io\n\n  4 item(s) imported. SSH config updated. Keys stored in ~/.sshelf/keys/.\n  Run `sshelf doctor` to verify everything looks good.\n```\n\n---\n\n### `sshelf whoami`\n\nPrint a snapshot of your current identity at a glance. Useful at the start of a work session.\n\n```bash\nsshelf whoami\n\n  Active profile:   work\n  Git identity:     alex \u003calex@acme.io\u003e\n  SSH key:          ~/.sshelf/keys/work_ed25519  (created 12 days ago)\n  Agent:            running · PID 34521 · 2 keys loaded\n  Host aliases:     github-work, staging, prod\n```\n\n---\n\n### `sshelf completion`\n\nGenerate shell completion scripts. After sourcing, `\u003cTab\u003e` completes profile names, host aliases, and key names live from your store.\n\n```bash\n# Zsh\nsshelf completion zsh \u003e\u003e ~/.zshrc \u0026\u0026 source ~/.zshrc\n\n# Bash\nsshelf completion bash \u003e\u003e ~/.bashrc \u0026\u0026 source ~/.bashrc\n\n# Fish\nsshelf completion fish \u003e ~/.config/fish/completions/sshelf.fish\n```\n\n```bash\nsshelf profile switch \u003cTab\u003e\n# work    personal    freelance\n\nsshelf host connect \u003cTab\u003e\n# staging    prod    dev-box    github-work\n\nsshelf key rotate \u003cTab\u003e\n# work_ed25519    personal_ed25519    freelance_rsa\n```\n\n---\n\n### `sshelf shell`\n\nShell integration utilities.\n\n#### `sshelf shell setup`\n\nInstall the `sshelf-switch` function into your shell config file. Run once after installing sshelf — then switch profiles without ever typing `eval $(...)` again.\n\n```bash\nsshelf shell setup\n# ✓ Shell integration installed in ~/.zshrc\n# Apply now:  source ~/.zshrc\n# Then use:   sshelf-switch work\n```\n\nDetects your shell automatically (`$SHELL`). Supports zsh, bash, and fish. Safe to re-run — checks for existing installation and skips if already done.\n\n---\n\n## Shell integration\n\n### `sshelf shell setup`\n\nRun this **once** after installing sshelf. It detects your shell (zsh, bash, or fish) and appends the `sshelf-switch` function to your config file automatically.\n\n```bash\nsshelf shell setup\n# ✓ Shell integration installed in ~/.zshrc\n#\n# Apply now (or open a new terminal):\n#   source ~/.zshrc\n#\n# Then switch profiles with:\n#   sshelf-switch work\n#   sshelf-switch personal\n```\n\nFrom then on, switching is a single short command:\n\n```bash\nsshelf-switch work       # activates work profile + sets git identity\nsshelf-switch personal   # activates personal profile\n```\n\n**What it installs** (you can also add this manually if you prefer):\n\nZsh / Bash (`~/.zshrc` or `~/.bashrc`):\n```bash\n# sshelf shell integration\nsshelf-switch() {\n  eval $(sshelf profile switch \"$1\")\n}\n```\n\nFish (`~/.config/fish/config.fish`):\n```fish\n# sshelf shell integration\nfunction sshelf-switch\n  eval (sshelf profile switch $argv)\nend\n```\n\n### Auto-start the agent on login\n\n```bash\n# ~/.zshrc or ~/.bashrc\nif ! sshelf agent status \u0026\u003e/dev/null; then\n  eval $(sshelf agent start)\nfi\n```\n\n---\n\n## How sshelf stores data\n\nsshelf keeps all state under `~/.sshelf/`. The only thing it touches in `~/.ssh/` is a clearly delimited block inside `~/.ssh/config`.\n\n| Path | Contents | Permissions |\n|---|---|---|\n| `~/.sshelf/` | Root config directory | `0700` |\n| `~/.sshelf/profiles.toml` | All profile definitions | `0600` |\n| `~/.sshelf/hosts.toml` | Host alias definitions | `0600` |\n| `~/.sshelf/keys/` | Managed key pairs | `0700` |\n| `~/.sshelf/keys/\u003cname\u003e` | Private key | `0600` |\n| `~/.sshelf/keys/\u003cname\u003e.pub` | Public key | `0644` |\n| `~/.sshelf/keys-backup-\u003cdate\u003e.vault` | AES-256-GCM encrypted key backup | `0600` |\n| `~/.sshelf/agent.env` | Agent PID + socket path (runtime) | `0600` |\n| `~/.ssh/config` | sshelf block only — everything else untouched | system |\n\n### The managed SSH config block\n\n```\n# (your own entries above — sshelf never touches these)\n\n# BEGIN sshelf-managed — do not edit this block manually\nHost github-work\n  HostName github.com\n  User git\n  IdentityFile ~/.sshelf/keys/work_ed25519\n  IdentitiesOnly yes\n\nHost github-personal\n  HostName github.com\n  User git\n  IdentityFile ~/.sshelf/keys/personal_ed25519\n  IdentitiesOnly yes\n\nHost staging\n  HostName 203.0.113.42\n  User ubuntu\n  IdentityFile ~/.sshelf/keys/staging.pem\n# END sshelf-managed\n\n# (your own entries below — also untouched)\n```\n\n`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).\n\n---\n\n## Key rotation\n\nSSH keys age. `sshelf key rotate` makes rotation painless:\n\n```bash\nsshelf key rotate work_ed25519\n```\n\nWhat happens:\n\n1. A fresh ed25519 key pair is generated at the same path.\n2. The old key is moved to `work_ed25519.bak` — not deleted yet.\n3. The SSH config block updates immediately to the new key.\n4. The new public key is printed for you to add to GitHub/GitLab.\n\nOnce you've added the new key to all platforms:\n\n```bash\nsshelf key remove work_ed25519.bak\n```\n\n`sshelf doctor` warns you when any managed key is older than 1 year.\n\n---\n\n## Jump hosts (bastion servers)\n\nMany companies route internal server access through a bastion host. sshelf handles this natively.\n\n**Setup:**\n\n```bash\n# Add the bastion first\nsshelf host add --name bastion --hostname bastion.acme.io --user alex --profile work\n\n# Add the internal app server\nsshelf host add --name prod-app --hostname 10.0.1.50 --user ubuntu --profile work\n\n# Link prod-app through bastion\nsshelf host jump prod-app --via bastion\n```\n\nsshelf generates this in `~/.ssh/config`:\n\n```\nHost bastion\n  HostName bastion.acme.io\n  User alex\n  IdentityFile ~/.sshelf/keys/work_ed25519\n\nHost prod-app\n  HostName 10.0.1.50\n  User ubuntu\n  IdentityFile ~/.sshelf/keys/work_ed25519\n  ProxyJump bastion\n```\n\nConnect through the bastion in one command:\n\n```bash\nssh prod-app\n# or\nsshelf host connect prod-app\n```\n\n---\n\n## Doctor checks reference\n\n| Check | ID | Severity | Auto-fixable | What it checks |\n|---|---|---|---|---|\n| Config directory | `config-dir` | warn | yes | `~/.sshelf/` exists with `0700` permissions |\n| Key permissions | `permissions` | fail | yes | Every private key is `0600`, public keys `0644` |\n| Agent running | `agent` | warn | no | ssh-agent process is alive and socket reachable |\n| Active profile | `active-profile` | warn | no | At least one profile is marked active |\n| Key age | `key-age` | warn | no | No managed key older than 1 year |\n| Orphaned keys | `orphaned-keys` | warn | yes | No key file exists without a profile referencing it |\n\n---\n\n## Security\n\n- **Private keys** are stored at `0600`. `sshelf doctor` flags any drift and can fix it automatically.\n- **No network calls.** sshelf never transmits your keys or config to any remote service.\n- **Atomic writes.** Every config and key write uses temp-file → `os.Rename`. A crash mid-write cannot corrupt your config.\n- **`IdentitiesOnly yes`** on every generated entry prevents SSH from offering the wrong key to hosts where you have multiple accounts.\n- **Key generation** delegates to the system `ssh-keygen` binary. sshelf does not implement cryptography for key generation.\n- **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.\n- **Destructive operations require confirmation.** Pass `--yes` only in scripts where you control the input.\n- **sshelf never reads private key content.** It manages paths, metadata, and permissions only.\n\n---\n\n## Building from source\n\nRequires **Go 1.23+** and the system `ssh-keygen` / `ssh-agent` binaries.\n\n```bash\ngit clone https://github.com/ajf1016/sshelf.git\ncd sshelf\n\n# Run tests with race detector\ngo test -race ./...\n\n# Development build\ngo build -o sshelf ./cmd/sshelf\nsudo mv sshelf /usr/local/bin/\n\n# Release build — strip debug info, inject version\ngo build \\\n  -trimpath \\\n  -ldflags=\"-s -w -X github.com/ajf1016/sshelf/internal/config.AppVersion=v0.1.0\" \\\n  -o sshelf \\\n  ./cmd/sshelf\n```\n\n### Cross-compile\n\n```bash\n# Linux amd64\nGOOS=linux GOARCH=amd64 CGO_ENABLED=0 \\\n  go build -trimpath -o dist/sshelf-linux-amd64 ./cmd/sshelf\n\n# macOS arm64\nGOOS=darwin GOARCH=arm64 CGO_ENABLED=0 \\\n  go build -trimpath -o dist/sshelf-darwin-arm64 ./cmd/sshelf\n```\n\n---\n\n## Contributing\n\nContributions are welcome. Before opening a PR:\n\n1. **Run the test suite** with the race detector:\n   ```bash\n   go test -race ./...\n   ```\n\n2. **Run the linter:**\n   ```bash\n   go vet ./...\n   golangci-lint run\n   ```\n\n3. **Keep commands thin.** Business logic belongs in `internal/core/`, not `internal/commands/`. Commands parse flags, call core services, and format output — nothing more.\n\n4. **Tests for core logic are required.** Packages under `internal/core/` need test coverage. Command files don't.\n\n5. **No real credentials in tests.** Use `t.TempDir()` for all paths; never commit hostnames, IPs, emails, or key material.\n\n### Project layout\n\n```\nsshelf/\n├── cmd/sshelf/         # binary entry point — main.go only\n├── internal/\n│   ├── commands/       # cobra command wrappers — thin, no business logic\n│   ├── config/         # constants, permission bits, default paths\n│   ├── core/           # all business logic: profiles, keys, hosts, agent, vault, doctor\n│   ├── platform/       # OS-specific helpers (clipboard)\n│   ├── ui/             # lipgloss styles, table renderer, formatting helpers\n│   ├── utils/          # atomic file I/O, typed errors\n│   └── wizard/         # interactive huh forms for init and import flows\n```\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n---\n\n*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).*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fajf1016%2Fsshelf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fajf1016%2Fsshelf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fajf1016%2Fsshelf/lists"}