Skip to content

Repository files navigation

better-ssh

Website Tests codecov Python 3.12+ License: MIT

A command-line tool for managing SSH connections with an interactive interface, password management, and optional encryption.

Table of Contents

Overview

better-ssh simplifies SSH connection management by providing an interactive terminal interface for selecting and connecting to servers. It supports password storage with optional SSH key-based encryption, automatic password clipboard integration, and works across Windows, macOS, and Linux.

Features

  • Interactive server selection menu with search capabilities
  • Pinned favorites that stay above recent history
  • Interactive menus prioritize recently used servers automatically
  • Import existing hosts from ~/.ssh/config
  • Password storage with optional SSH key-based encryption
  • Automatic password clipboard integration
  • Support for SSH private key and certificate authentication
  • ProxyJump support — connect through a bastion host (or a chain of hosts)
  • Optional SSH keep-alive per server (ServerAliveInterval)
  • Per-server port forwarding presets (local -L, remote -R, dynamic SOCKS -D)
  • Optional X11 forwarding per server (ssh -X)
  • Per-server environment variables pushed to the remote session (ssh -o SetEnv)
  • Pre- and post-connect shell hooks per server (VPN setup, cleanup, notifications)
  • File transfer (bssh put / bssh get) that reuses a server's stored profile
  • Round-trip with OpenSSH config: bssh isc imports Host blocks; bssh esc exports them back
  • Parallel command broadcast (bssh exec) across matched servers with per-host colored output
  • Free-form notes and tags attached to each server
  • Detailed per-server card view (bssh view <name>)
  • Time-sorted "recents" list (bssh recent) with a relative Last used column
  • Server management (add, edit, remove, list)
  • bssh ls <query> filter by name, host, user, tag, id prefix, or jump host
  • Server availability checking (ping individual or health check all)
  • Configuration backup and restore (export/import)
  • Cross-platform compatibility (Windows, macOS, Linux)
  • Short command aliases for faster workflow
  • Flexible server identification (by name, partial name, or ID)
  • Non-interactive CLI flags for scripted provisioning
  • Auto-detection of SSH keys in standard locations

Installation

Prerequisites

  • Python 3.12 or higher
  • uv package manager
  • SSH client (OpenSSH)

Steps

  1. Clone the repository:
git clone https://github.com/aa-blinov/better-ssh.git
cd better-ssh
  1. Install as a global tool:
uv tool install .
  1. Verify installation:
bssh --help

Both bssh and better-ssh are registered as entry points and work identically.

For development instead of a global install:

uv sync
uv run bssh --help

SSH Client Installation

The tool requires a system SSH client.

Windows:

# Via Windows Settings
Settings -> Apps -> Optional Features -> OpenSSH Client

# Via winget
winget install --id Microsoft.OpenSSH.Client -e

macOS:

SSH client is pre-installed. If needed:

brew install openssh

Linux:

# Ubuntu/Debian
sudo apt install openssh-client

# Fedora/RHEL
sudo dnf install openssh-clients

# Arch Linux
sudo pacman -S openssh

Shell Completion

bssh --install-completion   # install for current shell
bssh --show-completion      # preview without installing

Usage

Quick Start

bssh
bssh <query>
bssh import-ssh-config
  • bssh opens the interactive connect menu immediately
  • bssh <query> connects directly when the match is unique
  • bssh import-ssh-config bootstraps your saved hosts from ~/.ssh/config

Available Commands

Usage: bssh [OPTIONS] COMMAND [ARGS]...

Better SSH: quick server selection, connection and password management.

Commands:
  add                 Add a new server.               Alias: a
  connect             Connect to a server.            Alias: c
  copy-pass           Copy password to clipboard.     Alias: cp
  decrypt             Disable password encryption.    Alias: dec
  edit                Edit a server.                  Alias: e
  encrypt             Enable password encryption.     Alias: enc
  encryption-status   Show encryption status.         Alias: es
  exec                Run a command on one or more servers in parallel.
  export              Export servers to JSON file.    Alias: ex
  export-ssh-config   Export servers to SSH config.   Alias: esc
  get                 Download a remote file/dir (scp).
  health              Check all servers availability. Alias: h
  import              Import servers from JSON file.  Alias: im
  import-ssh-config   Import hosts from SSH config.   Alias: isc
  list                Show list of servers.           Alias: ls
  pin                 Pin a server to the top of lists.
  ping                Check server availability.      Alias: p
  put                 Upload a local file/dir (scp).
  recent              Show recently used servers.     Alias: r
  remove              Remove a server.                Alias: rm
  sftp                Open an interactive SFTP session.
  show-pass           Show password.                  Alias: sp
  unpin               Remove a server from pinned favorites.
  view                Show a detailed card for a server. Alias: v

Run bssh without a subcommand to open the interactive connect menu immediately.

Run bssh <query> to connect directly when the match is unique. If the query is ambiguous or missing, the tool falls back to an interactive menu.

Use bssh pin <query> to keep critical hosts above the normal recent/frequent ordering, and bssh unpin <query> to remove them from favorites.

bssh recent (alias r) lists servers by most-recently-connected, newest first — independent of pin status. Cap the list with --limit N / -n N (default 10). Servers that were never connected via bssh don't appear. The rendered table includes a Last used column with a relative timestamp (just now, 5m ago, 2h ago, 3d ago, or an ISO date for anything older than 30 days). The same column is shown in bssh ls once at least one server has a recorded connection.

Most commands work without arguments and will present an interactive menu.

bssh add walks through the optional fields after Name/Host/Port/Username. Binary/complex fields (SSH key, password, jump host, keep-alive, forwards, env vars, pre/post hooks) use a y/N confirm before asking for the value. Free-text fields (note, tags) use a direct prompt with "(Enter to skip)" — a one-keystroke skip if you don't want one, or just type the value. All of these also have non-interactive flags for scripted use (see Adding Servers Non-Interactively).

For password-based hosts, bssh copies the saved password to your clipboard before starting OpenSSH. You still paste it into the SSH password prompt manually; the password is not injected into the ssh command automatically.

For detailed help on any command, use --help:

bssh connect --help
bssh add --help

Filtering the Server List

bssh ls accepts an optional query that filters the table by substring against name, host, username, tags, jump host, and id prefix (all case-insensitive except the id prefix):

bssh ls prod          # matches name/host/user/tag/jump containing "prod"
bssh ls bastion       # surfaces both the bastion itself and everything routed through it
bssh ls a1b2          # id-prefix match

When nothing matches, the command prints a single friendly line instead of an empty table. Omitting the query lists everything as before.

Adding Servers Non-Interactively

Every field bssh add prompts for has an equivalent flag, so the full flow can be scripted. Pair with -s / --skip to suppress the "Add SSH key?", "Add password?", "Use a jump host?" etc. confirms — any field not passed via flag stays unset.

# Minimal non-interactive add:
bssh add --name prod-db --host 10.0.0.5 --username deploy -s

# With the full profile populated via flags:
bssh add \
  --name prod-db \
  --host 10.0.0.5 \
  --port 22 \
  --username deploy \
  --key ~/.ssh/prod_ed25519 \
  --certificate ~/.ssh/prod_ed25519-cert.pub \
  --jump bastion \
  --keep-alive 60 \
  --notes "main postgres" \
  -t prod -t db \
  -s

Without -s, bssh add drops into the interactive confirm sequence for any field you didn't pre-populate, which is friendly for humans but inconvenient for Ansible/Terraform-style runs.

Flag reference:

Flag Short Purpose
--key <path> Path to SSH private key
--certificate <path> Path to SSH certificate
--password <value> Password (see security note below)
--jump <name> -J Saved server name to use as ProxyJump (case-insensitive)
--keep-alive <seconds> -K ServerAliveInterval in seconds; 0 leaves it disabled
--notes <text> Free-form note attached to the server
--tag <value> -t Tag (repeatable: -t prod -t db)
--local-forward <spec> -L Local forward, repeatable: [bind:]port:host:port
--remote-forward <spec> -R Remote forward, repeatable: [bind:]port:host:port
--dynamic-forward <spec> -D Dynamic SOCKS forward, repeatable: [bind:]port
--x11 Enable X11 forwarding (ssh -X)
--env <K=V> -e Environment variable pushed via ssh -o SetEnv (repeatable)
--pre <cmd> Shell command run locally BEFORE each connect (empty string clears)
--post <cmd> Shell command run locally AFTER each connect (empty string clears)
--skip -s Skip all interactive "Add X?" confirms (provisioning / scripts)

Passing an empty string (--key "", --notes "") stores None — useful when a script wants to be explicit about clearing a field.

--password security note: values passed on the command line end up in shell history (e.g. ~/.bash_history), /proc/<pid>/cmdline, and process-listing tools. Prefer the interactive prompt when possible. The flag exists for provisioning scripts that source the password from a secure secret store at invocation time.

The same flag set is also available on bssh edit, plus --name, --host, --port, and --username for in-place field updates. By default, flags you pass are applied and everything else still goes through the interactive prompts, so mixing is fine:

# Apply the listed fields, interactively review the rest
bssh edit prod --keep-alive 60 --notes "updated" -t prod -t eu

# Update one field, then interactively review the rest
bssh edit prod --host 10.0.0.9

Pair any flag with -s / --skip to bypass the interactive review entirely — only the flags you pass are applied, everything else is preserved verbatim:

# Clear just the pre-connect hook, no prompts
bssh edit prod --no-pre -s

# Update only the host, no prompts
bssh edit prod --host 10.0.0.9 -s

Empty-string clearing works here too: --jump "" drops the ProxyJump, --notes "" wipes the note, etc. The dedicated --no-pre / --no-post / --no-env / --no-forwards flags do the same thing without having to quote an empty string (useful on PowerShell, where --pre "" gets eaten before reaching the CLI).

Boolean toggles use Typer's --x11/--no-x11 syntax on bssh edit: pass --x11 to enable, --no-x11 to disable, or omit both to keep the current value.

Server Notes, Tags, and Keep-Alive

Four optional per-server fields surface as columns in the ls / recent tables when at least one server has them set:

  • Notes — free-form text attached to a server (Notes column, truncated at 40 chars). Edit with bssh edit or set at creation time with --notes.
  • Tags — small labels (e.g. prod, db, eu-west) used to organize and filter the list (Tags column). Tags are comma-separated in the interactive prompt or passed repeatedly as -t prod -t db. bssh ls <query> matches on tags, so bssh ls prod pulls every server labeled that way.
  • Keep-Alive — a ServerAliveInterval value in seconds. When set, bssh connect passes -o ServerAliveInterval=<N> -o ServerAliveCountMax=3 to OpenSSH, preventing NAT/idle disconnects (Alive column, e.g. 60s). Enter 0 at the prompt (or --keep-alive 0) to leave it disabled.
  • Last used — a relative timestamp (5m ago, 2h ago, 3d ago, or an ISO date for older) automatically recorded on every successful bssh connect. Surfaces in both ls and recent so you can see freshness at a glance.

All four columns are auto-hidden when no server has a value set.

Port Forwarding

bssh stores per-server port-forward presets and passes them to OpenSSH automatically on every bssh connect. Three forwarding types are supported, matching OpenSSH flags:

Type Flag Example Meaning
Local -L -L 5432:localhost:5432 Forward local port 5432 to localhost:5432 on the remote side (typical: tunnel a remote DB to your laptop).
Remote -R -R 9000:internal:9000 Forward remote port 9000 back to internal:9000 on your network (typical: expose a local service to the remote host).
Dynamic -D -D 1080 Open a SOCKS proxy on local port 1080 that routes through the server.

All three accept an optional bind address (e.g. 127.0.0.1:5432:localhost:5432).

Non-interactive provisioning:

bssh add --name prod-db --host 10.0.0.5 --username deploy \
  -L 5432:localhost:5432 \
  -L 6379:redis-internal:6379 \
  -D 1080

Interactive flow: bssh add and bssh edit ask "Configure port forwards?" — picking yes opens a small loop where you choose local/remote/dynamic and enter a spec, repeating until you pick (done).

Editing an existing server's forwards:

  • Passing any -L/-R/-D flag to bssh edit replaces the entire forwards list (consistent with --tag).
  • bssh edit <name> --no-forwards clears all forwards.
  • Passing no forward-related flags keeps the existing list and opens the interactive prompt when no other flags trigger non-interactive mode.

bssh ls shows a Fwd column with the count (hidden when nobody has any). bssh view <name> lists each forward on its own line.

File Transfer (put / get)

bssh put uploads and bssh get downloads, using scp under the hood and composing with the server's saved profile (port, key, certificate, jump host, keep-alive).

# Upload
bssh put prod-db ./backup.sql /var/backups/backup.sql
bssh put prod-db ./releases -r -C              # directory with compression
bssh put prod-db ./archive.tar.gz /var/backups/ -p   # keep mtime/atime/mode

# Download
bssh get prod-db /var/log/app.log ./app.log
bssh get prod-db /etc/nginx -r ./nginx-snapshot
bssh get prod-db /var/snapshots/data.bin ./data.bin -p   # preserve timestamps

Flag reference:

Flag Short Purpose
--recursive -r Recurse into directories (scp -r)
--compress -C Enable scp compression
--preserve -p Preserve modification times, access times, and modes (scp -p)

Notes:

  • Port forwards (-L/-R/-D) and X11 on the server profile are not applied to transfers — they're connection-only settings.
  • Password authentication prompts come from OpenSSH itself; bssh's clipboard copy is only wired into bssh connect and bssh sftp, not put/get.
  • ProxyJump uses the server's stored jump_host chain, same as connect.
  • Requires scp on PATH (shipped with OpenSSH client tools). Missing binary -> exit code 127.

Interactive SFTP Session

bssh put / bssh get work great when you already know the remote path. When you don't — you want to browse, list, grab a few files — drop into a full SFTP session via bssh sftp:

bssh sftp                  # pick from the interactive menu
bssh sftp prod-db          # go straight to this server
bssh sftp prod-db --no-copy  # skip the password-to-clipboard copy

Then you're at the sftp> prompt:

sftp> cd /var/log
sftp> ls -la
sftp> get app.log
sftp> put ./local-patch.sh /tmp/
sftp> bye

The session reuses the server's full profile — port, key, certificate, jump-host chain, keep-alive — and honors the same pre / post-connect hooks bssh connect does. So a VPN-up hook, an aws sso login refresh, or an sshfs mount that you configured for a server runs here too. Forwards / X11 / SetEnv are connection-oriented features and don't apply to sftp.

Requires the sftp binary on PATH (shipped with the OpenSSH client package). Missing binary exits with rc=127.

Pre- and Post-Connect Hooks

Two optional per-server shell commands that run locally around every bssh connect. Typical use cases: bringing up a corporate VPN, refreshing an AWS SSO token, mounting sshfs, posting to Slack, or running audit logging.

# Bring a VPN up before each connect, tear it down after:
bssh edit prod-db --pre "openvpn-connect corp" --post "openvpn-disconnect corp"

# Refresh SSO credentials before each connect:
bssh edit prod --pre "aws sso login --profile prod"

# Mount a remote filesystem alongside the interactive session:
bssh edit dev --pre "sshfs dev:/srv ~/remote-dev" --post "fusermount -u ~/remote-dev"

# Clear both:
bssh edit prod --no-pre --no-post          # explicit clear
bssh edit prod --pre "" --post ""          # empty-string form works too

Semantics:

  • Pre runs before ssh. It is a prerequisite: if the hook exits non-zero, bssh connect aborts with the hook's exit code and does not attempt ssh. The post hook is not run in this case.
  • Post always runs once pre has succeeded (or no pre was set) — after a successful ssh session, after Ctrl+C (rc 130), after an auth failure, and after an upfront config error like a broken jump-host reference where ssh was never launched. This is the place for cleanup and the "setup ran ⇒ teardown runs" contract holds even when ssh itself never starts. A non-zero post exit is reported as a yellow warning but does not override the final exit code.
  • Commands execute through the platform shell (sh -c on POSIX, cmd.exe /c on Windows). Pipes, redirects, and environment-variable expansion all work.
  • Hooks only apply to bssh connect (and the interactive bssh <query> shortcut). bssh exec, bssh put, bssh get intentionally do not run hooks — they're non-interactive flows where running hooks per-host would be surprising.

Security note: the commands run on your local machine with your shell and your privileges. A servers.json imported from an untrusted source could ship a malicious pre/post hook; audit any imported config before letting it touch your machine.

Environment Variables

Per-server environment variables pushed to every bssh connect via OpenSSH's SetEnv directive. Useful for fixing LANG / LC_ALL mismatches, flagging debug mode (DEBUG=1), injecting project / cluster context (DEPLOY_ENV=prod), etc.

bssh add --name prod --host h --username u \
  -e LANG=en_US.UTF-8 \
  -e DEPLOY_ENV=prod \
  -s

bssh edit prod --env LANG=en_US.UTF-8  # replaces the whole env dict
bssh edit prod --no-env                # clears all env vars
bssh view prod                         # shows "Environment: LANG=..., DEPLOY_ENV=..."

Notes:

  • Each pair becomes a separate -o SetEnv=KEY=VALUE on the ssh command line.
  • SetEnv (OpenSSH 7.8+) pushes a literal value and does not require AcceptEnv on the remote sshd. Older servers silently drop unknown env vars.
  • Passing any --env to bssh edit replaces the stored dict (symmetric with --tag). Use --no-env to clear.
  • Empty values are allowed (--env FOO=), useful for unsetting an inherited variable pattern. Values may contain = (partitioned on the first = only).

X11 Forwarding

Set x11_forwarding on a server to have bssh connect pass -X to OpenSSH. GUI apps launched on the remote side will try to render through a local X11 display.

bssh add --name workstation --host ws.example --username dev --x11
bssh edit workstation --x11       # turn on
bssh edit workstation --no-x11    # turn off
bssh view workstation             # shows "X11: enabled (ssh -X)" row when on

Prerequisites on the client side:

  • Linux — an X11 display is usually already running; nothing extra to install.
  • macOS — install XQuartz and log out/in once so $DISPLAY is set.
  • Windows — install an X server: VcXsrv, Xming, or use WSLg (Windows 11). Without one, OpenSSH will log X11 forwarding request failed and GUI apps will fail with Can't open display.

-X vs -Y: bssh emits -X (untrusted mode) which is the safer default. Some X11 apps — older xterm, certain Matplotlib backends — fail under the X11 SECURITY extension and need trusted mode (-Y). If you hit BadAccess / SECURITY errors, enable ForwardX11Trusted yes for that host in ~/.ssh/configbssh's -X composes with your SSH config, so the directive takes effect.

Viewing a Single Server

bssh view <name> (alias v) renders a full-fidelity card for one server in a Rich panel. Unlike ls, nothing gets truncated — useful when you want to see the complete notes, the resolved jump chain (user@bastion:port → target-name), and the list of other servers that reference this one as a jump host.

bssh view prod
bssh v prod-db

The view also flags a broken jump-host reference in red instead of failing silently, so you can spot stale ProxyJump targets before a connection attempt.

Importing From SSH Config

Import hosts from your OpenSSH config:

bssh isc

Or import from a custom path:

bssh isc ~/.ssh/work-config

The importer resolves each host through ssh -G, so Host *, Include, explicit IdentityFile, and explicit CertificateFile are reflected in imported entries.

Default OpenSSH keys remain implicit. If a host works with plain ssh host because of default keys or ssh-agent, bssh will keep using that behavior without pinning a key path unless your SSH config explicitly does so.

When servers already exist locally, both bssh import (JSON) and bssh isc (SSH config) ask how to reconcile — keep existing + add new (merge) or wipe and replace (replace). Flags let you pick in scripts:

# Interactive: mode picker + explicit safety confirm naming the counts
bssh import backup.json
bssh isc

# Non-interactive: pre-pick the mode
bssh import backup.json --merge
bssh isc --replace

# CI / scripted (skip the safety prompt entirely):
bssh import backup.json --replace --yes    # WIPES everything and imports
bssh isc --merge -y

The safety confirmation spells out the destructive action with counts: DELETE all 12 existing server(s) and import 3 from 'backup.json'? — so a reflex y doesn't silently wipe the store. Passing --merge and --replace together is rejected (rc=2).

Parallel Command Broadcast

bssh exec runs a single shell command on every server matched by a query, in parallel, with a per-host colored prefix on each output line and an aggregated summary at the end.

bssh exec "uptime" prod           # every server matched by the 'prod' query
bssh exec "df -h /" --all         # every saved server
bssh exec "systemctl is-active nginx" web --timeout 15

The query matches the same fields as bssh ls / shorthand connect: name, host, username, tag, id prefix, or jump host. Use --all to skip the filter.

Flag reference:

Flag Purpose Default
--all Run on every saved server instead of filtering off
--timeout <seconds> Per-host overall timeout (remote command is killed after) 30
--connect-timeout <seconds> TCP connect timeout (ssh -o ConnectTimeout) 10

Each host's stdout lines are tagged with [<name>] in its own color, stderr lines are red, and a (<duration>, ok) or (<duration>, exit N) status line closes each host's output. The summary reports how many hosts succeeded, e.g. Summary: 8/10 ok in 2.3s. Exit code is 0 only when every host returned 0 without a connection-level error.

Known caveats:

  • Runs use ssh -o BatchMode=yes to avoid interleaved password prompts from parallel runs. Password-only servers will fail fast with a clear auth error — use key or certificate auth (or ssh-agent) for exec.
  • Port forwards and X11 forwarding on the server profile are ignored here (they're interactive-connection features).
  • No concurrency cap in v1; every matched server gets its own ssh process. Fine up to low hundreds of hosts.

Exporting Back to SSH Config

The reverse operation — turn every saved server into Host blocks that native OpenSSH tooling (ssh/scp/rsync/git/IDEs) can read directly:

bssh export-ssh-config ~/.ssh/config.bssh
# or the short alias:
bssh esc ~/.ssh/config.d/bssh.conf

Each server becomes a Host <name> block with every field that has an ssh_config equivalent:

  • HostName, User, non-default Port
  • IdentityFile, CertificateFile
  • ProxyJump
  • ServerAliveInterval + ServerAliveCountMax 3 (when keep-alive is set)
  • ForwardX11 yes (when X11 is enabled)
  • LocalForward / RemoteForward / DynamicForward (one line per port forward, bind host preserved)

Fields that have no native ssh_config syntax (password, tags, notes) are written as leading comments so the context survives for a human reader.

After exporting, activate the file by adding

Include ~/.ssh/config.bssh

to your ~/.ssh/config, or pass -F ~/.ssh/config.bssh directly to ssh. Overwriting an existing file prompts for confirmation — pass -f / --force (or use bssh export -f for the JSON variant) to skip the prompt for scripts/CI. Exit code is 0 on success, 1 when the user declines the overwrite prompt, so pipelines can tell whether the file was actually written.

Server Identification

Every command that takes a <query> argument (connect, edit, view, remove, pin, unpin, ping, put, get, copy-pass, show-pass, ls, exec, health) resolves it the same way. Match order for a single-target command:

  1. Exact server ID
  2. Unique server-ID prefix
  3. Exact name (case-insensitive)
  4. Unique broad match — case-insensitive substring in name, host, username, any tag, or jump host

If step 4 finds more than one server, the command drops into an interactive picker over the matching candidates (same set bssh ls <query> would print). If nothing matches at all, the command prints a "not found" error.

bssh ls <query> and bssh exec <query> print / act on every matching server directly, so they skip the picker step; the match fields are the same.

Names are unique (case-insensitive): adding or renaming to a name already in use is rejected with an error pointing at the existing entry.

Jump Hosts (ProxyJump)

bssh supports connecting through one or more bastion hosts using OpenSSH's ProxyJump (-J). A jump host is just another saved server referenced by name.

Setting a jump host

During bssh add, after the basic fields, the tool asks:

Use a jump host (ProxyJump)? [y/N]

Confirming opens a picker over your other saved servers. The picker marks the current selection and shows how many other servers already use each candidate as a jump host.

For non-interactive use (scripts, automation), pass the name directly:

bssh add --name prod --host prod.example --username deploy --jump bastion
# short form
bssh add --name prod --host prod.example --username deploy -J bastion

Reference matching is case-insensitive; the canonical casing from the saved server is stored.

Editing and removal

  • bssh edit always shows "Change jump host?" when one is set, opening the picker with a (none — direct connection) option first.
  • Renaming a server used as a jump host by others automatically updates all referencing servers in one save.
  • bssh rm warns when the target is used as a jump host by others and offers to clear jump_host on those dependents (default yes). Declining aborts the removal entirely.
  • Cycles (A → B → A) and unknown references are rejected at save time, not silently accepted.

Importing from ~/.ssh/config

bssh isc reads the ProxyJump directive and sets jump_host when the referenced target matches another imported alias (case-insensitive).

Known limitations

  • Bastion auth is not forwarded into ssh -J. We pass only user@host:port for each hop; OpenSSH resolves credentials for the bastion through its own mechanisms (~/.ssh/config, ssh-agent, default keys). A key_path or password saved on the bastion entry in bssh is not used during a jump connection — if the bastion needs a specific key, declare it via IdentityFile in ~/.ssh/config or add it to ssh-agent. This is a limitation of ssh -J itself, not bssh.
  • Password clipboard covers only the target, not the bastion. You'll be prompted for the bastion's password separately during connection.
  • Multi-hop ProxyJump h1,h2 and inline user@host:port specs in ~/.ssh/config are skipped during import. Only single-hop alias references to other imported hosts are preserved. Set multi-hop chains manually by adding each bastion as its own server and chaining jump_host references.

Configuration

Configuration files are stored in platform-specific directories:

  • Windows: %LOCALAPPDATA%\better-ssh\
  • macOS: ~/Library/Application Support/better-ssh/
  • Linux: ~/.config/better-ssh/

Configuration Files

  • servers.json — server configurations and encrypted passwords
  • settings.json — application settings (encryption status, key source, salt)

Password Encryption

By default, passwords are stored in plaintext. The application offers optional encryption using your SSH private key as the encryption key source.

Managing Encryption

bssh es            # check encryption status
bssh enc           # enable encryption (interactive)
bssh dec           # disable encryption (interactive)

# For scripts / CI — skip the "are you sure?" prompt:
bssh enc -y
bssh dec -y

When exporting servers, you can choose to export passwords in plaintext or encrypted format through an interactive prompt.

How It Works

The encryption system uses your SSH private key (~/.ssh/id_ed25519 or id_rsa) to derive an encryption key via PBKDF2-HMAC-SHA256 with 100,000 iterations and a random per-installation salt stored in settings.json. Passwords are encrypted using Fernet (symmetric encryption) and stored in base64 format.

Important Considerations

  • Key Dependency: If you delete or modify your SSH key, encrypted passwords become inaccessible
  • Machine Specific: Decryption requires the same SSH key and salt on the same machine
  • Backup Recommended: Back up your SSH key and settings.json before enabling encryption
  • Automatic Operation: Passwords are automatically encrypted on save and decrypted on load

Security Properties

  • Passwords remain protected if the servers.json file is compromised
  • No master password required for daily use
  • SSH key protected by operating system file permissions
  • Random per-installation salt prevents precomputed key attacks

Platform Support

  • Windows 10/11
  • macOS 10.15+
  • Linux (any distribution with Python 3.12+)

All Python dependencies are cross-platform. The only external requirement is a system SSH client, which is typically pre-installed on macOS and Linux.

Contributing

Contributions are welcome. Please follow these guidelines:

Reporting Issues

  • Check existing issues before creating a new one
  • Include your operating system and Python version
  • Provide steps to reproduce the problem
  • Include relevant error messages or logs

Development Setup

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/aa-blinov/better-ssh.git
  3. Create a virtual environment: uv sync
  4. Create a feature branch: git checkout -b feature-name
  5. Make your changes

Code Standards

  • Follow PEP 8 style guidelines
  • Use type hints for function signatures
  • Write docstrings for public functions and classes
  • Run linting: uv run ruff check app tests
  • Format code: uv run ruff format app tests
  • Ensure all checks pass before submitting

Testing

uv run pytest                                    # run all tests
uv run pytest -v                                 # verbose output
uv run pytest tests/test_models.py              # specific file
uv run pytest --cov=app --cov-report=html       # with coverage report

Test Structure:

  • tests/test_models.py — Server model tests
  • tests/test_domain.py — Pure domain helpers (tag parsing, name uniqueness, jump-chain validation, sort/filter)
  • tests/test_encryption.py — Encryption/decryption tests
  • tests/test_storage.py — Configuration persistence tests
  • tests/test_ssh.py — SSH command and availability tests
  • tests/test_ssh_config.py — SSH config importer tests
  • tests/test_transfer.py — put / get scp wrapper tests
  • tests/test_exec_cmd.py — parallel broadcast command tests
  • tests/test_cli.py — CLI commands and interface tests

Source Layout:

app/
  models.py         Pydantic Server model
  domain.py         Pure logic (sort/filter, validation, tag parsing, jump-chain walking)
  storage.py        JSON file I/O with transparent encryption layering
  encryption.py     Fernet + PBKDF2 key derivation from an SSH key
  ssh.py            OpenSSH command construction, jump resolution, availability check
  ssh_config.py     ~/.ssh/config importer via `ssh -G`
  cli/
    __init__.py     Package entry: builds the Typer app, re-exports for tests
    _shared.py      Typer app instance, Console, pickers, common helpers
    connection.py   connect, copy-pass, show-pass + root callback
    manage.py       add, edit, remove, view
    organize.py     list, pin, unpin
    crypto.py       encrypt, decrypt, encryption-status
    backup.py       export, import, import-ssh-config, export-ssh-config
    health.py       ping, health
    transfer.py     put, get (scp wrappers)
    exec_cmd.py     exec (parallel broadcast)

Submitting Changes

  1. Ensure all tests pass: uv run pytest
  2. Run linting: uv run ruff check app tests
  3. Format code: uv run ruff format app tests
  4. Commit your changes with clear, descriptive messages
  5. Push to your fork
  6. Submit a pull request with a description of your changes
  7. Ensure all CI checks pass

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

A friendlier layer over OpenSSH: interactive picker, encrypted password clipboard, jump chains, parallel exec, SSH config round-trip.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages