Skip to main content
Hermes team provisioning gives each member their own gateway process, running as their own operating system user, on the same host. Members share the machine and the model credentials, and nothing else.
Provisioning runs on a systemd Linux host only. The commands refuse to run anywhere else, because each member’s gateway is a systemd service. Members themselves can connect from macOS, Windows, iOS or Android.

How the isolation works

Every member gets four things of their own: The boundary is an operating system user rather than a token check inside one shared process, so the kernel enforces it. One member’s gateway cannot read another’s memories, sessions, plans or credentials, and a bug in the plugin cannot leak them across profiles. Each unit runs with systemd hardening applied: NoNewPrivileges, PrivateTmp, RestrictSUIDSGID, LockPersonality, RemoveIPC, and the ProtectKernel* and ProtectControlGroups family. Writable paths are bound to the member’s home directory and nothing else.

Before you start

You need root on the host, a shared Hermes virtual environment, and the plugin installed into it:
The provisioning scripts ship inside the wheel, so there is nothing to copy onto the server separately and nothing to keep in sync by hand. If the venv lives elsewhere, point HERMES_VENV_PYTHON at its python3. Configure the gateway’s own Hermes profile first, at /root/.hermes by default. New members inherit their model provider and credentials from it, so provisioning against an unconfigured profile produces members whose assistants cannot answer. Install Tailscale on the host if members will connect from their own laptops or phones. Without it, provisioning still succeeds, but each member is reachable only from the host itself.

Provisioning a member

That single command runs the whole sequence:
  1. Validates the label. Labels must match ^[a-z0-9][a-z0-9-]{0,30}$ and must not collide with a reserved system account name such as root, daemon, docker or hermes.
  2. Creates the OS user, with a locked password, a nologin shell and a home directory at mode 0700.
  3. Builds the profile skeleton under /home/alice/.hermes, including memories, sessions, skills, plans, workspace and plugins, each owned by the member at mode 0700.
  4. Writes the member’s .env with a freshly generated 48 character relay token, their derived listen port and their home channel.
  5. Seeds the model provider from the gateway’s own profile, as described under what members inherit.
  6. Installs the plugin shim into the member’s own plugins directory.
  7. Renders and starts the systemd unit, then verifies it is genuinely serving.
  8. Publishes the port to your tailnet with tailscale serve, and confirms it appears in tailscale serve status.
  9. Prints the member’s pairing code and QR, generated by running the CLI as that member against their own profile.
If any step fails after the OS user is created, the command rolls back. It disables and removes the unit, deletes the user and their home directory, and tells you the member was not provisioned, so a failed run does not leave a half-created member behind.

Why the verification step matters

A systemd unit that is crash-looping reports active in the moment between restarts. Checking systemctl is-active on its own would report success for a gateway that is not actually serving anyone. Provisioning therefore waits for three conditions together: the unit is active, its restart count is still zero, and the derived port is genuinely accepting connections. It polls for up to 20 seconds, and if the conditions are not met it fails with the exact journalctl command to run.

What members inherit, and what they do not

Members inherit only what they need to reach a model. Four configuration keys are copied from the gateway’s profile:
  • model
  • fallback_providers
  • fallback_model
  • model_catalog
Credentials are copied selectively rather than wholesale. The seeding step reads the gateway’s credential pool, works out which providers this member’s model configuration actually uses, and copies only those entries. Any provider the member will never call is dropped before their auth.json is written, and only the environment variables the surviving credentials reference are copied into their .env. Nothing personal crosses the boundary. Memories, sessions, chat integrations, skills and plans all stay with the profile that owns them.
If the gateway’s profile has no stored credentials, or its credentials do not cover the member’s configured provider, provisioning still completes but prints an explicit warning. The member’s gateway will start and accept a pairing, then fail to answer. Read the output rather than treating a zero exit code as proof the member is ready.

How members reach their gateway

Each member’s port is derived arithmetically from their UID:
The defaults are 8765 and 100. Deriving the port rather than allocating one means it stays the same across reprovisioning and never needs to be recorded anywhere. If the derived port is already held by a process the tool does not manage, provisioning stops instead of quietly taking it. The relay URL is resolved in priority order:
  1. --url if you passed one, used verbatim.
  2. --relay-host HOST, which becomes https://HOST:PORT.
  3. The host’s own tailnet DNS name, detected automatically.
  4. http://127.0.0.1:PORT as a last resort, with a warning that the member cannot connect from their own devices.
Only a non-loopback host is recorded in STAGEWHISPER_ALLOW_INGRESS_HOSTS, so a loopback trial never widens what the listener accepts.

Approving the first connection

The first time a member connects, their assistant refuses to answer and prints an approval code instead. Approve it as that member, not as root:
You need the runuser wrapper here. Each member has their own Hermes profile, so a bare hermes pairing approve run as root looks in root’s profile and reports the code as not found. The provisioning output prints this command with the member’s real paths already filled in.

Revoking a member

Revocation stops and disables the unit, then confirms it actually stopped, polling for up to 10 seconds. If the process is still running at that point, the command fails loudly and tells you revocation did not succeed, because a revocation that silently leaves a gateway reachable is worse than none at all. It then withdraws the port from your tailnet and deletes the member’s .env, which removes their relay token. Deleting the token is what makes revocation survive a restart: a unit re-enabled later has no credential to serve with. Revocation leaves the OS user, the home directory and all Hermes state in place. Cutting off access and destroying someone’s data are separate decisions, and only the first one can be undone. Remove the user by hand if that is what you want.

Re-running provisioning

provision is safe to re-run for an existing member. An existing relay token is read and preserved rather than regenerated, so a member who has already paired does not have to pair again. Configuration, the plugin shim and the unit are all refreshed. Two guards apply. Provisioning refuses to adopt an OS user that already exists but has no matching unit file, so it cannot take over an unrelated system account. It also refuses to continue if a member’s .env exists but contains no relay token, since it cannot tell a partially written file from one you edited on purpose.

Reference

Commands

Provisioning options

Per-member environment

Troubleshooting

The member’s assistant accepts the pairing but never answers. This is almost always missing provider credentials. Re-read the provisioning output for a warning about the credential pool, fix the gateway’s own profile, then re-run provision for that member. The approval code is reported as not found. You ran hermes pairing approve as root instead of through runuser with the member’s HERMES_HOME. The member cannot connect from their laptop. The relay URL fell back to loopback, which happens when the host is not on a tailnet or you passed a loopback --url. Install Tailscale and re-run, or pass --relay-host. tailscale serve reports that HTTPS is disabled. Enable HTTPS certificates for your tailnet in the Tailscale admin console under DNS, then re-run. The unit will not stay up. Provisioning prints the exact command to use: