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.
Ubuntu 24.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 Persistent terminal sessions for Codex and Hermes
├── 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 and immediately enter a
login shell as hermes. Noninteractive SSH commands remain explicit and
predictable.
- 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.
The current implementation is tested with:
- macOS on ARM64
- smolvm
1.0.4 - Ubuntu
24.04 - Hermes Agent
0.16.0
Hermes is pinned to commit
81eaedd0f5c471c7ee748990066135a684f3c962. 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.21 by release-archive digest because
0.11.22 reproducibly deadlocks while building Hermes under smolvm 1.0.4.
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.
Required host commands:
go 1.24+ smolvm ssh ssh-keygen lsof
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.
Clone and configure:
git clone https://github.com/davis7dotsh/hermes-box.git
cd hermes-box
cp hermes-box.conf.example hermes-box.confEdit hermes-box.conf and explicitly enable networking:
HERMES_BOX_NETWORK_MODE=fullTo install the optional Executor gateway at the same time, also set:
HERMES_BOX_EXECUTOR_ENABLED=trueBuild the base image and start the runtime machine:
./bin/hermes-box init
./bin/hermes-box statusinit 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.
The generated SSH key, base image, local configuration, snapshots, and runtime state are ignored by Git.
Open an interactive SSH session:
./bin/hermes-box sshYou should land here without running sudo:
hermes@container:/workspace/work$
Configure inference:
hermes modelFor ChatGPT/Codex subscription authentication:
- Select OpenAI.
- Accept the Codex/ChatGPT subscription option.
- Open the displayed device-login URL on the host.
- Enter the one-time code.
- Return to the terminal and select a model.
Check the installation and start chatting:
hermes status
hermes doctor
hermes tools
hermesKeep an interactive session alive across SSH disconnects:
tmux new -As codex
codexDetach with Ctrl-b, then d. Reconnect and run tmux new -As codex again
to reattach to the same session.
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.jsonDo 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.
New Hermes Box images install a conservative, one-command permission reviewer
on top of Hermes 0.16.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: trueThe gate uses Hermes' existing Codex OAuth credentials. If that provider has not been authenticated yet, reviewer calls safely escalate to the normal human approval. Authenticate interactively inside the box when ready:
hermes login --provider openai-codex
sudo supervisorctl restart hermesDo not change HERMES_BOX_HERMES_COMMIT casually. Port the patch against the
new revision and rerun its regression suite before updating the pin.
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
codexCodex 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 --versionThe login cache is stored as /workspace/codex-home/auth.json. Treat snapshots
and portable packages as credentials after signing in.
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=4788After the box starts, open the portal and create the first admin account:
./bin/hermes-box executor openExecutor's UI and MCP endpoint share that loopback-only port. Create an API key in the portal, then store it in the per-machine macOS Keychain entry. The key is prompted for securely and is never passed as a command-line argument:
./bin/hermes-box executor auth set
./bin/hermes-box executor mcp-testThe host CLI can inspect the service without exposing secrets:
./bin/hermes-box executor status
./bin/hermes-box executor status --json
./bin/hermes-box executor logs -f
./bin/hermes-box executor connections
./bin/hermes-box executor connections --json
./bin/hermes-box executor tools
./bin/hermes-box executor tools "calendar events" --namespace googleConnection 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.
Once the MCP test passes, register the in-VM endpoint with Hermes:
./bin/hermes-box executor connect-hermesThis 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. Interrupted downloads and extractions remain in temporary directories and are not activated; the completed runtime is selected with an atomic symlink update.
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.
Run these from the repository root:
./bin/hermes-box start
./bin/hermes-box stop
./bin/hermes-box restart
./bin/hermes-box status
./bin/hermes-box ssh
./bin/hermes-box logs
./bin/hermes-box logs -f
./bin/hermes-box executor open
./bin/hermes-box executor status
./bin/hermes-box executor logs -f
./bin/hermes-box executor auth status
./bin/hermes-box executor connections
./bin/hermes-box executor tools
./bin/hermes-box executor mcp-test
./bin/hermes-box executor connect-hermes
./bin/hermes-box package configured-agentOpen the host-controlled root console:
./bin/hermes-box shellRun a noninteractive command as Hermes:
./bin/hermes-box ssh 'sudo -iu hermes hermes status'Display every command:
./bin/hermes-box helpCreate a consistent snapshot:
./bin/hermes-box snapshot configured-and-workingThe wrapper:
- Stops supervised services.
- Archives the merged root filesystem.
- Archives
/workspace. - Rejects snapshots containing tar warnings.
- 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.hermesboxRestore first validates a temporary candidate on a random loopback port. It replaces the primary machine only after that candidate passes health checks, and it takes a safety snapshot of the current machine before starting.
Keep images/hermes-base.smolmachine with your backups. Restore requires it.
Create a self-contained portable archive:
./bin/hermes-box package configured-agentpackage takes a fresh consistent snapshot, then bundles:
- The runnable Hermes Box project
images/hermes-base.smolmachine- The new
backups/*.hermesboxsnapshot - The dedicated SSH private and public keys
- A portable
hermes-box.confusing repository-local data directories
For Executor-enabled boxes, the generated configuration preserves whether
Executor is enabled, its loopback port, and the exact digest-pinned image.
Executor's database, integrations, policies, OAuth tokens, API credentials,
and Hermes MCP token are carried inside the /workspace snapshot.
It writes both files under backups/:
hermes-box-portable-YYYYMMDD-HHMMSS-configured-agent.tar
hermes-box-portable-YYYYMMDD-HHMMSS-configured-agent.tar.sha256
These files include the machine SSH identity and may include Hermes OAuth tokens, API keys, sessions, memories, and generated work. Encrypt portable archives at rest.
The source Mac's Keychain entries and browser sessions are not included. The
repullable Executor runtime under /workspace/.hermes-box-runtime is also
excluded, so the destination needs outbound network access on first start to
fetch the pinned runtime again. After restore, add a destination-local Executor
API key with
./bin/hermes-box executor auth set if host-side management commands are
needed; Hermes' in-guest Executor connection is restored from the snapshot.
On another compatible host, install the prerequisites and restore without reconfiguring Hermes:
shasum -a 256 -c hermes-box-portable-*.tar.sha256
tar -xpf hermes-box-portable-*.tar
cd hermes-box
./bin/hermes-box restore backups/*.hermesbox
./bin/hermes-box status
./bin/hermes-box executor status
./bin/hermes-box ssh \
'sudo -iu hermes env HERMES_HOME=/workspace/hermes-home hermes mcp test executor'Host environment variables referenced by an optional secret-env.txt must
still exist on the restore host. See PORTABLE_RESTORE.md.
HERMES_BOX_NETWORK_MODE accepts:
full: unrestricted outbound networkingnone: rejected on smolvm 1.0.4strict: rejected on smolvm 1.0.4
Live testing found that smolvm 1.0.4 hostname allowlists could be bypassed by direct-IP or unlisted-host traffic, and its no-network options still allowed external HTTPS. Hermes Box therefore fails closed instead of presenting those settings as meaningful containment.
Use full only when unrestricted VM egress is acceptable.
Copy hermes-box.conf.example to hermes-box.conf and adjust:
HERMES_BOX_MACHINE_NAME=hermes-box
HERMES_BOX_BUILDER_NAME=hermes-builder
HERMES_BOX_SSH_PORT=2222
HERMES_BOX_CPUS=4
HERMES_BOX_MEMORY_MIB=8192
HERMES_BOX_STORAGE_GB=15
HERMES_BOX_OVERLAY_GB=6
HERMES_BOX_NETWORK_MODE=full
HERMES_BOX_HERMES_COMMIT=81eaedd0f5c471c7ee748990066135a684f3c962
HERMES_BOX_EXECUTOR_ENABLED=false
HERMES_BOX_EXECUTOR_PORT=4788HERMES_BOX_EXECUTOR_IMAGE defaults to the reviewed v1.5.12 multi-platform OCI
image pinned by digest. Overrides are accepted only when they also contain an
explicit tag and full SHA-256 digest. The guest always resolves and verifies
the Linux ARM64 child image.
Explicit HERMES_BOX_* environment variables override the config file. This
makes disposable test machines safe to run with different names and ports.
Configuration files are parsed as assignments rather than executed as shell
code. Plain, single-quoted, double-quoted, and optional export assignments are
supported.
Set HERMES_BOX_DATA_DIR to keep images/, backups/, and state/ under a
different directory. The disposable lifecycle suite uses a temporary data
directory so it cannot replace primary recovery artifacts.
Optional smolvm host-secret mappings can be placed in secret-env.txt; use
secret-env.txt.example as the template.
Run static and syntax checks:
./tests/static.shRun the complete local check suite:
make checkRun a disposable lifecycle test:
HERMES_BOX_E2E=1 \
HERMES_BOX_MACHINE_NAME=hermes-box-test \
HERMES_BOX_BUILDER_NAME=hermes-builder-test \
HERMES_BOX_SSH_PORT=2223 \
HERMES_BOX_EXECUTOR_ENABLED=true \
HERMES_BOX_EXECUTOR_PORT=4789 \
HERMES_BOX_NETWORK_MODE=full \
./tests/lifecycle.shNever reuse the primary machine name, builder name, SSH port, or Executor port for destructive tests. The script creates and removes its own isolated data directory.
hermes-box/
├── bin/hermes-box
├── cmd/hermes-box/
├── internal/
│ ├── app/
│ ├── config/
│ └── process/
├── guest/
│ ├── bootstrap.sh Installs Hermes and seeds persistent state
│ ├── boxadmin.bash_profile
│ ├── hermes-box.sudoers Grants the agent full passwordless sudo
│ ├── executor.sh Installs and runs the pinned Executor payload
│ ├── extract-executor.py Safely applies the selected OCI image layers
│ ├── hermes_gated_approval.py Conservative one-shot reviewer module
│ ├── patch-hermes-gated-approval.py Strict pinned-source installer
│ ├── install-node.sh Installs the latest checksum-verified Node 24 LTS
│ ├── restore.sh
│ ├── snapshot.sh
│ ├── start.sh Installs Codex once, then starts services
│ ├── supervisord.conf
│ └── workspace-seed.sh
├── tests/
├── Makefile
├── Smolfile
├── hermes-box.conf.example
├── network-hosts.txt
├── secret-env.txt.example
├── images/
├── backups/
└── state/
The Hermes browser and Node-based Hermes TUI payloads are intentionally removed from the base image to keep smolvm pack and restore operations reliable. The native Codex TUI is installed into the persistent workspace on first boot. Hermes CLI, inference, skills, messaging, gateway, and web-search tooling remain available.
The Go CLI preserves the hermes-box-v2 backup format and can restore snapshots
created by the original Bash host wrapper.