🐒 Ming's Note

← Back to 2026-07-17-hermes-setup-guide

TOOLS_SETUP.md

Tools Setup Guide

How the memory system and Main Dashboard are configured. Reference for replicating or troubleshooting these setups.


Memory System

Three layers of memory, each with a specific purpose.

Layer 1: SOUL.md (Identity)

File: ~/.hermes/SOUL.md Loaded: Every message (no restart needed) Purpose: Agent identity, language, style, rules

# Identity
You are [Name] — [description].

# Language
ALWAYS use [language].

# Style
- Be direct without being cold
- Prefer substance over filler

# Rules
- Never hallucinate. Always web search to verify.
- Ask approval before modifying system settings.

When to edit: User mentions response style, language preference, or behavioral rules.


Layer 2: MEMORY.md (Working Notes)

File: ~/.hermes/MEMORY.md Loaded: Injected into every session at start Purpose: Environment facts, tool quirks, lessons learned

[Company] environment.
[OS, server info].
[Key tool paths and quirks].
[Project conventions].

What goes here: - User preferences and corrections - Environment discoveries (OS, paths, tool behavior) - Stable conventions that reduce future steering

What does NOT go here: - Session progress, temporary TODOs - System-injected info (model name, provider) - Anything stale in a week


Layer 3: USER.md (User Profile)

File: ~/.hermes/USER.md Loaded: Injected into every session Purpose: User's name, timezone, personal preferences

[Name]. [Timezone]. [Language preference].
[Role, interests].
[Personal notes relevant to agent interaction].

Layer 4: Warm Memory (Structured)

File: ~/.hermes/memories/warm.db (SQLite FTS5) Script: scripts/warm-memory.py Hook: scripts/warm-memory-hook.py (pre_llm_call)

Structured memory cards with type/label/content. Auto-injected into agent context before each LLM call.

Card types: - state — Current facts (e.g., "user prefers concise responses") - event — Something that happened - knowledge — Reusable insight - preference — User preference

CLI:

python3 scripts/warm-memory.py add henry state "prefers concise" "User prefers concise responses"
python3 scripts/warm-memory.py list henry --active
python3 scripts/warm-memory.py search henry "language"
python3 scripts/warm-memory.py block henry   # Get cards for injection

Hook (auto-inject):

# config.yaml
hooks:
  pre_llm_call:
    - command: /opt/data/scripts/warm-memory-hook.py
      timeout: 5

The hook reads session info from stdin, determines the user, runs warm-memory.py block <user>, and outputs JSON context injected into the LLM call.


Main Dashboard (Dashboard)

A Flask app that serves markdown docs, project context, and sandbox output.

Script: scripts/md-reader.py Port: 9119 Venv: /opt/data/venv/dashboard/

Routes

Route Serves
/ Homepage — dashboard, active plans, categories
/doc/<path> Markdown files from wiki or projects
/sandbox/ List all sandbox sessions
/sandbox/<name> Files in a session
/sandbox/<name>/file/<path> Individual file (renders .md as HTML)
/wiki/ Wiki index
/projects/<name> Project context files

Static Files

Serves from /opt/data/ — any file in that tree is accessible at its relative path.

Proxy Routes

For apps that run on separate ports:

# In md-reader.py
@app.route('/myapp/')
def myapp_proxy():
    return proxy_request('http://localhost:5000')

@app.route('/myapp/api/<path:path>')
def myapp_api_proxy(path):
    return proxy_request(f'http://localhost:5000/api/{path}')

Auto-Start (Gateway Hook)

Main Dashboard auto-starts when the gateway boots.

Hook: hooks/dashboard-startup/

hooks/dashboard-startup/
├── HOOK.yaml
└── handler.py

HOOK.yaml:

name: dashboard-startup
description: "Start md-reader dashboard on gateway boot."
events:
  - gateway:startup

handler.py:

import asyncio
import subprocess
import urllib.request

PORT = 9119
CHECK_URL = f"http://localhost:{PORT}/"
PYTHON = "/opt/data/venv/dashboard/bin/python"
SCRIPT = "/opt/data/scripts/md-reader.py"

async def handle(event_type: str, context: dict) -> None:
    # Check if already running
    try:
        resp = urllib.request.urlopen(CHECK_URL, timeout=3)
        if resp.status == 200:
            print("[dashboard] Already running")
            return
    except Exception:
        pass

    # Start the server
    subprocess.Popen(
        [PYTHON, SCRIPT],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        start_new_session=True,
    )
    await asyncio.sleep(3)

    try:
        resp = urllib.request.urlopen(CHECK_URL, timeout=5)
        print(f"[dashboard] Started: {resp.status}")
    except Exception as e:
        print(f"[dashboard] FAILED: {e}")

Venv Setup

# Create persistent venv (survives container restart)
python3 -m venv /opt/data/venv/dashboard
/opt/data/venv/dashboard/bin/pip install flask markdown markupsafe requests

Important: Use /opt/data/venv/ not ~/.hermes/ — the latter is ephemeral on Docker/Zeabur.

Restart

pkill -f "md-reader.py"
/opt/data/venv/dashboard/bin/python /opt/data/scripts/md-reader.py &

Or via gateway hook: kill -TERM 1 (triggers Zeabur container restart).

Check

curl -s -o /dev/null -w "%{http_code}" http://localhost:9119/
# Should return 200

Config Reference

Relevant config.yaml sections:

hooks:
  pre_llm_call:
    - command: /opt/data/scripts/warm-memory-hook.py
      timeout: 5
  pre_tool_call:
    - command: /opt/data/scripts/block-web-extract.sh
      matcher: web_extract

memory:
  memory_enabled: true
  user_profile_enabled: true

These setups are specific to this VPS environment. Adapt paths and ports for your instance.