hermes-box
| Name | Last commit date |
|---|---|
| .agents | 4 months ago |
| .github | 4 months ago |
| backups | 4 months ago |
| bin | 4 months ago |
| cmd | 4 months ago |
| guest | 3 months ago |
| images | 4 months ago |
| internal | 4 months ago |
| state | 4 months ago |
| tests | 3 months ago |
| .gitignore | 4 months ago |
| AGENTS.md | 4 months ago |
| EXECUTOR_CONNECTIONS.md | 4 months ago |
| go.mod | 4 months ago |
| hermes-box.conf.example | 4 months ago |
| Makefile | 4 months ago |
| network-hosts.txt | 4 months ago |
| PORTABLE_RESTORE.md | 4 months ago |
| README.md | 4 months ago |
| secret-env.txt.example | 4 months ago |
| Smolfile | 4 months ago |
Hermes Box
Hermes Box runs Hermes Agent inside a persistent, isolated smolvm machine.
It is designed for running an autonomous agent without exposing host files, the host SSH agent, Docker, or a GPU. Hermes gets its own persistent workspace, while the host retains an out-of-band root console and can stop or restore the machine regardless of what happens inside it.
What You Get
Ubuntu 26.04 VM
├── boxadmin Key-only SSH entry account
├── hermes Agent account with passwordless full sudo
├── codex Interactive Codex CLI/TUI, full access inside VM
├── node + npm Current Node.js 24 LTS toolchain
├── tmux Blessed persistent `main` terminal session
├── sshd Bound to host loopback only
├── supervisord
│ ├── optional native Executor self-host service
│ └── hermes gateway run
└── /workspace Private persistent disk
├── hermes-home/ Auth, config, sessions, memory, skills, logs
├── codex-home/ Codex binary, auth, config, sessions, skills, logs
├── executor/data/ Optional Executor database and secret store
└── work/ Hermes working directory
Interactive SSH logins authenticate as boxadmin, switch to hermes, and
immediately create or attach the tmux session named main in /workspace/work.
Noninteractive SSH commands remain explicit and predictable.
Security Boundaries
- No host directories are mounted.
- No host SSH agent, Docker socket, or GPU is exposed.
- SSH accepts only the generated project key.
- Root login, passwords, forwarding, tunnels, and X11 are disabled.
- SSH is published only on
127.0.0.1/::1; startup aborts otherwise. - Hermes has passwordless full
sudoinside the isolated VM. - Host root recovery remains available through
./bin/hermes-box shell. - Snapshots contain the full VM and may contain credentials.
This protects the host filesystem. With networking enabled, Hermes can still reach internet and LAN services and can transmit information it knows.
Compatibility
The current implementation is tested with:
- macOS on ARM64
- Go
1.24or newer - smolvm
1.0.4 - Ubuntu
26.04 - Hermes Agent
0.17.0
Hermes is pinned to commit
2bd1977d8fad185c9b4be47884f7e87f1add0ce3. The pin is a security boundary:
the guest provisioning applies a narrow gated-approval source extension whose
upstream anchors are verified against that revision. Provisioning also fetches
that commit's installer by immutable URL and verifies its SHA-256 digest. The
managed uv binary is pinned to 0.11.23 by release-archive digest after
bounded cold- and warm-cache Hermes builds under smolvm. This qualification is
required because 0.11.22 previously deadlocked while building the project.
Fresh builders also retry guest DNS and package-index readiness with a bounded
deadline before installing anything. If smolvm reports networking enabled but
boots a disposable builder without an IPv4 route, Hermes Box cycles that
builder before provisioning and fails after three unhealthy boots. Builders
explicitly use smolvm's virtio-net backend rather than its portless TSI
default so package installation gets a real guest NIC and DNS path. Hermes Box
also raises smolvm 1.0.4's extraction-cache ceiling for runtime creation and
verifies the packed-layer marker immediately, preventing that version's cache
eviction from surfacing later as a failed first boot.
The Ubuntu base selection applies when init builds a new image. Existing
Ubuntu 24.04 machines are not upgraded in place, and restoring a snapshot
retains the snapshot's archived root filesystem. To move a box itself to
Ubuntu 26.04, create a new box with init under distinct machine names and
ports, then migrate the needed workspace state deliberately. Keep a package of
the old box for rollback; restoring that package is recovery, not an OS
upgrade.
Required host commands:
go 1.24+ smolvm ssh ssh-keygen lsof
The launcher needs Go 1.24 or newer to build the host CLI on its first run.
Check go version before invoking it. Once the CLI starts, init validates Go
again along with every other requirement before it creates a key or VM. If Go
is missing or old, install a supported macOS ARM64 release from
https://go.dev/dl/. If smolvm is missing or has another version, install the
official Darwin ARM64 asset from the
v1.0.4 release.
Rerun the same probe or init command after fixing the prerequisite; preflight
failures are safe to resume.
The host CLI is written in Go. The small bin/hermes-box launcher builds a
private cached binary under state/ when the Go sources change, then executes
it. Guest provisioning remains in Bash because it is native Ubuntu
system-administration work.
Quick Start
Clone the repository:
git clone https://github.com/davis7dotsh/hermes-box.git
cd hermes-box
Full outbound networking is the default. The generic “set up a new Hermes Box” agent workflow enables Executor so tools and integrations are available; ask for a lean box to omit it. For a manual setup, decide before creating the machine because adding Executor later requires recreating the box:
cp hermes-box.conf.example hermes-box.conf
sed -i '' \
's/HERMES_BOX_EXECUTOR_ENABLED=false/HERMES_BOX_EXECUTOR_ENABLED=true/' \
hermes-box.conf
Otherwise no configuration file is required.
Create and start the box:
./bin/hermes-box init
init creates a temporary networked builder, installs Hermes and the guest
services, packages a reusable base image, deletes the builder, and starts the
real runtime box. Before changing anything, it checks macOS ARM64 support,
exact Go and smolvm versions, secret mappings and referenced host values,
data-directory writes, requested ports, and machine-name collisions. Its final
output gives the exact guest sign-in phase and host-side Executor, backup,
shutdown, and debugging handoff for the selected machine and ports.
If the first runtime boot fails after creation, init prints diagnostics and
preserves the stopped machine so ./bin/hermes-box start can resume partial
first-boot work, including a completed Codex setup and already verified
Executor OCI blobs. Use destroy --force followed by init only when you
intend to discard that private partial state and rebuild from scratch.
The generated SSH key, base image, local configuration, snapshots, and runtime state are ignored by Git.
Configure Hermes
Open an interactive SSH session:
./bin/hermes-box ssh
You should land in the persistent main tmux session without running sudo:
main:0 bash
Authenticate OpenAI Codex once for the gated reviewer and for inference when you choose a ChatGPT/Codex subscription model:
hermes auth add openai-codex --type oauth
Then configure inference:
hermes model
For ChatGPT/Codex subscription inference:
- Select OpenAI.
- Accept the Codex/ChatGPT subscription option; Hermes reuses the saved OAuth credential instead of starting a duplicate device flow.
- Select a model.
Start chatting:
hermes
The messaging gateway is managed by Supervisor because Hermes Box does not boot systemd. After changing Discord or other gateway configuration, reload it with:
sudo supervisorctl restart hermes
sudo supervisorctl status hermes
cat /workspace/hermes-home/gateway_state.json
Do not run hermes gateway install or hermes gateway start in the box. The
upstream hermes gateway status command checks systemd and may label the
Supervisor-managed process as manual even when it is healthy. Supervisor,
gateway_state.json, and the gateway log are authoritative.
Hermes authentication and configuration live entirely inside
/workspace/hermes-home.
Command Approval Gate
New Hermes Box images install a conservative, one-command permission reviewer
on top of Hermes 0.17.0. Stock Hermes supports manual, smart, and off; the
gated mode here is a source extension, not a YAML-only setting. Provisioning
therefore follows a hard order:
- Install the pinned Hermes source revision.
- Verify and patch exact upstream anchors in
tools/approval.pyandgateway/run.py. - Compile the patched files and run the gated regression suite.
- Only then seed
approvals.mode: gatedin the persistent profile.
Any commit mismatch, missing anchor, compile failure, or regression failure
aborts image creation. The resulting gate sends bounded, secret-redacted turn
context to openai-codex / gpt-5.5 with low reasoning and maps configured
fast service to the Responses API priority tier. It auto-approves only a
single invocation when scope is once, risk is at most medium, and
confidence is at least 0.75. It never writes session or permanent approval
state. Malformed output, reviewer errors, timeouts, unknown/excessive risk,
unsupported scope, and low confidence all fall through to the existing human
approval flow with the reviewer reason attached. An explicit reviewer denial
blocks the command, while Hermes' deterministic hardline blocklist remains
authoritative before the model is called. Cron stays fail-closed with
cron_mode: deny.
The seeded profile block is:
approvals:
mode: gated
timeout: 60
cron_mode: deny
gateway_timeout: 300
gate:
enabled: true
provider: openai-codex
model: gpt-5.5
reasoning_effort: low
service_tier: fast
timeout: 30
scope: once
min_confidence: 0.75
max_context_chars: 12000
auto_approve_max_risk: medium
escalate_on_error: true
escalate_on_low_confidence: true
security:
redact_secrets: true
tirith_enabled: true
The gate uses Hermes' existing Codex OAuth credentials. If that provider has
not been authenticated yet, reviewer calls safely escalate to the normal human
approval. The hermes auth add openai-codex --type oauth step under Configure
Hermes authenticates it before model selection, avoiding a duplicate device
flow when inference also uses the ChatGPT/Codex subscription.
Hermes resolves the saved credentials on demand; no Supervisor restart is needed after this authentication step.
Do not change HERMES_BOX_HERMES_COMMIT casually. Port the patch against the
new revision and rerun its regression suite before updating the pin.
Configure Codex
Codex 0.141.0 is installed from its official standalone release archive,
verified by SHA-256, including the interactive TUI. Its persistent standalone
layout remains compatible with codex update. Open the box and sign in using
the device flow:
./bin/hermes-box ssh
codex login --device-auth
codex
Codex defaults to approval_policy = "never" and
sandbox_mode = "danger-full-access". This is the persistent equivalent of
--yolo: Codex can autonomously read, write, execute, and use the network
inside the VM, while the Hermes Box boundary still isolates it from the host.
The default /workspace/work directory is pre-trusted. The hermes account
has passwordless full sudo inside the VM; the smolvm boundary protects the
host rather than restricting the agent within its box.
Codex's executable, login cache, configuration, sessions, and update metadata
live under /workspace/codex-home, so snapshots preserve them together. Update
the standalone installation as the hermes user:
codex update
codex --version
Interactive SSH already runs inside the persistent main session. From any
Hermes shell, create or return to that exact session with:
tm
codex
Detach with Ctrl-b, then d. The SSH connection exits after a normal detach;
the next ./bin/hermes-box ssh reattaches. The dark-green status bar supports
mouse clicks for window selection. With the keyboard, use Ctrl-b n and
Ctrl-b p for the next and previous windows, or Ctrl-b followed by a window
number. Ghostty metadata, true color, clipboard escape sequences, focus events,
and extended keys are passed through; tmux 3.5 and newer uses CSI-u so
Shift+Enter remains distinct in TUIs such as Codex.
The login cache is stored as /workspace/codex-home/auth.json. Treat snapshots
and portable packages as credentials after signing in.
Optional Executor Gateway
Hermes Box can run a digest-pinned
Executor service inside the VM.
Enable it before init:
HERMES_BOX_EXECUTOR_ENABLED=true
HERMES_BOX_EXECUTOR_PORT=4788
Put those assignments in hermes-box.conf (or export them in the shell that
runs init).
After the box starts, open the portal:
./bin/hermes-box executor open
Executor's UI and MCP endpoint share that loopback-only port. In the portal, create the first admin account, configure the provider integrations and OAuth connections you need, then create a destination-local API key. Follow EXECUTOR_CONNECTIONS.md for provider prerequisites. Store the API key in the per-machine macOS Keychain entry; it is prompted for securely and is never passed as a command-line argument:
./bin/hermes-box executor auth set
The host CLI can inspect the service without exposing secrets. These are most useful for diagnosis after setup:
./bin/hermes-box executor status
./bin/hermes-box executor status --sizes
./bin/hermes-box executor logs -f
./bin/hermes-box executor connections
./bin/hermes-box executor tools
./bin/hermes-box executor mcp-test
The default executor status is intentionally fast and skips recursive runtime
and data-directory size scans. Add --sizes only when you need that disk-usage
detail.
Connection creation, OAuth, and policy changes intentionally stay in the web
portal. This makes browser-based setup through Computer Use straightforward:
open the portal with the CLI, create the connection, follow the provider's OAuth
flow in the same browser session, and use the exact callback URL displayed by
Executor. For Google testing, a localhost HTTP redirect is appropriate, but the
OAuth client's authorized redirect URI must exactly match Executor's displayed
value. Confirm the result afterward with executor connections and
executor tools. See EXECUTOR_CONNECTIONS.md for
provider-dashboard credential retrieval, secret handoff, policy, and validation
steps for Google, YouTube, X, Discord, Airtable, GitHub, and Notion.
After creating the needed portal integrations, register the in-VM endpoint with Hermes:
./bin/hermes-box executor connect-hermes
This validates from inside the guest that the MCP server exposes only execute
and resume before changing Hermes' persistent configuration. It then writes
the bearer-token reference, copies the normalized token into
/workspace/hermes-home/.env over SSH stdin, runs hermes mcp test executor,
and restarts the supervised Hermes gateway. The token therefore exists in both
the host Keychain and Hermes' guest credential store, both scoped to this box.
Start a new Hermes CLI session after the command returns so it discovers the
new MCP tools.
Hermes Box does not run nested Docker. On first boot, skopeo copies the
digest-pinned ARM64 image and a constrained extractor installs its application
plus bundled Bun runtime under disposable
/workspace/.hermes-box-runtime/executor.
Supervisor then runs the published self-host payload natively before Hermes.
Executor's database and generated secret keys live under
/workspace/executor/data, so normal snapshots preserve them while excluding
the repullable 2.5 GB runtime. The service keeps
EXECUTOR_ALLOW_LOCAL_NETWORK=false; smolvm remains the only host port
publisher.
The Executor launcher also sets
BUN_FEATURE_FLAG_DISABLE_IPV6=1. smolvm 1.0.4 can present a default IPv6
route even when external IPv6 traffic is blackholed, and Bun 1.3.14's HTTP
client may wait on that path instead of falling back to working IPv4. The flag
is scoped to Executor and keeps its Google, YouTube, and Notion requests on the
working IPv4 path. Remove it only after the pinned Bun runtime has been proven
to fall back correctly in this topology.
Image layers are verified by SHA-256, rejected if they contain unsafe paths, and applied with OCI whiteout handling. Downloads and extractions are prepared away from the active runtime, and interrupted work is never activated; the completed runtime is selected with an atomic symlink update. An Executor-enabled startup has a two-hour outer safety bound, while the image pull itself stops after ten minutes without writing OCI data instead of discarding an actively downloading large layer at a shorter fixed deadline. A retry reuses completed verified blobs; partially transferred blob bytes are not resumable.
Executor provides account and tool policy enforcement, but it is not a
credential-isolation boundary: the hermes user has passwordless root inside
the same VM. Use a separate machine if Executor credentials must be hidden from
Hermes itself.
Daily Commands
Run these from the repository root:
./bin/hermes-box start
./bin/hermes-box stop
./bin/hermes-box ssh
./bin/hermes-box logs -f
./bin/hermes-box package configured-agent
Use status for a health summary and shell for the host-controlled root
console when SSH is unavailable:
./bin/hermes-box status
./bin/hermes-box shell
Run a noninteractive command as Hermes:
./bin/hermes-box ssh 'sudo -iu hermes hermes status'
Display the advanced restore, snapshot, restart, and Executor commands:
./bin/hermes-box help
Snapshots
Create a consistent snapshot:
./bin/hermes-box snapshot configured-and-working
The wrapper:
- Stops supervised services.
- Archives the merged root filesystem.
- Archives
/workspace. - Rejects snapshots containing tar warnings.
- Records the stable SSH-key fingerprint and writes SHA-256 checksums.
- Restarts the machine if it was previously running.
Snapshots are stored under backups/*.hermesbox.
A completed snapshot is retained and its path is reported if restarting the
machine afterward fails.
Restore:
./bin/hermes-box restore \
backups/hermes-box-YYYYMMDD-HHMMSS-configured-and-working.hermesbox
When the target machine does not exist, restore creates it directly and runs the complete health verification once. When replacing an existing machine, restore first takes a safety snapshot and validates a temporary candidate on a random loopback port; it replaces the primary only after that candidate passes.
Keep images/hermes-base.smolmachine with your backups. Restore requires it.
Portable Backups
Create a self-contained portable archive:
Document preview limited. Open the file to continue