I’ve been running Hermes Agent in production for several months now, and the most common question I get is: “How do I actually set this up?” The official docs cover the concepts well, but there’s a gap between understanding the architecture and having a working instance you trust. This guide fills that gap.
By the end, you’ll have Hermes running locally, its 8-loop orchestration verified in logs, and a custom skill wired in. I’ll also cover the five errors that trip up almost every new install.
Prerequisites
Before cloning anything, make sure your environment satisfies these requirements:
| Requirement | Minimum | Recommended |
|---|---|---|
| Node.js | 20.x LTS | 22.x LTS |
| npm / pnpm | npm 10+ | pnpm 9+ |
| Claude API key | Haiku tier | Sonnet tier |
| RAM | 2 GB free | 4 GB free |
| OS | macOS 13, Ubuntu 22.04, Windows 11 (WSL2) | Ubuntu 24.04 |
Why Node 20 minimum? Hermes uses native fetch, structuredClone, and AsyncLocalStorage without polyfills. Node 18 works for basic sessions but breaks under concurrent loop execution.
Check your versions before proceeding:
node --version # expect v20.x or v22.x
npm --version # expect 10.x+
Get your Claude API key from console.anthropic.com. Haiku is fine for development; I recommend Sonnet for any session where you’re testing reasoning-heavy skills.
Installation
1. Clone the repository
git clone https://github.com/luonghongthuan/hermes-agent.git
cd hermes-agent
2. Install dependencies
pnpm install
# or: npm install
The install pulls around 40 packages. The heaviest is the vector store adapter — if you don’t need semantic memory, you can skip it later in the config.
3. Copy the environment template
cp .env.example .env
Open .env and fill in your values:
# .env — never commit this file
# Required
ANTHROPIC_API_KEY=sk-ant-YOUR_KEY_HERE
HERMES_SESSION_NAME=my-first-session
# Model selection (default: claude-haiku-4-5)
HERMES_MODEL=claude-sonnet-4-5
# Loop configuration
HERMES_MAX_LOOPS=8
HERMES_LOOP_TIMEOUT_MS=30000
# Memory backend: "in-memory" | "sqlite" | "redis"
HERMES_MEMORY_BACKEND=sqlite
HERMES_SQLITE_PATH=./data/hermes.db
# Logging
HERMES_LOG_LEVEL=info # debug | info | warn | error
HERMES_LOG_PRETTY=true
# Optional: disable telemetry
HERMES_TELEMETRY=false
Configuration File Deep Dive
Hermes uses hermes.config.ts (or .js) in the project root for structural configuration. This is separate from .env — the config file defines what runs; the env file defines secrets and tuning.
Here’s a production-ready starting config with every field annotated:
// hermes.config.ts
import { defineConfig } from "./src/config";
export default defineConfig({
// ─── Identity ────────────────────────────────────────────────────────────
name: "hermes", // used in log prefixes and session IDs
version: "1.0.0", // surfaced in health checks
// ─── Loop Engine ─────────────────────────────────────────────────────────
loops: {
maxIterations: 8, // hard cap — see "Verifying 8 loops" below
timeoutMs: 30_000, // per-loop wall-clock timeout
backoffMs: 500, // delay between loops (exponential by default)
earlyExit: true, // stop when model signals task complete
},
// ─── Model ───────────────────────────────────────────────────────────────
model: {
provider: "anthropic",
id: "claude-sonnet-4-5",
temperature: 0.3, // lower = more deterministic tool use
maxTokens: 4096,
systemPromptFile: "./prompts/system.md", // optional override
},
// ─── Memory ──────────────────────────────────────────────────────────────
memory: {
backend: "sqlite", // persists across sessions
path: "./data/hermes.db",
maxContextTokens: 8000, // trim older turns when context grows
embeddings: false, // set true to enable semantic recall
},
// ─── Skills ──────────────────────────────────────────────────────────────
skills: [
"./skills/built-in/web-search.ts",
"./skills/built-in/file-read.ts",
"./skills/built-in/file-write.ts",
"./skills/custom/my-first-skill.ts", // we'll create this below
],
// ─── Hooks ───────────────────────────────────────────────────────────────
hooks: {
onLoopStart: "./hooks/on-loop-start.ts", // optional
onLoopEnd: "./hooks/on-loop-end.ts", // optional
onTaskDone: "./hooks/on-task-done.ts", // optional
},
// ─── Output ──────────────────────────────────────────────────────────────
output: {
format: "markdown", // "markdown" | "json" | "plain"
saveToFile: true,
outputDir: "./outputs",
},
});
A few fields deserve extra attention:
loops.earlyExit — when true, Hermes stops as soon as the model appends a <done/> sentinel to its response. This is almost always what you want in production; the maxIterations cap is the safety net, not the happy path.
model.temperature — I run 0.3 for tool-heavy sessions. Tool invocation is structured output; you want consistency, not creativity. Raise to 0.7 for sessions that are more writing-focused.
memory.maxContextTokens — Hermes trims oldest turns when the context window fills. Setting this too high burns tokens; too low and the model loses thread across loops. 8000 works well for Sonnet’s 200k window when you have 8 loops of ~500 tokens each.
Running Your First Session
With config in place, start an interactive session:
pnpm dev
# or: npm run dev
You should see:
[hermes] Starting session: my-first-session
[hermes] Model: claude-sonnet-4-5
[hermes] Memory backend: sqlite (./data/hermes.db)
[hermes] Skills loaded: 4
[hermes] Loop engine: max=8, timeout=30s
[hermes] Ready. Type your task and press Enter.
>
Try a simple task to confirm everything is wired:
> Summarize the key differences between REST and GraphQL in 3 bullet points.
A working instance will run 1–2 loops for this (no tools needed), produce output, and exit cleanly.
Verifying the 8 Loops Are Working
The most important thing to verify in a new install is that the loop engine is running correctly — both that it can use all 8 loops and that it stops early when the task is done.
What to look for in logs
Set HERMES_LOG_LEVEL=debug and run a multi-step task:
> Research the top 3 AI coding assistants in 2026, compare their pricing, and write a markdown table.
In debug mode, you’ll see loop boundaries explicitly:
[loop:1] Entering loop 1/8
[loop:1] Model response received (tokens: 312)
[loop:1] Tool calls detected: ["web_search"]
[loop:1] Executing: web_search("top AI coding assistants 2026")
[loop:1] Tool result: 847 chars
[loop:2] Entering loop 2/8
[loop:2] Model response received (tokens: 445)
[loop:2] Tool calls detected: ["web_search"]
[loop:2] Executing: web_search("AI coding assistant pricing 2026 comparison")
...
[loop:4] Model response received (tokens: 892)
[loop:4] Early exit signal detected (<done/>)
[loop:4] Session complete. Loops used: 4/8
The key lines to verify:
Entering loop N/8— confirms the engine is counting correctlyTool calls detected— confirms skills are being invokedEarly exit signal detected— confirms the model can short-circuitLoops used: N/8— confirms early exit fired before the cap
If you see Loops used: 8/8 on a simple task, the early exit sentinel is not working — check your system.md includes the <done/> instruction (it’s in the default template).
Loop state diagram
flowchart TD
A([Task Received]) --> B[Loop 1]
B --> C{Tool calls?}
C -->|Yes| D[Execute Tools]
D --> E[Append Results to Context]
E --> F{Loop < 8?}
F -->|Yes| G[Loop N+1]
G --> H{Early exit signal?}
H -->|Yes| Z([Done])
H -->|No| C
F -->|No — cap reached| Z
C -->|No| HCreating Your First Custom Skill
Skills in Hermes are TypeScript modules that export a SkillDefinition. Here’s the minimal shape:
// skills/custom/my-first-skill.ts
import { defineSkill } from "../../src/skills";
export default defineSkill({
name: "get_current_date",
description: "Returns the current date and time in ISO 8601 format. Use when the user asks about today's date or current time.",
parameters: {
type: "object",
properties: {
timezone: {
type: "string",
description: "IANA timezone name, e.g. 'Asia/Ho_Chi_Minh'. Defaults to UTC.",
},
},
required: [],
},
async execute({ timezone = "UTC" }) {
const now = new Date();
const formatted = now.toLocaleString("en-US", {
timeZone: timezone,
dateStyle: "full",
timeStyle: "long",
});
return { iso: now.toISOString(), formatted, timezone };
},
});
Register it in hermes.config.ts under skills:
skills: [
// ...existing skills
"./skills/custom/my-first-skill.ts",
],
Restart the dev server. In the next session, the model can now call get_current_date as a tool. Test it:
> What time is it right now in Ho Chi Minh City?
In debug logs, you’ll see:
[loop:1] Tool calls detected: ["get_current_date"]
[loop:1] Executing: get_current_date({ timezone: "Asia/Ho_Chi_Minh" })
[loop:1] Tool result: {"iso":"2026-06-11T07:30:00.000Z","formatted":"...","timezone":"Asia/Ho_Chi_Minh"}
Skill design checklist
| Concern | Guidance |
|---|---|
description clarity | Write it for the model, not for humans. Be explicit about when to call it. |
| Parameter types | Use JSON Schema strictly — the model relies on it for structured calls. |
| Error handling | Throw named errors; Hermes catches and surfaces them as tool failure messages. |
| Idempotency | Skills may be called multiple times across loops; design accordingly. |
| Side effects | Log side-effectful skills (writes, API calls) via the Hermes logger, not console.log. |
Troubleshooting: 5 Most Common Setup Errors
1. Error: ANTHROPIC_API_KEY is not set
Symptom: Process exits immediately after start with an env validation error.
Fix: Confirm .env exists and contains your key. A common gotcha: if you copied .env.example to .env.local instead of .env, Hermes won’t pick it up by default (it loads .env only). Check the dotenv call in src/config/env.ts to see which files are searched in your version.
# Quick check
grep ANTHROPIC_API_KEY .env
2. LoopTimeoutError: Loop 3 exceeded 30000ms
Symptom: Sessions with web search or file operations time out mid-run.
Fix: Increase HERMES_LOOP_TIMEOUT_MS for tool-heavy sessions, or set it per-skill:
// in your skill definition
timeout: 60_000, // override global timeout for this skill only
The default 30s is aggressive if your network is slow or the tool is doing real I/O. I use 60s in development and 45s in production.
3. Skills not loading — SkillLoadError: Cannot find module './skills/custom/my-first-skill.ts'
Symptom: Config lists the skill but session startup fails.
Fix: Paths in skills[] are resolved relative to the config file location, not process.cwd(). If your config is at the root and your skill is at skills/custom/..., the path must start with ./skills/. Also double-check the filename matches exactly — Linux filesystems are case-sensitive.
4. MemoryError: SQLite database is locked
Symptom: Starting a second session while one is already running causes a lock error on the SQLite DB.
Fix: SQLite doesn’t support concurrent writers. Either:
- Use
HERMES_MEMORY_BACKEND=in-memoryfor parallel testing - Switch to
redisfor multi-session production setups - Use session-namespaced DB paths:
HERMES_SQLITE_PATH=./data/hermes-${SESSION_NAME}.db
5. ModelError: max_tokens must be at least 1024
Symptom: Session starts but fails on the first model call.
Fix: Some older Hermes configs set maxTokens: 512 which Anthropic’s API now rejects. Update hermes.config.ts:
model: {
maxTokens: 4096, // minimum recommended: 1024
}
Production Configuration Example
For a production deployment (containerized, multi-session, persistent memory):
// hermes.config.ts — production profile
export default defineConfig({
name: "hermes-prod",
loops: {
maxIterations: 8,
timeoutMs: 45_000,
backoffMs: 1_000,
earlyExit: true,
},
model: {
provider: "anthropic",
id: "claude-sonnet-4-5",
temperature: 0.2,
maxTokens: 8192,
},
memory: {
backend: "redis",
redisUrl: process.env.REDIS_URL, // set in container env
maxContextTokens: 12_000,
embeddings: true, // semantic memory for long projects
},
skills: [
"./skills/built-in/web-search.ts",
"./skills/built-in/file-read.ts",
"./skills/built-in/file-write.ts",
],
output: {
format: "json",
saveToFile: true,
outputDir: "/var/hermes/outputs",
},
});
And the corresponding docker-compose.yml snippet:
services:
hermes:
image: node:22-alpine
working_dir: /app
volumes:
- ./:/app
- hermes-outputs:/var/hermes/outputs
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- REDIS_URL=redis://redis:6379
- HERMES_LOG_LEVEL=info
- HERMES_TELEMETRY=false
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
hermes-outputs:
redis-data:
What’s Next
With Hermes running and verified, the natural next steps in this series are:
- Skill composition — chaining skills so the output of one becomes the input of the next within a single loop
- Memory strategies — when to use episodic vs semantic memory, and how to tune retrieval
- Multi-agent setup — running Hermes as a sub-agent orchestrated by a planner layer
The install is the foundation. Once you’ve seen the 8-loop engine running in your own logs and shipped your first custom skill, the architecture starts to feel intuitive rather than abstract.
If you hit an error not covered in the troubleshooting section above, check the GitHub issues — the community is active and most edge cases are documented there.