Hermes
Run Hermes agents on macOS or Linux, each one confined to its own directory by a nono sandbox.
One bot is one declaration. The @skunk-ape/hermes/bot model type renders its
sandbox profile, environment library, entrypoints and service definitions from
that declaration, builds the workdir it runs in, loads it into the host's
service manager, and probes the running sandbox to report what it actually
enforces.
| Method | What it does |
|---|---|
scaffold |
Write a starter declaration for a new bot. |
render |
Write the sandbox profile, environment library, entrypoints and service definitions. |
provision |
Build the workdir: checkout, interpreter, venv, memory tooling, starter config. |
verify |
Probe the sandbox from inside and report what it enforces. |
install / uninstall |
Load and unload the bot's services. |
status / restart |
What is running, and put it back. |
snapshot / restore |
Move a bot to another machine without losing its memory. |
The @skunk-ape/hermes/fleet report renders one table across every bot in an
audit run, including a side-by-side comparison of their boundaries.
Two platforms, one declaration. Services are launchd user agents on macOS and
systemd user units on Linux; both unit files are rendered on both platforms so
the drift check stays meaningful on either machine. The confinement is nono —
Seatbelt on macOS, Landlock on Linux, where enforcing a network block needs
kernel 6.7+ and install refuses below it.
See README.md for the declaration format, the sandbox fields, and what each verification outcome means.
| Argument | Type | Description |
|---|---|---|
| slug | string | Slug of the NEW bot. Names its workdir, profile, launcher and launchd label. |
| displayName? | string | The new bot's name as a person says it. Defaults to the slug, capitalized. |
| vault? | string | Swamp vault for its secrets. Defaults to `<slug>-secrets`. One vault per bot is the point: a work credential and a personal one are never reachable from the same place. |
| force? | boolean | Overwrite an existing declaration. Off by default — a declaration is hand-edited after scaffolding, and that edit is the valuable part. |
| Argument | Type | Description |
|---|---|---|
| check? | boolean | Report what would change and write nothing. Use this to detect a generated file that has been edited by hand — the failure mode that makes a declaration stop being the source of truth. |
| outputDir? | string | Where to write, relative to the repository root. Empty means the repository root itself, which is what an ops repo wants: profiles/, bin/ and launchd/ land in their usual places. |
| Argument | Type | Description |
|---|---|---|
| check? | boolean | Report what is missing and change nothing. The honest answer to "is this workdir complete?", which `status` can only approximate from the outside. |
| hermesRepo? | string | Override the declaration's agent.repo for this run. Empty means use the declaration, which is where this belongs: a bot's Hermes is a property of the bot, not of the person who last ran provision. |
| hermesRef? | string | Override the declaration's agent.ref for this run. Empty means use the declaration. Cloned shallow, so a branch or tag, not an arbitrary commit. |
| pythonVersion? | string | Interpreter version uv installs INSIDE the workdir. Never the host's: uv's pythons resolve sys.prefix back to their install path, so a copied interpreter cannot run from anywhere else. |
| seedHfFrom? | string | Slug of a sibling bot whose Hugging Face embedding cache to copy. A bot with a named egress allowlist cannot download it, and widening the allowlist for a one-time fetch is the wrong trade. Empty means do not seed. |
| funesBin? | string | Host funes binary to stage into the runtime's tools/. Staged rather than shared so a bot's memory tooling cannot change under it when the host's copy updates. |
| restageFunes? | boolean | Replace the staged funes with the host's when they differ — how a bot moves to a newer funes after `funes update` on the host. Also re-fetches the funes integration and re-runs `funes add hermes`, which is what moves a bot from funes's shell hook to its Hermes plugin. |
| skipDeps? | boolean | Create the venv but do not sync dependencies. For a fast structural test of everything around the slowest step. |
| Argument | Type | Description |
|---|---|---|
| probePort? | number | An unlisted loopback port to bind a real listener on before probing. Without a listener the loopback check proves nothing — a closed port refuses the connection whether or not the sandbox blocks it — and reports VACUOUS instead. |
| deniedHost? | string | A host outside every bot\ |
| foreignPorts? | array | Loopback services on this host that belong to somebody else — typically another bot's credential-holding MCPs. Ports this bot declares are skipped, and so are ports with nothing listening: only a live listener can answer the loopback question, since a closed port refuses the connection either way. Empty means the probe asks about no foreign ports at all, which is the honest default on a machine this type knows nothing about. |
| Argument | Type | Description |
|---|---|---|
| out? | string | Where to write the archive. Empty means `<repo>/snapshots/<slug>-<timestamp>.tar.gz`. |
| includeHfCache? | boolean | Carry funes/hf as well. It is 1.2GB, architecture-independent, and re-obtainable — by download for a bot with open egress, and by `provision --input seedHfFrom=<sibling>` for one whose allowlist forbids huggingface.co. Off by default because including it makes the archive roughly forty times larger to save a step that already exists. |
| force? | boolean | Snapshot even though the gateway is still running. Off by default: state.db is written on every turn, and an archive taken mid-write carries a torn database. Stop the bot instead. |
| Argument | Type | Description |
|---|---|---|
| archive | string | Path to the archive written by `snapshot` on the other machine. |
| fromHome? | string | The HOME the snapshot was taken under, e.g. /Users/x. Every mention of `<fromHome>/agents/<slug>` in the files that are read to decide what to do — config.yaml above all — is rewritten to this host's workdir. Leave empty ONLY when both hosts share a home path: without it a restored config.yaml points the funes MCP at a directory that does not exist, and the gateway comes up healthy with no memory tool and nothing to say about it. |
| force? | boolean | Overwrite files that already exist and differ. Off by default, so restoring onto a workdir that has already been used reports the conflicts instead of silently resolving them. |
| Argument | Type | Description |
|---|---|---|
| stageOnly? | boolean | Write the unit files into the platform's unit directory (~/Library/LaunchAgents, or ~/.config/systemd/user) but do not load them. Use when installing on a machine you are not ready to start the bot on. |
| ignorePreflight? | boolean | Install even though the host reported a blocking problem. The Linux problems are a kernel too old for Landlock's network rules (open_port and allow_domain silently enforce nothing) and a user manager without lingering (every bot stops at logout). Both produce a deployment that reports healthy while enforcing less than it claims, which is why they block by default. Set this only when you have read the specific problem and decided to accept it. |
| Argument | Type | Description |
|---|---|---|
| only? | array | Which MCPs to bounce by name. Empty means all of them. The gateway is ALWAYS restarted regardless — Hermes parks an MCP whose connection drops and does not retry until a reconnect is requested, so bouncing a server on its own leaves the gateway holding a dead handle and the agent reporting it cannot reach something that is plainly running. |
Resources
Every Hermes bot on this machine in one table: what is running, how wide each sandbox is, and which claims about those sandboxes were actually proven.
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Docs keep secrets in a vault1/1earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License2/2earned
- Verified public repository2/2earned
Spawns a subprocess with Deno.Command().
- models/_lib/proc.ts:39 — no reason given
A field that looks like a secret but is not marked sensitive, so its value is not vaulted.
- models/_lib/decl.ts:96 — no reason given
- models/_lib/decl.ts:98 — no reason given
- models/_lib/decl.ts:100 — no reason given