Skip to content

The hermes container

hermes packages Hermes Agent — Nous Research's self-improving, multi-provider agentic CLI — inside a hardened container, with the same network-containment posture as claude-code: read-only root filesystem, a minimal Linux capability set, and outbound network access denied by default.

Like claude-code, two mechanisms control what the container can reach over the network — an in-container allowlist (the default) or gateway-client mode, tunnelling all traffic over SSH to a separate agent-gateway container. See Egress control below; the mechanism and env vars are identical to claude-code's, since both consume the same shared AGENT_*//etc/agent/* contract.

Build

The image's build context needs agent-images/shared supplied as a named Buildx build context:

docker build --build-context shared=agent-images/shared \
  -t hermes agent-images/hermes

Run

docker run -it --rm \
  --security-opt=no-new-privileges \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /run:exec \
  --cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SETUID --cap-add=SETGID --cap-add=CHOWN --cap-add=DAC_OVERRIDE \
  -v hermes-data:/opt/data \
  hermes

Capabilities

In the documentation below, upstream refers to the Hermes Agent official base image and its own release engineering, as distinct from the containment/hardening layer this repo's hermes image adds on top.

Flag Purpose
--cap-add=NET_ADMIN --cap-add=NET_RAW Same as claude-code — the entrypoint always installs a default-deny iptables/ip6tables policy, and domain-based allowlist matching needs NET_RAW.
--cap-add=SETUID --cap-add=SETGID Upstream's own privilege-drop mechanism (s6-setuidgid, its equivalent of gosu), not a claude-style entrypoint drop — this image has none.
--cap-add=CHOWN Upstream's first-boot hook (stage2-hook.sh) chowns the freshly-created /opt/data volume from root to the hermes user; needs CAP_CHOWN under --cap-drop=ALL. Neither claude-code nor agent-gateway need this.
--cap-add=DAC_OVERRIDE Required for s6-supervise/s6-rc to open lock files for services (main-hermes, dashboard) that upstream's 02-reconcile-profiles cont-init step registers dynamically, after the static service trees are already chowned.

--tmpfs /run:exec, not --tmpfs /run

Docker's default --tmpfs /run mounts noexec. Upstream's s6-overlay boot sequence execs a staged binary from /run/s6/basedir/bin/init, and fails with Permission denied (exit 126) without the exec mount option — so hermes, unlike claude-code, needs /run mounted executable.

A hardening constraint: no UID/GID remap

Upstream supports remapping the hermes user to an arbitrary UID/GID at boot via HERMES_UID/HERMES_GID (or PUID/PGID), which requires a writable rootfs (usermod/groupmod) — incompatible with --read-only. This image never sets those variables: the container always runs as the image's baked-in UID 10000, and --read-only is kept. Unlike claude-code (where the in-container UID is chosen at build time to match a host user), hermes's container-side UID is fixed.

Persistent state

A named volume at /opt/data (upstream's HERMES_HOME) — analogous to claude-home, but at the path upstream dictates. Config (config.yaml, auth.json), skills, memories, session/cron state, and lazy-installed optional-provider packages all persist there across container recreation.

Provider / auth configuration

Hermes supports dozens of LLM and messaging-platform providers via env vars or hermes setup (which writes into the persisted config.yaml/ auth.json):

docker run -it --rm \
  --security-opt=no-new-privileges \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /run:exec \
  --cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SETUID --cap-add=SETGID --cap-add=CHOWN --cap-add=DAC_OVERRIDE \
  -v hermes-data:/opt/data \
  -e OPENROUTER_API_KEY=replace-with-your-key \
  hermes

See upstream's own .env.example and documentation for the full provider list rather than duplicating it here.

Out of scope for this image: mounting /var/run/docker.sock (leave terminal.backend at its local default — this container already is the sandbox), and a plugins.txt-equivalent build-time skill seeding mechanism (Hermes has its own skills/MCP ecosystem, unrelated to Claude Code's plugin marketplaces).

Custom configuration file

Hermes treats /opt/data/config.yaml as mutable provider state and persists model changes to it. Keep that file in hermes-data rather than mounting the individual file read-only. Keep provider credentials in runtime environment variables or Hermes's persisted authentication state. See custom configuration files for the general state constraints and local models for the version-recorded custom-endpoint candidate.

Optional configuration

  • packages-apt.txt / packages-npm.txt / tools-uv.txt / packages-uv.txt — general (non-agent) software to install at build time, one package per line. packages-apt.txt (apt-get install) ships prefilled with common CLI tools (gh, jq, ripgrep, fd-find, tree, unzip, less, file, lsof, yq, miller); the rest ship empty. packages-npm.txt runs npm install -g. tools-uv.txt runs uv tool install, one isolated venv per entry, for standalone Python CLI tools (e.g. ruff) — only that entry's own console-script ends up on PATH, nothing importable lands anywhere shared. packages-uv.txt runs uv pip install --system into the image's system Python instead, for plain importable libraries with no console-script of their own (e.g. langfuse) that need to be importable by whatever runs inside the image. Edit any of the four and rebuild the image to change what's installed.

The image also sets UV_LINK_MODE=copy — see claude-code's uv cache notes for why.

Egress control

In-container allowlist

The default mode, identical in mechanism to claude-code's: AGENT_ALLOWED_EGRESS (or a mounted /etc/agent/egress-allowlist.txt, which takes precedence) against the container's own OUTPUT chain, defaulting to deny-all if neither is set.

To supply a host-maintained policy file instead, add -v "/path/to/egress-allowlist.txt":/etc/agent/egress-allowlist.txt:ro to the command.

docker run -it --rm \
  --security-opt=no-new-privileges \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /run:exec \
  --cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SETUID --cap-add=SETGID --cap-add=CHOWN --cap-add=DAC_OVERRIDE \
  -v hermes-data:/opt/data \
  -e 'AGENT_ALLOWED_EGRESS=openrouter.ai,your-provider.example.com' \
  hermes

For unrestricted egress, use -e 'AGENT_ALLOWED_EGRESS=*'. Quoting prevents shells such as zsh from treating * as a filename pattern.

Gateway-client mode

Setting AGENT_GATEWAY_HOST switches hermes from the in-container allowlist to gateway-client mode — sshuttle tunnels all outbound traffic to an agent-gateway container, exactly as described in claude-code's Gateway-client mode. The same AGENT_GATEWAY_PORT/AGENT_GATEWAY_USER/ AGENT_GATEWAY_BOOTSTRAP_ALLOW/AGENT_GATEWAY_ACCESS_HOSTNAME variables and /etc/agent/gateway-key//etc/agent/gateway-known-hosts mounts apply unchanged — see that page for the full configuration table and worked examples (same-host sibling, genuinely remote gateway, Cloudflare Tunnel).