Skip to main content

hermes-ssh

CI License: MIT Python 3.11+

SSH remote operations plugin for Hermes Agent.

Run commands, transfer files, track background sessions, and reuse connections across a named machine registry.

/ssh web1 uptime
ssh_transfer action=upload machine=web1 source="./dist/app.tar.gz" destination="/srv/releases/app.tar.gz"
ssh_machines action=add name=web1 host=192.168.1.50 user=deploy

quick start

hermes-ssh is for named multi-host operations. Hermes's core SSH backend is useful for one configured remote terminal; this plugin adds a reusable machine inventory, aliases, per-command targeting, file transfer, background sessions, and audit history.

Requires Python 3.11+, Hermes Agent, and OpenSSH clients named ssh and sftp.

Install the package into the same Python environment that runs Hermes:

python -m pip install hermes-ssh
hermes plugins enable hermes-ssh --no-allow-tool-override

Run /reset or restart Hermes, then verify:

python -m pip show hermes-ssh
hermes plugins list --enabled --plain

If Hermes cannot see the package, the python command above was not Hermes's Python. Follow the environment procedure in the repository AGENTS.md.

For source development:

git clone https://github.com/TheEpTic/hermes-plugins.git
cd hermes-plugins/hermes-ssh
./deploy.sh
hermes plugins enable hermes-ssh --no-allow-tool-override

Run /reset or restart Hermes after changing the source tree.

features

ssh_terminal — run commands

Execute any command on a remote machine. Commands run through bash -c with pipefail, so pipelines work correctly.

# synchronous
ssh_terminal machine=web1 command="df -h"

# background
ssh_terminal machine=web1 command="tail -f /var/log/syslog" background=true

# custom timeout
ssh_terminal machine=web1 command="make -j4" timeout=300

When output exceeds max_output_chars (default: 50,000), the full output is saved under the plugin's restricted output directory and a summary with the path is returned.

Long-running commands can spool stdout and stderr to restricted files in the background:

ssh_terminal poll=<session_id>
ssh_terminal read_output=<session_id>

ssh_sessions action=poll session_id=<session_id>
ssh_sessions action=read_output session_id=<session_id>

ssh_transfer — upload and download files

Transfer a regular file or directory between the Hermes host and a registered machine. The tool uses OpenSSH SFTP, reuses the same ControlMaster connection settings as ssh_terminal, and records transfer metadata in the existing audit log.

# upload a release
ssh_transfer action=upload machine=web1 source="./dist/app.tar.gz" destination="/srv/releases/app.tar.gz"

# download a log
ssh_transfer action=download machine=web1 source="/var/log/app.log" destination="./app.log"

# upload a directory
ssh_transfer action=upload machine=web1 source="./public" destination="/srv/app/public" recursive=true

# explicitly replace an existing regular file
ssh_transfer action=upload machine=web1 source="./app.tar.gz" destination="/srv/app.tar.gz" overwrite=true

Transfer behaviour is intentionally conservative:

  • Existing files are not replaced unless overwrite=true.
  • Directories require recursive=true.
  • Existing directories are never merged or replaced.
  • Uploads and downloads stage through generated temporary paths before final rename.
  • Local and remote credential paths are blocked.
  • Symbolic links, special files, traversal segments, wildcard remote paths, and recursive trees containing links are rejected.
  • Upload and download paths are explicit destinations, not shell expressions.

ssh_machines — machine registry

Register servers once, then refer to them by name or alias.

ssh_machines action=add name=web1 host=192.168.1.50 user=deploy key=~/.ssh/id_ed25519
ssh_machines action=add name=prod-web host=10.0.0.1 aliases=web1 tags=production,web
ssh_machines action=list
ssh_machines action=test name=web1
ssh_machines action=inspect name=web1

Machine names must be alphanumeric with dots, hyphens, or underscores (1-64 characters). Slashes, spaces, and glob characters are rejected.

ssh_sessions — session tracking

Background commands are tracked as sessions with their process, machine, command count, and idle time.

ssh_sessions action=list
ssh_sessions action=kill session_id=<session_id>
ssh_sessions action=cleanup
ssh_sessions action=prune

Idle sessions are automatically killed after 30 minutes. Closed sessions are pruned after 24 hours.

/ssh slash command

Quick terminal access from chat:

/ssh
/ssh web1
/ssh web1 uptime
/ssh web1 docker ps
/ssh test
/ssh cleanup
/ssh help

File transfers use the ssh_transfer tool rather than slash-command syntax so direction, paths, overwrite behaviour, and recursion remain explicit.

configuration

Settings live in src/ssh_tools/config.py as an SSHConfig dataclass:

setting default description
default_port 22 SSH port for new machines
default_user current local user SSH user for new machines
connect_timeout 5s SSH handshake timeout
command_timeout 30s command execution timeout
max_output_chars 50,000 output truncation threshold
audit_log_mode redacted redacted, metadata, or off
idle_check_interval 60s seconds between idle checks
idle_timeout_minutes 30m auto-kill after this idle time
closed_prune_hours 24h remove closed sessions after this
strict_host_key_checking yes SSH host key verification

ssh_transfer has a 300-second default timeout and a 3,600-second maximum supplied through its tool schema.

architecture

src/ssh_tools/
├── __init__.py          # plugin registration and Hermes hooks
├── approval.py          # Hermes dangerous-command approval bridge
├── config.py            # SSHConfig
├── manager.py           # machine, command, and session state
├── models.py            # Machine and Session dataclasses
├── schemas.py           # LLM-facing tool schemas
├── storage.py           # encrypted machine registry
├── transfers/           # SFTP policy, transport, staging, and audit
│   ├── __init__.py      # validated transfer entry point
│   ├── models.py        # transfer request and result models
│   ├── policy.py        # local and remote path safety
│   ├── service.py       # transfer orchestration and finalisation
│   └── transport.py     # OpenSSH SFTP argv and batch construction
├── utils.py             # handler response helpers
├── py.typed             # PEP 561 marker
└── handlers/
    ├── terminal.py      # ssh_terminal
    ├── transfer.py      # ssh_transfer
    ├── machines.py      # ssh_machines
    ├── sessions.py      # ssh_sessions
    └── slash.py         # /ssh

Key design decisions:

  • SSHManager owns the machine registry, command execution, and session state.
  • Tool handlers are thin closures that validate parameters and dispatch work.
  • Transfers use argv plus SFTP batch input, never shell=True or interpolated local shell commands.
  • Upload/download payloads are staged before their final rename.
  • Machine records are encrypted at rest.
  • JSON files use atomic writes for crash safety.
  • Data directories use 0o700; audit and output files use 0o600.
  • Machine names and transfer paths are validated before reaching OpenSSH.
  • Connections are reused through ControlMaster with a five-minute persist window.

security

See SECURITY.md for the full boundary.

Defaults and limitations worth knowing:

  • StrictHostKeyChecking=yes is the default. Add a verified host key to OpenSSH known_hosts before registering a machine. accept-new remains available only for an explicit compatibility override.
  • Machine credentials are encrypted at rest under ~/.hermes/ssh-tools/.
  • Audit logs redact common inline command credentials by default. Metadata mode stores hashes and lengths instead of transfer paths.
  • Commands and transfers run with the registered remote user's permissions.
  • Transfer path blocks reduce accidental credential movement but are not a sandbox for untrusted prompts.
  • Use dedicated non-root accounts and expose Hermes only to trusted operators.

requirements

  • Python 3.11+
  • OpenSSH clients named ssh and sftp
  • Hermes Agent

troubleshooting

Plugin installed but tools are absent

Enable the plugin and reset Hermes:

hermes plugins enable hermes-ssh --no-allow-tool-override
hermes plugins list --enabled --plain

Permission denied (publickey)

The stored user, key path, or remote authorisation is wrong. Verify the same account with OpenSSH outside Hermes.

Transfer says sftp is unavailable

Install the OpenSSH client package in the environment or container that runs Hermes and verify sftp is on that process's PATH.

Remote path rejected

Remote transfer paths must be absolute or begin with ~/, name an explicit file or directory, and contain no wildcard or traversal segments.

Destination already exists

Regular files require overwrite=true for replacement. Directory replacement and merging are intentionally unsupported.

Command timeout

Increase timeout or use background=true for terminal commands. Transfers accept timeouts from 1 to 3,600 seconds.

Output looks truncated

Large terminal output is retained under the restricted output directory. Use read_output or read_file on the returned path.

development

git clone https://github.com/TheEpTic/hermes-plugins.git
cd hermes-plugins/hermes-ssh
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'

black --check src/ssh_tools/ tests/
mypy src/ssh_tools/
pytest

CI runs those gates on Python 3.11, 3.12, and 3.13.

license

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hermes_ssh-0.4.0.tar.gz (92.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hermes_ssh-0.4.0-py3-none-any.whl (42.6 kB view details)

Uploaded Python 3

File details

Details for the file hermes_ssh-0.4.0.tar.gz.

File metadata

  • Download URL: hermes_ssh-0.4.0.tar.gz
  • Upload date:
  • Size: 92.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for hermes_ssh-0.4.0.tar.gz
Algorithm Hash digest
SHA256 a96961e20d383231181bf8aae92c8d1ba2dd99dbb80451b3465aafe7015e3ecc
MD5 2ccacfa363e1d165653a04bc66f01dfc
BLAKE2b-256 0a72ec5f0ec9d3db31f7375b5e6220525aac8aec991f2d2457b7545a1b7d451b

See more details on using hashes here.

Provenance

The following attestation bundles were made for hermes_ssh-0.4.0.tar.gz:

Publisher: pypi-publish-ssh.yml on TheEpTic/hermes-plugins

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hermes_ssh-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: hermes_ssh-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 42.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for hermes_ssh-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e4f0121807c414e77edd4a7648c2301d654bf9df40c3382f93e2da6826304cb8
MD5 2c5911640a08ee8e03328ca946e37d49
BLAKE2b-256 5970d5fafa78c069d8d8ced430bc6bdf5de64ff261bf94318cd2b821f1cd6b42

See more details on using hashes here.

Provenance

The following attestation bundles were made for hermes_ssh-0.4.0-py3-none-any.whl:

Publisher: pypi-publish-ssh.yml on TheEpTic/hermes-plugins

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.8

2 files

0.4.7

2 files

0.4.4

2 files

0.4.3

2 files

0.4.2

2 files

This release

0.4.0 This release

2 files

0.3.4

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page