English | 简体中文
sshc is a small SSH helper CLI for managing hosts, running remote commands,
transferring files, executing local scripts on remote hosts, and keeping per-host execution logs.
It is intended for lightweight deployment, troubleshooting, and day-to-day remote operations where a full automation platform would be too heavy.
- Manage SSH hosts in
~/.config/sshc/sshc.config.json- Encrypt saved passwords before writing to disk
- Manage shared credential profiles with
auth - Inspect and edit local settings with
cfg - Read simple host entries from
~/.ssh/config - Verify SSH host keys with
known_hostsby default - Run remote commands by saved host name, IP, or unique partial match
- Execute local shell scripts on remote hosts
- Connect through a single jump host with host config or
--jump - Run commands or scripts across multiple hosts with
batch-run/brun - Set remote working directory, timeout, environment variables, sudo, and sudo user
- Upload files with
uploadand download files withdownloadover SFTP- Verify single-file transfers with SHA256
- Keep per-host JSONL run logs under
~/.config/sshc/logs/ - Open an interactive remote PTY with
login/connect
- Recommended Install by eget:
eget install inhere/sshc - Install by Golang:
go install github.com/inhere/sshc/cmd/sshc@latest - Download the archive for your platform from GitHub Releases, extract it, and put the
sshcbinary on yourPATH.
git clone https://github.com/inhere/sshc.git
cd sshc
go build -o sshc ./cmd/sshc
# Windows
go build -o sshc.exe ./cmd/sshcsshc add --ip 192.168.1.10 --name devhost -u root -p password
sshc list
sshc run devhost -- uptime
sshc auth add dev-root -u root -p --remark "shared root login"
sshc host add --ip 192.168.1.10 --name devhost --auth dev-root # use auth refer
sshc host add --ip 10.0.0.8 --name inner-db --auth dev-root --jump bastion
sshc run devhost --script ./deploy.sh
sshc run inner-db --jump bastion -- hostname
sshc batch-run --hosts devhost,web-2 -- uptime
sshc scp -l ./dist -r /opt/app/dist devhost
sshc download -r /var/log/my-app/app.log -l tmp/logs/ devhost --sha256
sshc log devhost --tail 20sshc add Add or update a host
sshc list|ls List saved hosts
sshc cfg|config Manage config
sshc auth|cred Manage credential profiles
sshc host|hosts Manage hosts
sshc run|exec Run a remote command
sshc batch-run|brun Run a command or script on multiple hosts
sshc login Open an interactive SSH shell
sshc scp|upload Upload files or directories
sshc download|dl Download files or directories
sshc log Show or search run logs
Run command-specific help for full options:
sshc <command> --helpsshc add --ip 192.168.1.10 -u root -p password
sshc add --ip 192.168.1.10 --name devhost -u root -p password --port 22
sshc add --ip 192.168.1.10 --name devhost -u root --key ~/.ssh/id_rsa
sshc add --ip 192.168.1.10 --name devhost --auth dev-root
sshc add --ip 10.0.0.8 --name inner-db --auth dev-root --jump bastion
sshc add -I
sshc add --from-clipboardsshc add -I prompts for host fields interactively and hides password input.
--from-clipboard accepts either key=value/key: value lines or one CSV line:
ip=192.168.1.10
user=root
password=password
name=devhost
port=22
192.168.1.10,root,password,devhost,22
Use auth profiles when multiple hosts share the same user, password, or key:
sshc auth add dev-root -u root -p --remark "shared root login"
sshc auth add deploy-key -u deploy --key ~/.ssh/id_ed25519
sshc auth list
sshc auth show dev-root
sshc auth rm old-profile --yessshc auth add -p prompts for a hidden password. It intentionally does not
accept -p secret or --password secret.
Attach a profile to a host:
sshc host add --ip 192.168.1.10 --name devhost --auth dev-root
sshc host add --ip 10.0.0.8 --name inner-db --auth dev-root --jump bastionsshc host add --ip 192.168.1.10 --name devhost --auth dev-root
sshc host add --ip 10.0.0.8 --name inner-db --auth dev-root --jump bastion
sshc host list --group testing --show-ip
sshc host list --match devhost
sshc host show devhost
sshc host rm devhost --yes
sshc host rename old-name new-nameTop-level add, list, and ls remain available for quick daily use.
sshc host import -f ips.txt --format ips --auth dev-root --group testing --yes
sshc host import -f hosts.txt --format plain --dry-run
sshc host import -f hosts.csv --format csv --overwrite --yes
sshc host import --from-clipboard --format plain --auth dev-rootips is a simple one-target-per-line format:
10.0.0.8
10.0.0.9
web.internal
plain reuses the same key=value/key: value style as add --from-clipboard.
Separate multiple hosts with a blank line:
ip=10.0.0.8
name=devhost
auth=dev-root
group=testing
ip: 10.0.0.9
name: dbhost
user: root
password: secret
group: testing
CSV imports must include a header row:
name,ip,auth,group,remark,port
devhost,10.0.0.8,dev-root,testing,app server,22By default, conflicts fail without saving. Use --skip-existing to ignore saved
hosts or --overwrite to update them. --dry-run previews the plan. Imported
passwords are encrypted before saving and are not printed.
sshc list
sshc ls
sshc list --show-ipsshc list shows the host name, group, address, authentication type, and remark.
IPv4 addresses are masked by default, for example 10.*.*.8. Use --show-ip
when you need the full address.
Hosts from ~/.ssh/config are also listed when they have HostName, User, and
IdentityFile.
sshc run 192.168.1.10 -- uptime
sshc run devhost -- docker ps
sshc run devhost --cwd /opt/app -- python -m app
sshc run devhost --timeout 30s --kill-after 5s -- systemctl status nginx
sshc run devhost -e APP_ENV=prod -e DEBUG=1 -- printenv APP_ENV
sshc run devhost --efile ./remote.env -- envRemote commands must be placed after --.
Environment files support comments, blank lines, plain KEY=value, and
export KEY=value lines:
APP_ENV=prod
export DEBUG=1
NAME="hello world"
Set jump on the target host when it normally needs a bastion:
sshc host add --ip 1.2.3.4 --name bastion --auth dev-root
sshc host add --ip 10.0.0.8 --name inner-db --auth dev-root --jump bastionEquivalent config:
{
"hosts": [
{
"name": "bastion",
"ip": "1.2.3.4",
"auth_ref": "dev-root"
},
{
"name": "inner-db",
"ip": "10.0.0.8",
"auth_ref": "dev-root",
"jump": "bastion"
}
]
}Then use the target host as usual:
sshc run inner-db -- hostname
sshc login inner-db
sshc scp -l app.jar -r /tmp/app.jar inner-db
sshc download -r /var/log/app.log -l tmp/logs inner-dbUse --jump to override the configured jump host for one command:
sshc run inner-db --jump bastion -- hostname
sshc login inner-db --jump bastion
sshc scp -l app.jar -r /tmp/app.jar inner-db --jump bastion
sshc download -r /var/log/app.log -l tmp/logs inner-db --jump bastionThe initial implementation supports one jump host. Nested jump hosts,
ProxyCommand, and PVE/LXC/vhost-specific execution are not supported yet.
Host key checking still happens on the local machine for both the jump host and
the target host.
sshc run devhost --script ./deploy.sh
sshc run devhost --script ./deploy.sh --cwd /opt/app
sshc run devhost --script ./deploy.sh --remote-script-dir /opt/app/tmp
sshc run devhost --script ./deploy.sh --keep-remote-scriptUse --script for multiline shell, here-doc, source or virtualenv activation,
or commands that require heavy quoting.
Script mode uploads the local file to /tmp by default and runs it with bash.
Use --remote-script-dir when /tmp has restrictive mount options, permissions,
or cleanup policies.
sshc batch-run --hosts devhost,web-2 -- uptime
sshc brun --hosts devhost,web-2 -- hostname
sshc batch-run --group testing --parallel 5 --script ./deploy.sh
sshc batch-run --hosts-file hosts.txt -- hostname
sshc batch-run --hosts-file ips.txt --auth dev-root --script ./init.sh--hosts accepts a comma-separated list. --hosts-file reads one host target per
line and ignores blank lines and full-line comments. Saved hosts are resolved
first; unresolved IP or hostname targets can be used with shared auth options
such as --auth, -u, --key, or -p.
Use --parallel to limit concurrency. With --fail-fast, sshc stops starting
new hosts after the first failure and waits for already running hosts to finish.
sshc run devhost --sudo -- apt-get update
sshc run devhost --sudo-user app --cwd /opt/app -- whoami
sshc run devhost --script ./deploy.sh --sudo-user app --remote-script-dir /opt/app/tmp--sudo and --sudo-user require passwordless sudo, or an SSH user that already
has the required privileges.
sshc scp -l ./local-file.txt -r /tmp/remote-file.txt devhost
sshc scp -l ./local-file.txt -r /tmp/remote-file.txt devhost --sha256
sshc scp -l ./local-dir -r /tmp/remote-dir devhost
sshc scp -l ./dist -r /opt/app/dist devhost --remove-dir
sshc scp -l "./dist/*.jar" -r /opt/app/lib devhost
sshc scp -l ./a.jar -l ./b.jar -r /opt/app/lib/ devhost
sshc scp --map ./config/app.yml=/etc/app/app.yml --map ./scripts/deploy.sh=/opt/app/deploy.sh devhostUse repeatable -l/--local when multiple local paths should go into one remote
directory. Use repeatable --map local=remote when each local path needs an
explicit remote destination.
sshc download -r /tmp/remote-file.txt -l ./local-file.txt devhost
sshc download -r /tmp/remote-file.txt -l ./local-file.txt devhost --sha256
sshc download -r /var/log/my-app/app.log -l tmp/logs/ devhost --sha256
sshc dl -r /tmp/remote-dir -l ./local-dir devhostExisting local directories receive the remote base name. Local paths ending with
/ or \ are also treated as directories.
sshc log
sshc log devhost
sshc log devhost --match uptime
sshc log devhost --tail 50
sshc log devhost -m error --tail 50
sshc log --id 20260704-173012-a1b2c3
sshc log --id 20260704-173012-a1b2c3 --tail 80
sshc log --id 20260704-173012-a1b2c3 --lines 120,180
sshc log devhost --lines 20,80Every run writes one JSON log line to ~/.config/sshc/logs/<host>.log by
default. Each run has a task_id. Short output is kept inline in JSONL; larger
output is stored under ~/.config/sshc/logs/yyyyMMdd/<task_id>.out.log and can
be opened with sshc log --id <task_id>.
Interactive login sessions only record connection metadata.
sshc login
sshc login devhost
sshc connect devhost
sshc login --term xterm-256color devhostlogin and connect open an interactive remote PTY. The terminal type defaults
to the local TERM value and falls back to xterm-256color.
When no target is provided, or when a target matches multiple hosts, sshc
opens an interactive host selector.
Most commands accept a saved host name or IP. sshc resolves exact matches
first, then a unique partial match across host name, IP, remark, and group.
For example, if a saved host has name testing-web, group testing, and remark
gpu runner, these can match when the result is unique:
sshc run "testing web" -- hostname
sshc run "testing gpu" -- uptimeIf multiple hosts match, sshc returns the candidate list instead of guessing.
Default config file:
~/.config/sshc/sshc.config.json
Example config:
{
"version": 1,
"logs_path": "logs",
"defaults": {
"user": "root",
"port": 22,
"connect_timeout": "20s",
"remote_script_dir": "/tmp",
"host_key_check": "known_hosts",
"known_hosts_path": "~/.ssh/known_hosts"
},
"auth_profiles": [
{
"name": "dev-root",
"user": "root",
"password_enc": "v1:...",
"remark": "shared root login"
}
],
"hosts": [
{
"name": "devhost",
"ip": "192.168.1.10",
"auth_ref": "dev-root",
"group": "testing"
},
{
"name": "inner-db",
"ip": "10.0.0.8",
"auth_ref": "dev-root",
"jump": "devhost",
"group": "testing"
}
]
}logs_path can be absolute, start with ~, or be relative to
~/.config/sshc.
Default run log directory:
~/.config/sshc/logs/<host>.log
Use another config file:
SSHC_CONFIG=/path/to/sshc.config.json sshc listSaved hosts override entries loaded from ~/.ssh/config when the name or IP is
the same.
For compatibility, ~/.config/sshc/hosts.json is still read when the new default
config file does not exist.
Use cfg export/import to move a complete sshc config to another machine:
sshc cfg export -o sshc-export.enc
sshc cfg import -f sshc-export.enc --key "sshc-v1:..."
sshc cfg import -f sshc-export.enc --key "sshc-v1:..." --overwrite
sshc cfg import -f sshc-export.enc --key "sshc-v1:..." --replacecfg export writes an encrypted export package and prints a one-time export key.
Save that key separately; it is not stored in the export file or local config.
cfg import backs up the current config before writing. The default merge
strategy rejects conflicting host names, host IPs, and auth profile names.
Use --overwrite to update conflicting entries, or --replace to replace the
current config with the imported config.
Passwords from the export package are re-encrypted with the target machine's
local ~/.config/sshc/key when the imported config is saved. Importing plain IP
lists, CSV files, or pasted host snippets is handled by sshc host import, not
sshc cfg import.
Config helpers:
sshc cfg path
sshc cfg show
sshc cfg show --raw
sshc cfg get logs_path
sshc cfg set logs_path ./runtime/logs
sshc cfg set defaults.user root
sshc cfg set defaults.port 2222
sshc cfg set defaults.host_key_check known_hosts
sshc cfg unset logs_path
sshc cfg doctorcfg show masks passwords and encrypted password values. cfg show --raw
prints the config file as stored on disk and is intended for local debugging.
- Saved passwords are encrypted before being written to
sshc.config.json. - The local encryption key is stored at
~/.config/sshc/key; keep both files private. - Legacy plaintext
passwordfields are still readable for compatibility. - Prefer SSH keys over passwords when possible.
- If both password and
--keyare provided, key authentication is tried first. - SSH host keys are checked against
~/.ssh/known_hostsby default. - If a host is not trusted yet, connect once with
ssh devhostor add its key toknown_hostsbefore usingsshc. - Set
host_key_checktoinsecureonly when you explicitly want to skip host key verification. - With
--script --sudo-user, the uploaded temporary script is readable by local remote users so the target sudo user can execute it. For sensitive scripts, use--remote-script-dirwith a restricted remote directory. loginopens an interactive PTY. Session logs record connection metadata only, not typed commands or terminal output.
go test ./...
go build -o tmp/sshc ./cmd/sshcOn Windows:
go test ./...
go build -o tmp\sshc.exe ./cmd/sshcRelease builds are driven by tags:
git tag v0.1.0
git push origin v0.1.0MIT