Hermes stack install: agent runbook
Audience: an autonomous agent executing an install on a customer's own hardware. Companion doc: the human walkthrough, which covers the same install without commands.
Reference implementation: the build described in the case study, summarised at the end. Every command below was run on that build and is reproduced as it ran.
1. What this stack is
Four services on hardware the customer owns. Each gets its own container.
| # | Service | Role in plain terms | Port |
|---|---|---|---|
| 1 | Hermes agent | the worker: thinks, writes, uses tools | dashboard 9119, agent API 8642 |
| 2 | Paperclip | the board: every job lands here as an issue or comment | 3100 |
| 3 | Twenty | the CRM: contacts, companies, deals | 3000 |
| 4 | Semantica | the memory graph: durable facts written and searched later | 8000 |
The agent is useless without the other three. The board is how the customer sees the work. The graph is how the agent remembers between runs. The CRM is what it works from.
2. Contracts
These hold for every install. Violating one invalidates the run.
- Every stage ends with a verification block. A stage whose verification fails is not finished, whatever the installer printed. Read the service state on the box.
- An installer's exit code is not evidence. Read the version, the port, the row.
- Credentials go onto the boxes, owner-readable. Never into chat, notes, issue comments or email, except the ones the customer owns and must receive.
- Nothing on the customer's cluster gets deleted. Archive or rename. History stays.
- The agent holds scoped keys only. Never an admin key, on any service.
- Ask before anything destructive, and before anything that exposes a service to the internet. Both are one-way doors in a customer's eyes.
3. Preconditions
- A Debian 13 target (LXC container on Proxmox, or any Debian 13 VM or host).
- Root on the Proxmox host if you are creating containers.
- Outbound HTTPS from every container.
- The customer's LAN, and a decision on whether the services are reachable from it.
4. Stage 0: recon, before touching anything
pveversion # Proxmox hosts only
pct list # what already exists, and its state
free -h; df -h # does the host have headroom for four more containers
Inside each target container:
ip route | head -2
ping -c2 -W2 <gateway> # default route works
getent hosts deb.debian.org # DNS resolves
curl -fsS -m 10 -o /dev/null -w '%{http_code}\n' https://hermes-agent.nousresearch.com/
Do not proceed while any of these fails. Each one comes back later as a fault that looks like something else.
5. Stage 1: create the containers
One service per container. Sizing that worked, and why it is not uniform:
| Container | Memory | Why |
|---|---|---|
| Agent | 4 GB | runs on an API model, so no GPU or model weights in the box |
| Board | 8 GB | Node app plus PostgreSQL |
| CRM | 10 GB | the build is the memory-hungry part, see the NODE_OPTIONS note in stage 5 |
| Memory graph | 4 GB | Python service, SQLite or Postgres behind it |
Verify: pct list on the host shows four containers running with the memory above, and each one answers ip route with the expected gateway.
Addresses from DHCP will move. Pin them (static lease or static config) before handover, otherwise the customer's links rot overnight.
6. Stage 2: the worker (Hermes agent)
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates git sudo
useradd -m -s /bin/bash hermes
su - hermes -c "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --non-interactive"
Pitfall, and it will happen. The install can stop partway on a missing libatomic1, because the Node runtime the installer fetches needs it and a minimal Debian image does not ship it. The fix is one package and a re-run. The installer resumes where it stopped:
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq libatomic1
su - hermes -c "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --non-interactive"
Verify:
ls -la /home/hermes/.local/bin/hermes
su - hermes -c "/home/hermes/.local/bin/hermes --version"
The agent's home is /home/hermes/.hermes, not /root. An install driven by paths that assume /root reports success and then does nothing.
6.1 Provider and dashboard
- Put the model provider key in
~/.hermes/.envas thehermesuser. Secrets live in
.env; settings live in config.yaml. Never put a key in config.yaml.
- Dashboard auth: set
dashboard.basic_auth.username, then set
dashboard.basic_auth.password_hash via hermes config set. If both a plaintext password and a password_hash exist, the hash wins, so set the hash explicitly to the new password's hash or your change silently does nothing.
- The login endpoint is
POST /auth/password-loginwith JSON
{"provider":"basic","username":...,"password":...,"next":"/"}. Omitting provider returns 422, not 401. Do not chase a 422 as a credential fault.
6.2 Identity, then skills
Write the agent's identity file first: its name, whose it is, what it is allowed to touch, what it must never touch. The customer reads this file.
Then seed skills, copying the chosen skill directories into $HERMES_HOME/skills/. Verify on the box by listing them, and count them. On this build the first seed push silently missed one skill because it went to the wrong parent directory, and only a listing on the box caught it. A copier's success message is not a list of what arrived.
6.3 The gateway service
Install and enable the Hermes gateway service so the agent runs without a logged-in terminal. Confirm systemctl is-enabled as well as is-active: an active service that is not enabled dies at the next reboot, and that failure appears weeks later as "it stopped working".
7. Stage 3: the memory graph (Semantica)
apt-get update -qq
apt-get install -y -qq python3 python3-venv python3-dev python3-pip build-essential curl git libmagic1
python3 -m venv /opt/semantica/venv # PEP 668: never install into the system python
V=/opt/semantica/venv/bin
$V/pip install -q --upgrade pip setuptools wheel
$V/pip install -q 'semantica[parse-pdf,llm-deepseek]'
Probe for the server's real flags instead of assuming them, so the launcher matches the version you actually installed:
$V/python -m semantica.server --help
If that reports a missing module, add fastapi 'uvicorn[standard]' and re-probe. Then write a launcher and a systemd unit on port 8000, and verify:
systemctl is-active semantica; systemctl is-enabled semantica
for p in / /docs /openapi.json /api/graph; do
echo "$p -> $(curl -s -o /dev/null -w '%{http_code}' -m 8 http://127.0.0.1:8000$p)"
done
A version string proves a download. Prove the library works by importing it:
$V/python -c "
from semantica.context import ContextGraph
from semantica.provenance import ProvenanceManager
print('imports ok')"
Wire the agent to the graph, then prove the round trip: read GET /api/graph/stats, have the agent write facts, read it again, and confirm the node count rose. The claim that the agent uses the graph holds only once the count moves.
8. Stage 4: the board (Paperclip)
apt-get update -qq
apt-get install -y -qq build-essential git ripgrep curl ca-certificates openssl python3
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y -qq nodejs
npm install -g pnpm
PostgreSQL 17 from the PGDG repo:
install -d /usr/share/postgresql-common/pgdg
curl -fsSL -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
https://www.postgresql.org/media/keys/ACCC4CF8.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(. /etc/os-release; echo $VERSION_CODENAME)-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list
apt-get update -qq
apt-get install -y -qq postgresql-17
systemctl enable --now postgresql
Rust, which the build needs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
export PATH="/root/.cargo/bin:$PATH"
Source and build, from the latest release tag:
TAG=$(curl -fsSL https://api.github.com/repos/paperclipai/paperclip/releases/latest \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("tag_name",""))')
curl -fsSL --max-time 300 -o /tmp/pc.tar.gz \
"https://github.com/paperclipai/paperclip/archive/refs/tags/${TAG}.tar.gz"
mkdir -p /opt/paperclip-ai
tar -xzf /tmp/pc.tar.gz -C /opt/paperclip-ai --strip-components=1
cd /opt/paperclip-ai && pnpm install && pnpm build
Then run it as a service, create the human admin, and mint the agent's key.
8.1 Two wiring faults that cost the reference build most of a day
Plain-HTTP gateway. The board refuses to call an agent gateway over plain HTTP unless you say so explicitly. Set the agent's adapter config with dangerouslyAllowInsecureRemoteHttp: true. Without it the runs fail with an opaque connection error and the agent looks broken when it is only unreachable.
The run-id header. The board rejects a comment from an agent unless the request carries the heartbeat run id. The agent's posting tool must send X-Paperclip-Run-Id on every comment. When an agent's comment does not land, check this header before you check the key.
Mint a scoped key per agent (POST /api/agents/{id}/keys), keep the board-side copy and the agent-side copy separate, and prove the loop by reading the comment back and confirming it is authored by the agent's own id.
9. Stage 5: the CRM (Twenty)
apt-get install -y -qq build-essential redis-server curl ca-certificates openssl git python3
# PostgreSQL 17 + pgvector, exactly as in stage 4 plus the vector extension
apt-get install -y -qq postgresql-17 postgresql-17-pgvector
su - postgres -c "psql -q -d twenty_db -c 'CREATE EXTENSION IF NOT EXISTS vector;'"
Node 24 plus yarn 4 through corepack, then the source and the build:
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y -qq nodejs
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
corepack enable; corepack prepare [email protected] --activate
TAG=$(curl -fsSL https://api.github.com/repos/twentyhq/twenty/releases/latest \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("tag_name",""))')
curl -fsSL --max-time 900 -o /tmp/twenty.tar.gz \
"https://github.com/twentyhq/twenty/archive/refs/tags/${TAG}.tar.gz"
mkdir -p /opt/twenty && tar -xzf /tmp/twenty.tar.gz -C /opt/twenty --strip-components=1
cd /opt/twenty
export NODE_OPTIONS="--max-old-space-size=4096" # without this the frontend build dies
yarn install --immutable
npx nx run twenty-server:build
npx nx build twenty-front
cp -r packages/twenty-front/build packages/twenty-server/dist/front
Write /opt/twenty/.env mode 600 with PG_DATABASE_URL, REDIS_URL, SERVER_URL, APP_SECRET and STORAGE_TYPE=local, then run migrations (yarn database:init:prod) and install the two units, twenty-server and twenty-worker. The worker unit needs DISABLE_DB_MIGRATIONS=true and DISABLE_CRON_JOBS_REGISTRATION=true so it does not race the server.
Verify all four of redis-server, postgresql, twenty-server, twenty-worker are active and enabled, and that :3000, :5432, :6379 are listening.
10. Stage 6: routines, backups, health
Give the agent its schedule in the customer's own timezone, with real cron triggers visible in the board UI:
| Routine | Cron | Purpose |
|---|---|---|
| Daily self-check | 0 7 * |
is everything still up |
| Morning brief | 45 7 1-5 |
what needs attention today |
| End of day | 0 18 1-5 |
file the day's learnings into the graph |
| Nightly backup | 40 23 * |
dated archive, fourteen days kept, oldest rolled off |
| Weekly review | 0 8 1 |
what changed, what is stuck |
Add container-local jobs for the cheap work, so it costs no tokens: a health check every two minutes that self-repairs and reports only when something changes, a disk guard, and the nightly backup script.
Prove autonomy, and show the row that proves it. A working schedule leaves a row. Read routine_runs for source=schedule, status=completed, and confirm the run produced a real artifact: a comment on the board, and a node count that moved in the graph. One unattended run that completed and left evidence is the whole claim.
11. Stage 7: acceptance
Produce this table, filled in from the boxes:
| Check | Evidence |
|---|---|
| Agent binary runs | hermes --version output |
| Agent home correct | $HERMES_HOME resolves under /home/hermes |
| Dashboard responds | HTTP status on 9119 |
| Dashboard login works | correct password 200, wrong password 401 |
| Gateway survives reboot | systemctl is-enabled hermes-gateway |
| Skills present | a count from listing the skills directory on the box |
| Graph alive | HTTP 200 on /api/graph and a rising /api/graph/stats |
| Board alive | HTTP 200 on 3100; the agent's own comment readable |
| Agent posts as itself | comment authored by its agent id |
| CRM alive | /healthz returns ok; a human can sign in |
| Routine fired unattended | a routine_runs row, plus the artifact it produced |
| Backups exist | fourteen days of dated archives on disk |
| Addresses pinned | no DHCP lease left in the handover sheet |
| No admin path left open | the builder's remote access removed, or the customer told it exists |
12. Pitfalls, all of which happened
libatomic1missing breaks the Hermes install on a minimal Debian image. Install it and re-run the installer, which resumes.pct pushreads from the Proxmox host's filesystem, not yours. Copy files to the host first or it fails to open them.pct execmangles nested quoting. Write a script to a file, copy it in, then run it. Do not inline nested quotes.- The CRM frontend build needs
NODE_OPTIONS=--max-old-space-size=4096or it dies out of memory. - Agent comments need
X-Paperclip-Run-Id. Without the header the board silently refuses them. - The board refuses a plain-HTTP gateway unless
dangerouslyAllowInsecureRemoteHttpis set. - Cron installed as root never fires for the service user. Verify with
su - <user> -c "crontab -l", notcrontab -l. - Mail to an unrouted address is discarded silently when a catch-all is set to DROP. Check that a customer-facing address actually routes before you publish it or send from it. No bounce will warn you.
- A seed copier reports success for files that never arrived. Count the skills on the box.
- DHCP addresses move. Pin them before handover.
- An active service that is not enabled passes every check today and fails after the first reboot.
- Do not delete anything on the customer's cluster without written confirmation, including stale records that look obviously wrong. Say what you would delete and wait.
13. Case study
The reference build for this runbook is written up separately, with the customer's identity removed: Case study: one agent install, start to finish. Read it alongside these stages.
It covers what the build proved as opposed to what it installed, what was still open at handover, and how the builder's access was removed with the proof.
14. Handover
Deliver: a credential sheet listing what the customer owns and how to rotate it; the container map with pinned addresses; the routine schedule; the open items, stated plainly; and confirmation that the builder's remote access is gone. Then tell them it is done.