oh-my-ssh
v1.1.2
Published
Self-hosted SSH host manager for the terminal. CLI + TUI, projects your hosts into ~/.ssh/config so ssh, scp, sftp and VSCode Remote work unchanged.
Maintainers
Readme
omssh
SSH host manager for the terminal. Dashboard, forms and a two-pane SFTP browser — keyboard and mouse driven.
Your hosts live in a vault you own. omssh projects them into ~/.ssh/config.d/omssh.conf, so
ssh, scp, sftp, rsync and VSCode Remote work with them unchanged. No terminal
emulator, no lock-in, no subscription.
┌ filter ───────────────────┐┌ nas ──────────────────────────┐
│ na▮ ││ hostname 10.0.0.5 │
└───────────────────────────┘│ user chap port 22 │
┌ hosts 5/5 ────────────────┐│ password stored (sealed) │
│ ▾ home (2) │├ ~/.ssh/config.d/omssh.conf ───┤
│ ❯ nas 10.0.0.5 ││ Host nas │
│ pi 10.0.0.6 ││ HostName 10.0.0.5 │
│ ▾ work (1) ││ User chap │
│ bastion 1.2.3.4 ││ IdentityFile ~/.ssh/id_ed… │
└───────────────────────────┘└───────────────────────────────┘
↵ connect · a add · e edit · d rm · s sftp · f files · y copy · p pass · q quitInstall
npm i -g oh-my-ssh
omsshNode 22+ and OpenSSH. macOS and Linux.
Use
omssh # dashboard
omssh nas # connect straight away
omssh nas -- -L 8080:localhost:8080
omssh ls --json # machine-readable, for scripts and agentsAdding or editing a host writes ~/.ssh immediately — no apply step to forget.
Names ssh will not take
Call a host whatever you call it out loud — 개발_서버, NAS (attic).
ssh(1) will not: it checks its destination argument before reading any config and
refuses non-ASCII bytes, spaces and shell metacharacters, so ssh 개발_서버
dies with hostname contains invalid characters however the config is written.
So a host carries two names. The label is yours. The alias is the ASCII one
omssh hands to ssh and writes into ~/.ssh/config.d/omssh.conf, romanized from the
label and de-duplicated against the rest of the vault and any Host block you wrote
by hand. omssh commands take either, or a fragment of either.
omssh add --label '개발_서버' --hostname 192.0.2.10
# warning: ssh cannot use "개발_서버" as a destination
# — stored as gaebal-seobeo
omssh 개발_서버 # by label
omssh 개발 # by fragment, when it names one host
omssh gaebal-seobeo
ssh gaebal-seobeo # and outside omssh: scp, rsync, VSCode Remote
# --alias is rejected when it is not something ssh accepts, rather than fixed
# silently. Pass the readable name as --label and the ASCII one as --alias.
omssh add --label '개발_서버' --alias dev-1 --hostname 192.0.2.10
omssh doctor # aliases ssh would refuse, with replacements
omssh doctor --fix # rename them; the old name becomes the labelDashboard
| Key | |
| ----------- | ----------------------------------- |
| type | filter · tab switches to the list |
| ↵ | connect |
| a e d | add · edit · remove |
| s f | sftp session · file browser |
| y p | copy ssh <alias> · set a password |
| space | collapse a group |
| ? | all keys |
The footer is clickable, rows select on click and connect on double-click.
File browser — f
┌ local ~/project/release ─────────┐ ┌ nas /srv/www ────────────────────┐
│ ~/project/release▮ │ │ /srv/www │
└───────────────────────────────────┘ └───────────────────────────────────┘
┌───────────────────────────────────┐ ┌───────────────────────────────────┐
│ ../ │ │ ../ │
│ assets/ │ → │ assets/ │
│ ✓ app-2.1.0.tar.gz │ │ app-2.0.4.tar.gz │
│ ✓ checksums.txt │ ← │ index.html │
│ ❯ notes.md │ │ robots.txt │
└───────────────────────────────────┘ └───────────────────────────────────┘
████████████░░░░░░░░ 62% 4.1MB / 6.6MB
tab pane · ↑↓ move · ↵ open · - up · space mark · → upload · ← download · p path · r refresh · q closeLocal on the left, remote on the right. space marks (✓), → uploads what the left pane
has selected, ← downloads from the right — and both arrows are clickable. With nothing
marked, the row under the cursor is the one that moves.
↵ or a double-click opens a directory, ../ walks up, and the path boxes take typed input
including ~. Directories transfer whole. Local changes appear as they happen; the remote
side refreshes after a transfer or on r.
One multiplexed ssh connection serves the session, so changing directory costs a round trip rather than a reconnect.
Passwordless
omssh key setup # device key, loaded into the agent
omssh key deploy --all # each host's password, one last timeOr keep the password instead — omssh seals it to this device and hands it to ssh on demand,
leaving nothing on the server:
omssh password set nasWhere things live
~/.config/oh-my-ssh/vault.json hosts, no secrets
~/.config/oh-my-ssh/secrets.json passwords, sealed
~/.config/oh-my-ssh/device.key X25519 private key
~/.local/share/oh-my-ssh/install.key
~/.ssh/config.d/omssh.conf generated, rewritten on every changePasswords are sealed with X25519 + ChaCha20-Poly1305. Both the device key and the install
key are required to decrypt, and they sit in different directories. Crypto is node:crypto
only — no native modules.
Security
What it does: keeps passwords out of prompts, shell history, process arguments and agent transcripts; writes nothing to the remote host; and refuses to answer host-key questions, so a man-in-the-middle cannot collect a stored password by presenting an unknown key.
What it does not do: protect against code running as your user. Unattended decryption and resistance to local code execution cannot both hold — if the machine can decrypt without a human, so can anything running on it. That includes AI agents, which are ordinary code with your privileges.
Status
A personal project, offered as-is. No SLA.
Apache-2.0
