Skip to main content
Glama

bravoric-ssh-client

A full-screen SSH + tmux TUI client.

I wrote this for myself. It is a personal tool, not a product: I'm publishing it on GitHub mainly to show how I reason and how I build the tools I use every day. It lists your hosts, shows the tmux sessions running on each of them, and lets you attach, create, rename and kill sessions without leaving the terminal. All network operations run in background threads, so the UI always stays responsive. No external services, nothing to install on your servers.

CI Python License: MIT

Italiano? Vedi README.it.md.

About this project

This is a personal tool. I built it to scratch my own itch and I use it every day. I'm sharing it as-is, for reference and curiosity, to show the way I think and how I assemble my own tooling. It is not a supported product: no roadmap, no SLA, no promise of feature parity. Issues and suggestions are welcome, but I only merge changes that fit how I use it.

Related MCP server: 2native-ssh-mcp

Features

  • Host list → tmux sessions, keyboard driven, with live text filter.

  • Host management: add, edit, delete, duplicate, group, per-host credentials.

  • Connectivity: TCP ping on the SSH port, mass ping of all visible hosts.

  • Session management: attach, read-only attach (tmux attach -r), create, rename, kill, kill whole server, details, windows, new window, detach clients.

  • Credential providers: system keyring (GNOME/KDE/macOS/Windows) or a local plain file with 0600 permissions; per-host override (keyring, plain, key, prompt).

  • Import: from ~/.ssh/config and from Remmina profiles, plus a helper to copy passwords from Remmina into the app keyring.

  • File exchange: scp copy and a two-panel Midnight Commander session (local ↔ remote or remote ↔ remote) with automatic password handling.

  • SSH tunnels: -L / -R / -D port forwards, started in the background and shared between the TUI and the MCP server.

  • Jump host / bastion (-J / ProxyJump) with a multi-host askpass helper.

  • Broadcast snippets: run a command on many hosts at once, either in a new detached tmux session per host or directly over SSH (parallel).

  • Rotation profiles: auto-cycle through the panes of several sessions, with an optional interactive mode to send keys to the focused session.

  • Audit trail: record interactive sessions to gzip logs (tmux pipe-pane or script).

  • Local tmux: add an host with host = "localhost" to manage the tmux on the local machine without SSH.

  • MCP server: the whole client is exposed as a Model Context Protocol server on stdio, so an agent (e.g. opencode) can drive hosts, sessions, commands, broadcasts, tunnels and audit logs.

Requirements

  • Python 3.11+

  • OpenSSH ssh / scp client

  • tmux on the servers you manage (and locally for local hosts)

  • Optional: mc (Midnight Commander) for the file-exchange view

  • Optional: script (util-linux) to record plain SSH shells

  • Optional: a graphical terminal (kitty, ptyxis, gnome-terminal, konsole, alacritty) to open sessions in separate windows. The client detects the ones available in PATH and asks which to use each time you attach to or create a session — so you can pick a terminal that supports zooming out for a session that was resized elsewhere (kitty, for example, does not let you zoom out, while ptyxis does). If a single graphical terminal is installed there is nothing to choose and the prompt is skipped; set BRAVORIC_TERMINAL to pin one and skip the prompt even with several.

    • kitty also enables the kitty-unicode avatar renderer in pi-emote when TERM_PROGRAM=kitty is set in kitty.conf

Installation

git clone https://github.com/BravoRicDev/bravoric-ssh-client.git
cd bravoric-ssh-client
python -m venv .venv
.venv/bin/pip install -e ".[dev]"      # omit [dev] for a runtime-only install

Run it:

.venv/bin/bravoric-ssh

or, if installed on your PATH:

bravoric-ssh

Configuration

Config file: ~/.config/bravoric-ssh-client/config.toml (override the path with the BRAVORIC_CONFIG environment variable).

[credentials]
provider = "keyring"   # or "plain" (local file, permissions 0600)

[[hosts]]
alias = "web-prod"
host  = "server.example.com"
user  = "deploy"
# group = "clients"          # group hosts together (g key in the list)
# jump_host = "bastion"      # alias of the bastion for ProxyJump (-J)
# auth = ""                  # default: use the global provider
# auth = "key"               # SSH key / agent (no password)
# auth = "prompt"            # ask every time

General options ([general]):

[general]
restart_after_ssh = false  # reopen the TUI after an ssh session ends
history_size = 10          # how many recent sessions to keep
audit_log = false          # record sessions to compressed logs (pipe-pane/script)
# snippets_file = "..."    # snippet catalog path (default: <config_dir>/snippets.json)
# tunnels_file = "..."     # tunnel state path (default: <config_dir>/tunnels.json)

Import from your ssh config

.venv/bin/python scripts/import_ssh_config.py            # -> ~/.config/.../config.toml
.venv/bin/python scripts/import_ssh_config.py --merge    # add only new hosts

Import from Remmina

Press i on the host screen to import both ~/.ssh/config and Remmina SSH profiles (flatpak and legacy) without duplicates. For a new Remmina host, set the password the first time with p.

Import credentials from Remmina / SSH Pilot

.venv/bin/python scripts/import_keyring_credentials.py   # copy passwords into the app keyring

Using the TUI

  • Host screen: arrows + Enter to open a host. q quits.

    • a add a host · e edit · d delete (with confirmation) · D duplicate

    • p set/remove the host password in the provider

    • t connectivity test (TCP ping on the SSH port) · T ping all visible hosts

    • c copy a file (scp) · F file exchange via Midnight Commander

    • u SSH tunnels for the selected host (forward -L/-R/-D, live status)

    • B broadcast a snippet to multiple hosts (default: a new tmux session per host)

    • i import hosts from ~/.ssh/config and Remmina (merge)

    • g cycle groups · h recent sessions · R saved rotations · o observe a host

    • r reload config from disk · text field on top = live filter

    • a 🔑 next to a host means a password is stored

  • Session screen:

    • Enter attach · R read-only attach (tmux attach -r)

    • n new session · r rename · k kill (with confirmation) · K kill all

    • d session details · w list/manage windows (rename r, close k)

    • D detach other clients · W new window · g refresh

    • s open a plain SSH shell · Esc back to hosts

Attach, read-only attach, new session and plain shell open in a new window of a terminal you pick from the list; the TUI stays open, so you can launch more sessions from it without leaving it. The same happens when you attach from the observe screen, the quick-launch panel or the agent launcher. The new window runs a full bravoric-ssh process, so it behaves exactly like a direct launch: password, jump host, rotation, audit log, session history and window title all work the same way.

Window title: when you attach to (or create) a session, the terminal window title becomes HOST - SESSION. The client sets the remote tmux title string during the attach and restores it afterwards, so it works even with set-titles on on the server.

Automatic restart: with restart_after_ssh = true in [general], the TUI reopens automatically after you exit an ssh session.

Rotation profiles

If several agents/processes work in distinct tmux sessions (possibly across servers), create a rotation profile: the TUI shows each session's content (capture-pane) and, after cycle_interval seconds of inactivity, moves to the next one automatically. Any input stops the rotation on the current session.

  • R on the host screen → saved rotations (Enter to observe, n to create, a to reopen all sessions of the profile in separate windows).

  • Host config: auto_cycle = true and cycle_interval = 120 to enable automatic selection; profiles are saved in rotations.json.

Local tmux

Add a host with host = "localhost" (or 127.0.0.1, or set local = true): the TUI manages the local tmux directly without SSH. A localhost host is already included in the default config.

Every host change is written to config.toml with an automatic backup (config.toml.bak.<timestamp>).

SSH tunnels (u)

From the selected host press u: the list of configured tunnels with their status (🟢 active / 🔴 off). n adds one (kind L/R/D, port, destination), s/x start/stop a single tunnel, a/X start/stop all, d removes one. Tunnels run as background ssh -N -T processes (password from the keyring is injected automatically) and their definitions are saved in the config:

[[hosts]]
alias = "db-host"
host  = "server.example.com"
[[hosts.tunnels]]
name = "postgres"
kind = "L"
local_port = 5432
remote_host = "localhost"
remote_port = 5432
[[hosts.tunnels]]
kind = "D"          # SOCKS
local_port = 1080

Jump host / bastion (jump_host)

A host that is not directly reachable can be reached through a bastion: set the bastion alias in the config and the client injects -J (ProxyJump) into the connections, using the multi-host SSH_ASKPASS helper for both passwords (bastion and final host):

[[hosts]]
alias = "internal-app"
host  = "10.0.0.9"
user  = "deploy"
jump_host = "bastion"

Broadcasting snippets (B)

Press B on the host screen: snippet catalog (snippets.json in the config dir, n to add one). Pick a snippet, mark hosts with Space (or a for all) and press Enter. Two modes (t toggles, default: tmux):

  • tmux session (default): a detached tmux session named bcast-<snippet>-<host>-<YYYYMMDD-HHMMSS> is created on each host and runs the command. If tmux is not installed on a host, it falls back to direct mode.

  • Direct (ssh batch): the script runs in parallel (thread pool) and the results (stdout/exit code) appear immediately in the grid.

MCP server (bravoric-ssh-mcp)

The whole client is exposed as an MCP server on stdio: an agent (e.g. opencode) can control hosts, tmux sessions, commands, broadcasts, tunnels and audit logs exactly like the TUI.

bravoric-ssh-mcp            # start the server (stdio)

Exposed tools (47), grouped:

  • Host: list_hosts, get_host, get_status, ping, ping_all, hosts_summary, tmux_present

  • Overview: list_sessions_all, find_in_sessions, run_command_all, session_history

  • tmux sessions: list_sessions, create_session, session_details, rename_session, kill_session, kill_server, detach_clients, list_windows, new_window, rename_window, kill_window, capture_pane, send_keys, send_enter, send_raw

  • Run and wait: run_and_wait, broadcast_wait

  • Commands: run_command, run_command_many

  • Snippets/broadcast: list_snippets, add_snippet, remove_snippet, broadcast (tmux | direct)

  • Tunnels: list_tunnels, list_tunnels_all, start_tunnel, stop_tunnel, stop_tunnels, tunnel_health

  • Rotations: list_rotations, add_rotation, remove_rotation

  • Audit: list_audit_logs, read_audit_log, list_remote_audit_logs, read_remote_audit_log

Registration for opencode (global, ~/.config/opencode/opencode.jsonc):

"mcp": {
  "bravoric-ssh": {
    "type": "local",
    "command": ["bravoric-ssh-mcp"],
    "enabled": true,
    "environment": {}
  }
}

Use the absolute path to the executable if it is not on your PATH, e.g. /path/to/venv/bin/bravoric-ssh-mcp.

Agent skill

The repo ships a skill (docs/skill/SKILL.md) with the tool inventory, usage patterns (create detached → send-keys → capture-pane → kill), naming conventions and error handling.

# Claude Code
mkdir -p ~/.claude/skills/bravoric-ssh
cp docs/skill/SKILL.md ~/.claude/skills/bravoric-ssh/

# opencode
mkdir -p ~/.agents/skills/bravoric-ssh
cp docs/skill/SKILL.md ~/.agents/skills/bravoric-ssh/

Headless usage (no GUI)

The MCP server is a Python process on stdio and does not require a desktop:

  • Credentials: without a system keyring, use the plain provider (secrets.tsv, permissions 0600):

    [credentials]
    provider = "plain"
  • Config: point BRAVORIC_CONFIG at the file before starting bravoric-ssh-mcp (or create ~/.config/bravoric-ssh-client/config.toml).

  • The SSH_ASKPASS helpers work without a display (SSH_ASKPASS_REQUIRE=force). Typical systemd daemon:

    [Service]
    Environment=BRAVORIC_CONFIG=/etc/bravoric-ssh-client/config.toml
    ExecStart=/opt/bravoric-ssh-client/.venv/bin/bravoric-ssh-mcp

Audit trail (audit_log = true)

Interactive sessions are recorded to compressed logs:

  • attach / new tmux session: tmux pipe-pane -o 'gzip -c >> file' is injected during the attach and closed on detach. For local hosts the file is on this machine (<config_dir>/logs/YYYYMMDD_ALIAS_SESSION.log.gz), for remote hosts it lives in ~/.bravoric-ssh-client/logs/ on the server.

  • interactive shell (s): if script is installed, I/O is recorded locally; otherwise the shell starts without logging.

The .log.gz files are ready for offline parsing (zgrep, regex, ...).

When you pick an action (attach/new/shell) the session opens in a new window of the terminal you choose: the TUI stays alive, so you can launch more sessions from it, and the terminal running the TUI is never left dirty. Close the new window (or detach) to go back to the TUI.

Testing

.venv/bin/python -m pytest

With coverage and lint:

.venv/bin/python -m pytest --cov=bravoric_ssh_client
.venv/bin/ruff check .
.venv/bin/ruff format --check .

Contributing

This is a personal project, shared to show how I build my own tools. See CONTRIBUTING.md: it's small and modular on purpose, so bug reports and focused pull requests with tests are welcome — but I only take changes that fit the spirit of the tool.

License

MIT © Riccardo (bravoric) — Made by me, for me.

Available Tools

47 tools
add_rotationC

Aggiunge (o sostituisce per nome) un profilo di rotazione.

Inputs: name (required), entries (required). Returns: result

add_snippetC

Aggiunge uno snippet al catalogo.

Inputs: name (required), command (required), description. Returns: result

broadcastC

Broadcast di un comando/snippet su più host (mode=tmux|direct).

Inputs: aliases (required), mode, command, snippet. Returns: result

broadcast_waitC

Broadcast tmux su più host con attesa di completamento e raccolta output.

Inputs: aliases (required), command (required), timeout. Returns: result

capture_paneC

Cattura l'output di una sessione (ultime lines righe).

Inputs: alias (required), session (required), lines, window. Returns: result

create_sessionC

Crea una sessione tmux detached su un host (comando opzionale).

Inputs: alias (required), name, command. Returns: result

detach_clientsC

Stacca gli altri client dalla sessione.

Inputs: name (required), alias (required). Returns: result

find_in_sessionsC

Cerca una regex nelle pane delle sessioni tmux di un host.

Inputs: alias (required), pattern (required), lines. Returns: result

get_hostC

Dettagli di un host.

Inputs: alias (required). Returns: result

get_statusC

Diagnostica del server MCP (config, provider, tmux, dir log).

Inputs: no named parameters. Returns: result

hosts_summaryC

Dashboard: per ogni host raggiungibilità, tmux e numero sessioni.

Inputs: timeout. Returns: result

kill_serverC

Termina il server tmux di un host (tutte le sessioni).

Inputs: alias (required). Returns: result

kill_sessionC

Termina una sessione tmux.

Inputs: name (required), alias (required). Returns: result

kill_windowD

Chiude una finestra.

Inputs: alias (required), session (required), window_id (required). Returns: result

list_audit_logsB

Elenca i log audit compressi locali.

Inputs: no named parameters. Returns: result

list_hostsB

Elenca gli host configurati.

Inputs: no named parameters. Returns: result

list_remote_audit_logsC

Elenca i log audit compressi sul server remoto dell'host.

Inputs: alias (required). Returns: result

list_rotationsB

Elenca i profili di rotazione salvati.

Inputs: no named parameters. Returns: result

list_sessionsC

Elenca le sessioni tmux di un host.

Inputs: alias (required). Returns: result

list_sessions_allB

Elenca le sessioni tmux di tutti gli host in parallelo.

Inputs: no named parameters. Returns: result

list_snippetsB

Elenca gli snippet del catalogo.

Inputs: no named parameters. Returns: result

list_tunnelsB

Elenca i tunnel configurati di un host.

Inputs: alias (required). Returns: result

list_tunnels_allB

Elenca tunnel e stato attivo su tutti gli host.

Inputs: no named parameters. Returns: result

list_windowsC

Elenca le finestre di una sessione.

Inputs: alias (required), session (required). Returns: result

new_windowC

Crea una finestra in una sessione.

Inputs: alias (required), session (required), name. Returns: result

pingC

Verifica la raggiungibilità TCP di un host.

Inputs: alias (required), timeout. Returns: result

ping_allC

Ping TCP di tutti gli host.

Inputs: timeout. Returns: result

read_audit_logB

Legge un log audit .gz locale (max_lines=0 = tutto).

Inputs: filename (required), max_lines. Returns: result

read_remote_audit_logC

Legge un log audit .gz remoto (max_lines=0 = tutto).

Inputs: alias (required), filename (required), max_lines. Returns: result

remove_rotationB

Rimuove un profilo di rotazione.

Inputs: name (required). Returns: result

remove_snippetC

Rimuove uno snippet dal catalogo.

Inputs: name (required). Returns: result

rename_sessionC

Rinomina una sessione tmux.

Inputs: alias (required), new_name (required), old_name (required). Returns: result

rename_windowD

Rinomina una finestra.

Inputs: alias (required), session (required), new_name (required), window_id (required). Returns: result

run_and_waitB

Esegue un comando in tmux detached e attende il completamento (sentinel).

Inputs: alias (required), command (required), timeout. Returns: result

run_commandC

Esegue un comando batch su un host (ssh).

Inputs: alias (required), command (required), timeout. Returns: result

run_command_allC

Esegue un comando batch su tutti gli host raggiungibili.

Inputs: command (required), timeout. Returns: result

run_command_manyC

Esegue un comando batch in parallelo su più host.

Inputs: aliases (required), command (required), timeout, max_workers. Returns: result

send_enterC

Invia Invio alla sessione.

Inputs: alias (required), session (required). Returns: result

send_keysC

Invia testo letterale alla sessione.

Inputs: text (required), alias (required), session (required). Returns: result

send_rawC

Invia tasti tmux non letterali (es. C-c, Up).

Inputs: keys (required), alias (required), session (required). Returns: result

session_detailsC

Dettagli di una sessione tmux.

Inputs: name (required), alias (required). Returns: result

session_historyC

Cronologia delle sessioni recenti (history.json).

Inputs: no named parameters. Returns: result

start_tunnelC

Avvia un tunnel SSH (kind L/R/D).

Inputs: kind (required), alias (required), local_port (required), bind, +3 more. Returns: result

stop_tunnelC

Ferma il tunnel su una porta.

Inputs: alias (required), local_port (required). Returns: result

stop_tunnelsC

Ferma tutti i tunnel di un host.

Inputs: alias (required). Returns: result

tmux_presentC

True se tmux è installato sull'host.

Inputs: alias (required). Returns: result

tunnel_healthC

Verifica che le porte locali dei tunnel attivi ascoltino davvero.

Inputs: alias (required). Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    C2.5/5.0

    Scored across 47 tools

    Disambiguation3/5

    Many tools are clearly distinct (e.g., ping vs list_hosts vs create_session), but several overlapping pairs exist: run_command vs run_command_all vs run_command_many, broadcast vs broadcast_wait, list_tunnels vs list_tunnels_all, and list_audit_logs vs list_remote_audit_logs. Descriptions help but don't fully eliminate ambiguity, especially around batch vs single-host variants.

    Naming Consistency4/5

    The vast majority of tools follow a consistent verb_noun convention (list_hosts, get_host, create_session, kill_session, etc.). Minor deviations appear in compound names like hosts_summary (noun_verb) and ping_all, but overall it's predictable.

    Tool Count2/5

    47 tools is excessive for the apparent scope and goes well beyond the typical 3-15 range. Many tools are thin wrappers (send_enter, send_keys, send_raw) that could be consolidated, making the surface feel heavy and unwieldy.

    Completeness4/5

    Coverage is broad: host management, tmux sessions/windows/panes, tunnels, snippets, rotations, audit logs, and diagnostics are all present. A few gaps exist (e.g., no explicit update_host or snippet editing), but the lifecycle for most resources is covered.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      Enables programmatic control over tmux terminal sessions for SSH access, command execution, and terminal automation. Supports creating sessions, sending commands, capturing output, and managing multiple panes for interactive debugging and monitoring.
      8
      10 npm
      MIT
    • A
      license
      Not graded
      quality
      A
      maintenance
      Enables AI agents to remotely execute shell commands and transfer files on SSH hosts, including jump-host tunneling, background tasks, resumable transfers, and optional approval of destructive commands, while SSH credentials remain in the local process. It runs over stdio or a streamable HTTP daemon and supports auditing, allow/deny lists, and multiple authentication methods.
      ISC
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables agents to spawn, monitor, and control CLI coding agents running in isolated tmux sessions, with verified input injection, redacted pane capture, fleet and per-session state classification, lineage tracking, blocking exec with exit codes, cross-agent messaging, waste analytics, and log/ledger reading. Ownership checks, policy controls, and audit events keep every control action safe by default.
      3 npm
      Apache 2.0
    • A
      license
      Not graded
      quality
      B
      maintenance
      Exposes interactive Claude Code CLI sessions running inside tmux across one or more machines as MCP tools, enabling agents to list, create, read, and drive those sessions over stdio.
      1
      MIT