SSH remote execution plugin for Hermes Agent
Project description
hermes-ssh
SSH remote execution plugin for Hermes Agent.
Run commands on remote servers, track sessions, reuse connections — all from inside Hermes.
/ssh web1 uptime
ssh_machines add name=web1 host=192.168.1.50
ssh_sessions list
Quick Start
Requires Python 3.11+ and an OpenSSH client (
ssh) on the host system.
Option 1: Deploy script (recommended)
git clone https://github.com/TheEpTic/hermes-plugins.git
cd hermes-plugins/hermes-ssh
./deploy.sh
Then restart Hermes with /reset.
Option 2: Manual symlink
git clone https://github.com/TheEpTic/hermes-plugins.git
ln -s "$(pwd)/hermes-plugins/hermes-ssh/src/ssh_tools" ~/.hermes/plugins/hermes-ssh
Then /reset in Hermes. The symlink points at the source tree, but Python modules are imported once, so code changes still require /reset or a Hermes process restart before they load.
Option 3: As a Python package
pip install git+https://github.com/TheEpTic/hermes-plugins.git#subdirectory=hermes-ssh
Then enable it and restart Hermes:
hermes plugins enable hermes-ssh
Features
ssh_terminal — Run Commands
Execute any command on a remote machine. Commands run through bash -c with pipefail, so pipelines work correctly.
# Synchronous (waits for completion)
ssh_terminal machine=web1 command="df -h"
# Background (returns immediately)
ssh_terminal machine=web1 command="tail -f /var/log/syslog" background=true
# With timeout
ssh_terminal machine=web1 command="make -j4" timeout=300
Output truncation: 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 file path is returned. The LLM can then use read_file to access the complete output.
Background commands: Long-running commands can be backgrounded. The plugin tracks the process and lets you poll for status or retrieve output later.
# Check if still running
ssh_terminal poll=<session_id>
# Read output from completed command
ssh_terminal read_output=<session_id>
# Or via ssh_sessions
ssh_sessions action=poll session_id=<session_id>
ssh_sessions action=read_output session_id=<session_id>
ssh_machines — Machine Registry
Register servers once, refer to them by name or alias.
# Add a server
ssh_machines action=add name=web1 host=192.168.1.50 user=deploy key=~/.ssh/id_ed25519
# Add with aliases and tags
ssh_machines action=add name=prod-web host=10.0.0.1 aliases=web1,tags=production,web
# List all machines
ssh_machines action=list
# Test connectivity
ssh_machines action=test name=web1
# Get full details
ssh_machines action=inspect name=web1
Machine names must be alphanumeric with dots, hyphens, or underscores (1-64 chars). Slashes, spaces, and glob characters are rejected.
ssh_sessions — Session Tracking
Every command creates a session. Sessions track the PID, machine, command count, and idle time.
# List active sessions
ssh_sessions action=list
# Kill a session (terminates the SSH process)
ssh_sessions action=kill session_id=<session_id>
# Clean up all idle sessions (>30 min)
ssh_sessions action=cleanup
# Remove old closed sessions (>24 hours)
ssh_sessions action=prune
Idle sessions are automatically killed by a background checker after 30 minutes. Closed sessions are pruned after 24 hours.
/ssh Slash Command
Quick access from chat without remembering tool names:
/ssh # List machines and sessions
/ssh web1 # Inspect a machine
/ssh web1 uptime # Run a command
/ssh web1 docker ps # Run a command
/ssh test # Test connectivity to all machines
/ssh cleanup # Kill all idle sessions
/ssh help # Show help
Configuration
All 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 |
root | 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 |
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 |
accept-new | SSH host key verification |
Architecture
src/ssh_tools/
├── __init__.py # Plugin registration + Hermes hooks
├── config.py # SSHConfig (immutable dataclass)
├── manager.py # SSHManager — all state and operations
├── models.py # Machine, Session dataclasses
├── schemas.py # Tool schemas (LLM-facing)
├── utils.py # ok(), err(), require() helpers
├── py.typed # PEP 561 marker
└── handlers/
├── terminal.py # ssh_terminal (execute, poll, read_output)
├── machines.py # ssh_machines (add/list/remove/test/inspect)
├── sessions.py # ssh_sessions (list/kill/cleanup/prune/poll/read_output)
└── slash.py # /ssh slash command
Key design decisions:
SSHManagerowns all state. Thread-safe. No module-level mutable state.- Handlers are thin closures — validate params, dispatch to manager, return JSON.
- JSON files use atomic writes (temp file +
os.replace) for crash safety. - Data directory has restricted permissions (0o700). Audit log and output files use 0o600.
- Machine names are validated to prevent path traversal and glob injection.
- Connection reuse via
ControlMasterwith 5-minute persist window.
Security
See SECURITY.md for the full picture.
Defaults you should know about:
StrictHostKeyChecking=accept-new— accepts first-seen host keys but rejects changed keys. Set toyesfor strict production hosts.- Machine credentials are encrypted at rest in
~/.hermes/ssh-tools/machines.json. Data directory is 0o700. - All commands execute with the permissions of the Hermes agent process.
Hardening applied:
- Machine names validated against
^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$ - Output files written with 0o600 permissions
- Audit log created with 0o600 permissions
- Atomic JSON writes prevent corruption on crash
- Orphaned temp files cleaned on startup
Requirements
- Python 3.11+
- OpenSSH client (
ssh) - Hermes Agent
Troubleshooting
Connection refused
The remote host may not be listening on the expected port, or a firewall is blocking the connection. Verify with ssh -v user@host outside of Hermes.
Permission denied (publickey)
The SSH key path stored in the machine registry may be incorrect, or the remote host doesn't have the corresponding public key in ~/.ssh/authorized_keys. Verify with ssh -i /path/to/key user@host.
Command timeout
Commands exceeding command_timeout (default 30s) are killed. Increase the timeout or use background=true for long-running work.
Output looks truncated
This is intentional — large outputs are saved under the restricted plugin output directory and a summary is returned. Use read_output or read_file on the returned path for the full output.
Session stuck as "active" after process died
If the agent restarted, background process references are lost. Use ssh_sessions action=cleanup to kill stale sessions, or ssh_sessions action=prune to remove old closed ones.
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]'
# Run checks
black --check src/ssh_tools/ tests/
mypy src/ssh_tools/
pytest
See CONTRIBUTING.md for guidelines.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hermes_ssh-0.3.1.tar.gz.
File metadata
- Download URL: hermes_ssh-0.3.1.tar.gz
- Upload date:
- Size: 66.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a12a4c394197c270ecb4ebae835ffb760711dfd890cf9c97908db4a2e7c78a40
|
|
| MD5 |
d7783bb073d9e763ea7eb3732bde1358
|
|
| BLAKE2b-256 |
14a26e7486aa21bf112243428aaba9ef1c3c7bb5353572c35a43cef9fadb60e4
|
Provenance
The following attestation bundles were made for hermes_ssh-0.3.1.tar.gz:
Publisher:
pypi-publish-ssh.yml on TheEpTic/hermes-plugins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_ssh-0.3.1.tar.gz -
Subject digest:
a12a4c394197c270ecb4ebae835ffb760711dfd890cf9c97908db4a2e7c78a40 - Sigstore transparency entry: 2158199847
- Sigstore integration time:
-
Permalink:
TheEpTic/hermes-plugins@60c290642cd215b3c1f0e73e4e0462247234aa55 -
Branch / Tag:
refs/tags/hermes-ssh-v0.3.1 - Owner: https://github.com/TheEpTic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish-ssh.yml@60c290642cd215b3c1f0e73e4e0462247234aa55 -
Trigger Event:
push
-
Statement type:
File details
Details for the file hermes_ssh-0.3.1-py3-none-any.whl.
File metadata
- Download URL: hermes_ssh-0.3.1-py3-none-any.whl
- Upload date:
- Size: 28.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a164540efbc3c330caececa5930f94b7f3419357feb3103878406d6ad849d8bb
|
|
| MD5 |
017839e5b808ae90ec99a1fd6d35866e
|
|
| BLAKE2b-256 |
8a5f3ecc359f1c2d30ceacb57d86e0d3d39402909f93ef1e7b1cc33830ec5657
|
Provenance
The following attestation bundles were made for hermes_ssh-0.3.1-py3-none-any.whl:
Publisher:
pypi-publish-ssh.yml on TheEpTic/hermes-plugins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hermes_ssh-0.3.1-py3-none-any.whl -
Subject digest:
a164540efbc3c330caececa5930f94b7f3419357feb3103878406d6ad849d8bb - Sigstore transparency entry: 2158199891
- Sigstore integration time:
-
Permalink:
TheEpTic/hermes-plugins@60c290642cd215b3c1f0e73e4e0462247234aa55 -
Branch / Tag:
refs/tags/hermes-ssh-v0.3.1 - Owner: https://github.com/TheEpTic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish-ssh.yml@60c290642cd215b3c1f0e73e4e0462247234aa55 -
Trigger Event:
push
-
Statement type: