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:

RequirementMinimumRecommended
Node.js20.x LTS22.x LTS
npm / pnpmnpm 10+pnpm 9+
Claude API keyHaiku tierSonnet tier
RAM2 GB free4 GB free
OSmacOS 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 correctly
  • Tool calls detected — confirms skills are being invoked
  • Early exit signal detected — confirms the model can short-circuit
  • Loops 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| H

Creating 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

ConcernGuidance
description clarityWrite it for the model, not for humans. Be explicit about when to call it.
Parameter typesUse JSON Schema strictly — the model relies on it for structured calls.
Error handlingThrow named errors; Hermes catches and surfaces them as tool failure messages.
IdempotencySkills may be called multiple times across loops; design accordingly.
Side effectsLog 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-memory for parallel testing
  • Switch to redis for 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.

Export for reading

Comments