Skip to content

hex-wave-ai/ssh-profile-tool

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sshprofile

License: MIT Python

A small, safe CLI for managing SSH keys and developer profiles (key + git identity + ~/.ssh/config host blocks). Built for people who juggle work / personal / client identities and keep mixing up which key and which git user.email is active.

Switching contexts usually means changing three things at once — the SSH key, the git user.name/user.email, and the relevant Host entry in ~/.ssh/config. sshprofile treats those as a single switchable unit: a profile.

The tool never reads, prints, copies, or logs private key contents. It deals in paths and manages config — nothing more.

Requirements

  • Python 3.11+ (uses the stdlib tomllib to read TOML)
  • ssh-add / ssh-agent and git on PATH
  • macOS or Linux — the tool relies on POSIX file permissions (600/700) and the OpenSSH / git CLIs. Windows is not supported.

Install

Install as an isolated CLI with pipx:

# From the repository
pipx install --python python3.11 git+https://github.com/hex-wave-ai/ssh-profile-tool

# …or from a local clone
pipx install --python python3.11 .

--python is worth specifying explicitly so pipx doesn't pick up an older system interpreter.

For development:

python3.11 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest

Shell completion

Powered by Typer:

sshprofile --install-completion   # install for your shell
sshprofile --show-completion      # print it to inspect/customize

Commands

Command Description
sshprofile list List all known profiles and show which is active
sshprofile add Add a new profile (interactive or via flags)
sshprofile switch <name> Activate a profile: load its key into the agent, set git identity, update config
sshprofile show <name> Show details of one profile
sshprofile edit <name> Edit fields of an existing profile
sshprofile rename <a> <b> Rename a profile
sshprofile remove <name> Remove a profile from the registry (does not delete the key file)
sshprofile current Print the currently active profile
sshprofile keys List private keys found under ~/.ssh (with a safety check)
sshprofile doctor Audit ~/.ssh permissions; --fix tightens them to 600/700

Examples

# Add a profile interactively
sshprofile add

# Add via flags
sshprofile add work \
  --key ~/.ssh/id_ed25519_work \
  --git-name "Jane Dev" \
  --git-email jane@company.com \
  --host github.com-work \
  --hostname github.com   # real HostName the alias resolves to

# See everything
sshprofile list

# Preview a switch without changing anything
sshprofile switch work --dry-run

# Switch context (key + git identity + config)
sshprofile switch work

# Who am I right now?
sshprofile current

# Find loose-permission keys and fix them
sshprofile doctor --fix

What switch actually does

  1. Validates the profile: the key exists, is a regular file with safe permissions, the host alias is a single clean token, and the git identity has both name and email (or neither). Anything that fails aborts before a single change is made.
  2. Adds this profile's key first (ssh-add; on macOS with --apple-use-keychain), and only after that succeeds removes the previously active profile's key (unless --no-remove-old) — so a failed switch never leaves you with no key.
  3. Sets git identity if the profile defines one. By default the global git config user.name/user.email; with --local it sets only the current repo (and refuses if you're not inside one). Applied atomically — if the second field fails to write, the first is rolled back, so you never end up with a half-changed, mixed identity. A profile without an identity leaves git config untouched (switch reports git identity: unchanged).
  4. Only if the profile sets a host: ensures that Host block exists in ~/.ssh/config (in place — never duplicated), after backing the file up. Refuses if the alias shares a Host line with other aliases. A profile with no host never touches ~/.ssh/config at all.
  5. Records the active profile in the state file.

Each step is conditional on the matching field, so the only thing switch always does is load the key into ssh-agent.

--dry-run prints exactly what each of these steps would do — including a diff of the ~/.ssh/config change — and applies nothing.

Configuration & state

  • Config file: ~/.config/sshprofile/profiles.toml
  • State (active profile): ~/.config/sshprofile/state.toml
  • Both created 600, inside a 700 directory.
  • XDG_CONFIG_HOME is honored when set.

Example profiles.toml:

[profiles.work]
key = "~/.ssh/id_ed25519_work"
git_name = "Jane Dev"
git_email = "jane@company.com"
host = "github.com-work"
hostname = "github.com"

[profiles.personal]
key = "~/.ssh/id_ed25519"
git_name = "jane"
git_email = "jane@personal.dev"
host = "github.com"

The managed host block switch writes (or updates in place) looks like:

Host github.com-work
    HostName github.com
    AddKeysToAgent yes
    IdentityFile /home/jane/.ssh/id_ed25519_work
    IdentitiesOnly yes

IdentitiesOnly yes makes ssh offer only this key (not every key in the agent), and HostName (from --hostname) is what the alias actually connects to. Omit --hostname only when host is already a real hostname or an existing Host block you maintain yourself.

Note: clearing host/hostname later (edit … --host "") updates the registry but does not retroactively delete the block already written to ~/.ssh/config — remove or adjust it by hand. (Full managed-block reconciliation is on the roadmap.)

Do you even need host?

host is optional, and most people who already maintain ~/.ssh/config by hand don't need it. SSH selects a key per host statically — that mapping lives in ~/.ssh/config and never has to "switch". There are two common multi-key layouts:

  • Alias per identity (recommended, and likely what you already have): a separate Host block per key —

    Host github.com-work
        IdentityFile ~/.ssh/id_work
    Host github.com-personal
        IdentityFile ~/.ssh/id_personal

    ssh picks the key from whichever alias your git remote uses, so the keys never need switching. Here the only thing that genuinely changes per context is your git author identity — so leave host unset and let switch manage just the git identity (and the agent):

    sshprofile add work --key ~/.ssh/id_work --git-name "Jane" --git-email jane@work.dev
    sshprofile switch work     # changes git identity only; ~/.ssh/config untouched
  • One shared Host, swapped key: a single Host github.com whose active IdentityFile you flip between identities. This is the one case where letting switch rewrite the block (via host) earns its keep.

In short: set host only if you want sshprofile to own a Host block; otherwise omit it and switch won't touch ~/.ssh/config.

Safety guarantees

These are enforced in code and covered by tests:

  • Never touch key contents. The tool works with paths only; ssh-add output is parsed for fingerprints, never key bytes.
  • Validated input. Values destined for ~/.ssh/config are rejected if they contain newlines or control characters, and a host alias must be a single token — so a profile can never inject extra SSH directives.
  • Safe permissions, no surprises. Files the tool creates end up 600, directories it creates 700, and backups are always 600 even if the source was world-readable. It never silently re-permissions a directory that already exists (e.g. your ~/.ssh). It refuses to use a group/world-readable key, and doctor finds and fixes such keys.
  • Backup before any config write. ~/.ssh/config is copied to a timestamped .bak-… and replaced atomically (temp file + os.replace), so a hand-edited config is never silently destroyed.
  • Idempotent config edits. Re-adding an existing Host block updates it in place instead of duplicating; an unchanged switch writes nothing at all.
  • Friendly failures. A missing ssh-add or git produces a clear message, not a stack trace.

Project layout

sshprofile/
├── cli.py          # typer app, command definitions
├── profiles.py     # load/save profiles.toml, state.toml
├── ssh_config.py   # safe parse + write of ~/.ssh/config
├── validate.py     # profile-data validation (control chars, alias, identity)
├── agent.py        # ssh-add wrappers
├── gitident.py     # git config wrappers
└── fsutil.py       # perms checks, atomic write, backups
tests/
├── test_fsutil.py
├── test_profiles.py
├── test_ssh_config.py
├── test_validate.py
├── test_subprocess_wrappers.py
└── test_cli.py

Out of scope (v1)

  • Generating new keys (ssh-keygen).
  • Remote/cloud sync of profiles.
  • GUI.

These and other planned features are tracked in ROADMAP.md.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

5 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages