Skip to main content

Hermes

@skunk-ape/hermesv2026.10.25.1· MIT· 1d agoBEHAVIORSREPORTSACCEPTED WARNINGS
01README

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.

02Behaviors1
@skunk-ape/hermes/botv2026.09.22.1hermes.ts
fn scaffold(slug: string, displayName?: string, vault?: string, force?: boolean)
Write a starter declaration for a NEW bot: every field present, safe defaults filled in, and a comment on each decision a person has to make. It does not render or install anything — the declaration is meant to be edited first.
ArgumentTypeDescription
slugstringSlug of the NEW bot. Names its workdir, profile, launcher and launchd label.
displayName?stringThe new bot's name as a person says it. Defaults to the slug, capitalized.
vault?stringSwamp 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?booleanOverwrite an existing declaration. Off by default — a declaration is hand-edited after scaffolding, and that edit is the valuable part.
fn render(check?: boolean, outputDir?: string)
Render this bot's nono profile, environment library, entrypoints and service definitions — a launchd plist and a systemd unit, both on both platforms — from the declaration. With `check: true`, report what would change and write nothing.
ArgumentTypeDescription
check?booleanReport 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?stringWhere 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.
fn provision(check?: boolean, hermesRepo?: string, hermesRef?: string, pythonVersion?: string, seedHfFrom?: string, funesBin?: string, restageFunes?: boolean, skipDeps?: boolean)
Build this bot's workdir payload: directory tree, Hermes checkout, an interpreter and venv INSIDE the workdir, the staged funes binary and its index hook, and a starter config.yaml and SOUL.md. Idempotent — every step skips what it finds, and nothing the runtime owns is ever overwritten.
ArgumentTypeDescription
check?booleanReport what is missing and change nothing. The honest answer to "is this workdir complete?", which `status` can only approximate from the outside.
hermesRepo?stringOverride 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?stringOverride 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?stringInterpreter 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?stringSlug 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?stringHost 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?booleanReplace 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?booleanCreate the venv but do not sync dependencies. For a fast structural test of everything around the slowest step.
fn verify(probePort?: number, deniedHost?: string, foreignPorts?: array)
Probe this bot's sandbox from inside it, using assertions built from its own declaration. Reports PASS, FAIL, VACUOUS (the probe proved nothing) or GAP (a known, documented hole).
ArgumentTypeDescription
probePort?numberAn 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?stringA host outside every bot\
foreignPorts?arrayLoopback 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.
fn snapshot(out?: string, includeHfCache?: boolean, force?: boolean)
Archive everything about this bot that another machine cannot rebuild — state.db, the Funes memory, cron, skills, SOUL.md and the live config.yaml — and nothing that it can. Refuses while the gateway is running, because state.db is written on every turn.
ArgumentTypeDescription
out?stringWhere to write the archive. Empty means `<repo>/snapshots/<slug>-<timestamp>.tar.gz`.
includeHfCache?booleanCarry 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?booleanSnapshot 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.
fn restore(archive: string, fromHome?: string, force?: boolean)
Unpack a snapshot into this host's workdir and rewrite the old machine's absolute paths. Run it AFTER provision: it restores state, it does not build a runtime.
ArgumentTypeDescription
archivestringPath to the archive written by `snapshot` on the other machine.
fromHome?stringThe 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?booleanOverwrite 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.
fn install(stageOnly?: boolean, ignorePreflight?: boolean)
Substitute the unit placeholders into this platform's unit directory and load every service this bot owns, MCPs before the gateway. Refuses first if the host cannot enforce the boundary the declaration describes. Idempotent: an already-loaded service is replaced.
ArgumentTypeDescription
stageOnly?booleanWrite 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?booleanInstall 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.
fn uninstall()
Unload this bot's services and remove their unit files from the platform's unit directory. Never touches the workdir — the Funes memory and session history outlive the deployment.
fn status()
Report what is actually running: supervisor state and pid per service, whether each declared MCP port answers, and whether the gateway holds a live session to it.
fn restart(only?: array)
Bounce this bot's MCPs, wait for their ports, bounce the gateway, then confirm the sessions came back. Restarting an MCP without the gateway is never enough.
ArgumentTypeDescription
only?arrayWhich 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

rendered(infinite)— What this bot's runtime looks like: the files that make it up, their digests, and a flat summary of the sandbox boundary for reporting.
verification(90d)— What this bot's sandbox actually enforces, probed from inside it: which paths are readable, which hosts are reachable, and which claims could not be proven either way.
provisioned(infinite)— What this bot's workdir contains: the Hermes checkout and its ref, the interpreter and venv inside the workdir, the staged funes binary beside the host's, and which steps had to do work.
status(30d)— What is actually running for this bot: supervisor state per service, whether each MCP port answers, and whether the gateway holds a live session to it.
snapshot(infinite)— An archive of everything about this bot that a new machine cannot rebuild, and a record of what was deliberately left behind because provision will rebuild it.
restored(infinite)— What a snapshot put back on this machine: which files landed, how many absolute paths were rewritten from the old host, and which mentions of it survived.
03Reports1
@skunk-ape/hermes/fleetworkflow
fleet.ts

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.

hermesfleetsecurity
04Stats
A
100 / 100
Downloads
0
Archive size
137.8 KB
  • 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

06Platforms
07Labels