--- title: 'Hermes Agent: an autonomous agent on Telegram' url: https://blog.guigpap.com/en/hermes/ url_md: https://blog.guigpap.com/en/hermes.md category: hermes date: '2026-08-04' maturite: production techno: - docker - n8n - telegram - odoo application: - ai - knowledge - operations --- # Hermes Agent: an autonomous agent on Telegram > Hermes agent (Nous Research) in a container, Telegram as its only interface, 32 MCP tools served by N8N and Mnemosyne long-term memory ## 1. What? — Definition and context **Hermes Agent** is an autonomous conversational agent released by [Nous Research](https://hermes-agent.nousresearch.com/) under an MIT licence. Unlike the AI building blocks already running on the VPS — which are *gateways* invoked by workflows — Hermes is a **long-lived process that keeps the thread**: it retains memory across conversations, decides for itself which tools to call, and needs no workflow to work out what to do. On this infrastructure it runs in an `ai-stack` container with **a dedicated Telegram bot as its only interface**. No published port, no Caddy route, no dashboard. ### Components | Building block | Detail | |--------|--------| | **Container** | `nousresearch/hermes-agent:latest` — no published port, `n8n-internal` + `mcp-backend` networks | | **LLM** | ChatGPT/Codex subscription through the native `openai-codex` provider (`gpt-5.5`) | | **Business tools** | 32 MCP tools served by the N8N workflow `Hermes MCP Tools` | | **Debug tools** | 4 read-only N8N tools (`n8n-mcp`), reserved for workflow diagnostics | | **Memory** | External Mnemosyne provider — local SQLite, hybrid vector + FTS5 search | | **Plugins** | 3 in-house plugins hooked into the gateway lifecycle | > **Note - Agent, not gateway** > > `cli-ollama` translates an HTTP request into a CLI call and hands control back: nothing persists between two calls on the model side. Hermes does the opposite — it maintains state (sessions, memory, user profile) and decides its own path through the tools. Both coexist on the VPS and serve different purposes. ### Visual architecture ```mermaid flowchart TD TG(["Telegram · dedicated bot · strict allowlist"]) subgraph Cont["hermes container · no published port"] direction TB GW["Hermes gateway · gpt-5.5 via openai-codex"] MEM["Mnemosyne · SQLite + FTS5 · /opt/data"] PLG["Plugins · voice, message editor, usage alert"] GW --- MEM GW --- PLG end subgraph N8N["N8N"] direction TB Trig["MCP Server Trigger · /mcp/hermes-tools"] Adm["n8n-mcp · 4 diagnostic tools"] end subgraph Cibles["Business targets"] direction TB Odoo["Odoo · projects, tasks, contacts, CRM, timesheets"] Dock["Docker · status, logs, start/stop/restart"] Prom["Prometheus · alerts, PromQL"] Vault["Obsidian vault vps-vault · read + confined write"] end Plaud["Plaud · official MCP server · stdio"] TG <--> GW GW -->|"Bearer · normal flow · 32 tools"| Trig GW -.->|"Bearer · debug only"| Adm GW -->|"MCP stdio · outside N8N"| Plaud Trig --> Cibles ``` ### In this section This article covers the deployment: container, configuration, security model and operations. Three articles detail what plugs into it. | Article | Content | |---------|---------| | [The 32 MCP tools](/en/hermes/outils-mcp/) | Full catalogue, typed-input gateways, confined vault writes, pitfalls of the N8N MCP layer | | [Mnemosyne long-term memory](/en/hermes/memoire-mnemosyne/) | Wrapper mode, hybrid search, 20 memory tools, consolidation and day-to-day use | | [Custom skills](/en/hermes/skills/) | The skill system and the twelve procedures written for this deployment | | [Plugins and their hooks](/en/hermes/plugins/) | Four Python plugins hooked into the gateway lifecycle | --- ## 2. Why? — Stakes and motivations ### Six structuring decisions Taken in a scoping session before the first line of configuration, they explain almost the entire shape of the deployment. | Decision | Reason | |----------|--------| | LLM **direct** through `openai-codex`, bypassing `cli-ollama` | The subscription is consumed natively by the Hermes provider. Interposing the gateway would add a hop, a timeout and a loss of context for zero gain | | **Dual-channel** N8N access | One channel of curated business tools, one read-only diagnostic channel. The separation is explicit, not implicit | | **Telegram bot only** | No port, no Caddy route, no dashboard: the HTTP attack surface is nil | | Service in `ai-stack/docker-compose.yaml` | Cohabits with Qdrant and `cli-ollama`, same internal networks, same lifecycle | | **Reactive only** in v1 | The scheduler and automations stay disabled: an agent that acts unprompted is hard to audit until you trust its guardrails | | Data dir in the **daily backup** | All accumulated value (memory, sessions, skills) lives in a single directory — losing it means starting over | ### Why two MCP channels rather than one? The administrator MCP server (`n8n-mcp`) exposes **24 tools**, including `n8n_delete_workflow` and `n8n_manage_credentials`. Wiring them as-is would hand the agent the keys to the entire automation estate just so it could read an execution log. So the deployment separates them: - **`n8n-tools`** — the normal flow. Hand-written business tools with typed inputs, exposed by a dedicated N8N workflow. This is what the agent uses 99% of the time. - **`n8n-admin`** — filtered down to **4 read-only diagnostic tools** (`n8n_list_workflows`, `n8n_get_workflow`, `n8n_executions`, `n8n_validate_workflow`), and restricted by a system prompt rule to workflow debugging alone. ### Why external memory? Hermes' native memory is a `MEMORY.md` file re-injected into the prompt. Simple, but it plateaus: no search, and a character budget that forces a trade-off between keeping everything and staying readable. Mnemosyne replaces that mechanism with a local SQLite database featuring hybrid search (vector + FTS5) and background capture on every turn. Local-first: no conversation data leaves the VPS to be vectorised. --- ## 3. How? — Technical implementation ### The Docker service ```yaml hermes: image: nousresearch/hermes-agent:latest container_name: hermes command: gateway run networks: - n8n-internal # MCP Server Trigger (n8n:5678) - mcp-backend # n8n-mcp:3000 (debug only) environment: - PUID=${CLAUDE_USER_ID:-1000} - PGID=${CLAUDE_GROUP_ID:-1000} - TELEGRAM_BOT_TOKEN=${HERMES_TELEGRAM_BOT_TOKEN} - TELEGRAM_ALLOWED_USERS=${HERMES_ALLOWED_CHAT_ID} - HERMES_N8N_MCP_TOKEN=${HERMES_N8N_MCP_TOKEN} - N8N_MCP_AUTH_TOKEN=${N8N_MCP_AUTH_TOKEN} volumes: - ./hermes/data:/opt/data security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - SETUID - SETGID - DAC_OVERRIDE deploy: resources: limits: memory: 3G cpus: '2' healthcheck: test: ['CMD-SHELL', 'pgrep -f "bin/hermes[ ]gateway run" > /dev/null || exit 1'] interval: 30s ``` No `ports` section: that is deliberate and it is the heart of the security model. The only inbound path is outbound Telegram long-polling. > **Caution - cap_drop and s6-overlay** > > The set `CHOWN, SETUID, SETGID` alone breaks boot: s6-supervise loops on "unable to open supervise/lock: Permission denied". The image uses s6-overlay, whose stage 2 runs as root to remap PUID/PGID — without `DAC_OVERRIDE`, root can no longer touch files owned by the remapped user. The minimal set validated on throwaway containers is **`CHOWN, SETUID, SETGID, DAC_OVERRIDE`**; `FOWNER` and `KILL` are unnecessary. > **Tip - The healthcheck trick** > > The pattern `bin/hermes[ ]gateway run` uses a character class for a precise reason: without it, `pgrep -f` also matches the shell running the healthcheck itself, and the container reports healthy whatever the gateway's actual state. ### Configuration Hermes generates and migrates its own `config.yaml` on first start; only this deployment's keys are carried over by hand. ```yaml model: provider: "openai-codex" default: "gpt-5.5" base_url: "https://chatgpt.com/backend-api/codex" mcp_servers: n8n-tools: # normal flow url: "http://n8n:5678/mcp/hermes-tools" headers: Authorization: "Bearer ${HERMES_N8N_MCP_TOKEN}" timeout: 120 n8n-admin: # workflow debugging ONLY url: "http://n8n-mcp:3000/mcp" headers: Authorization: "Bearer ${N8N_MCP_AUTH_TOKEN}" tools: include: [n8n_list_workflows, n8n_get_workflow, n8n_executions, n8n_validate_workflow] memory: provider: "mnemosyne" plugins: enabled: - telegram-voice-transcriptor gateway: platforms: telegram: extra: allow_from: - "" # LITERAL value required ``` > **Caution - allow_from does not expand variables** > > `gateway.platforms.telegram.extra.allow_from` is read **raw** by the Telegram plugin: a `${VAR}` is never resolved there, it is compared as-is against the chat_id. Worse, as soon as this list is defined it becomes the sole authority and **replaces** the allowlist passed through the `TELEGRAM_ALLOWED_USERS` environment variable. The very first real message was blocked on exactly this. The chat_id must therefore be written literally in the config file. > **Danger - Never share the Codex login** > > Hermes automatically imports `~/.codex/auth.json` if it finds one. Mounting the host's `~/.codex` — or `cli-ollama`'s — into the container causes concurrent token rotations that invalidate both sides. The deployment uses a **dedicated OAuth login**, obtained once through a device flow inside the container, and nothing else: > > ```bash > docker compose -f ai-stack/docker-compose.yaml exec hermes hermes auth add codex-oauth > ``` ### The system prompt as a security layer Some rules cannot be enforced by plumbing: they live in `SOUL.md`, occupying the first slot of the system prompt. - **Normal flow**: every business action goes through `n8n-tools`. - **`n8n-admin` = debug only**, never for a business action, and never to modify a workflow. - **`docker_manage` = sensitive action**: no approval chain exists downstream, so the agent must ask for explicit confirmation before every `start`/`stop`/`restart`/`update`, naming the stack and the action. - **Vault**: always cite the source file path; writes only through `vault_write`, confined, and never on its own initiative. - **No unsolicited action**: the agent does what is asked and offers the rest. > **Note - The prompt is not access control** > > These rules are behavioural guardrails, not technical barriers. The real barriers are elsewhere: mandatory Bearer token, fail-closed Telegram allowlist, `tools.include` filtering on `n8n-admin`, action whitelist and blocking of destructive actions on `security-stack` coded into the `Docker Actions` workflow, path confinement in the vault gateway. The prompt adds a layer of caution on top; it does not replace them. ### What plugs into the gateway Three extension mechanisms coexist, and the distinction between them structures the whole deployment. | Mechanism | Nature | Where it lives | Detail | |-----------|--------|-----------|--------| | **MCP tools** | N8N nodes | `Hermes MCP Tools` workflow | [32 tools](/en/hermes/outils-mcp/) across 8 domains | | **Skills** | Markdown | `/opt/data/skills/` | [145 active](/en/hermes/skills/), 12 of them custom | | **Plugins** | Python | `/opt/data/plugins/` | [4 plugins](/en/hermes/plugins/), hooked into the lifecycle | The dividing rule: an MCP tool makes an action **possible**, a skill makes it **well done**, a plugin triggers it **without the agent deciding**. And long-term memory — [Mnemosyne](/en/hermes/memoire-mnemosyne/) — installs through the third mechanism, in wrapper mode. > **Tip - Everything meant to last lives in /opt/data** > > That is the principle common to all three. The application tree `/opt/hermes` belongs to the image: whatever you change there disappears on the next update, silently. Skills, plugins, the Mnemosyne venv, configuration and sessions therefore all live in the bind mount — they survive recreates and updates alike, and go into the daily backup. ### Security | Surface | Protection | |---------|-----------| | N8N MCP endpoint | Bearer required — 403 without a token, verified end-to-end | | MCP endpoint from the Internet | `/mcp/*` and `/mcp-test/*` return 404 at Caddy; only the internal `n8n:5678` path works | | Telegram bot | Strict fail-closed allowlist — an unknown chat_id is blocked without a reply | | Destructive Docker actions | Action whitelist in `Docker Actions`, `security-stack` not actionable | | `docker_manage`, `crm_delete_lead`, `timesheet_delete` | No downstream approval chain → confirmation required by the system prompt | | Vault writes | Confined to `research/` and `inbox/`, `.md` extension, anti-traversal, 150,000-byte cap | | Secrets | Passed through the compose environment, never written into `/opt/data` (which goes into the backup) | The confinement of `vault_write` deserves a word, because it solves a classic problem with agents that write: blind overwrites. A read through `vault_read` returns a `contentHash`; modifying an existing file requires passing that hash back as `Base_Hash`. Without a hash, the reply is `__EXISTS__`; if the file has moved since the read, `__STALE_READ__`. And errors **never** return the current hash — otherwise the model could harvest it and overwrite without having read. [Implementation detail](/en/hermes/outils-mcp/). ### Backup The data directory goes into the daily backup to Google Drive, after exclusions: OAuth credentials (`auth.json`, `google_token.json`, `google_client_secret.json`), Git credentials, the Mnemosyne venv and the model caches. The memory database, however, **is** included. The archive thus drops to 8.2 MB instead of 58 MB. After a full restore, three manual steps: redo the Codex device flow, redo the Google OAuth flow, reinstall the Mnemosyne venv. ### Operations commands ```bash docker exec hermes hermes mcp test n8n-tools # live MCP connection + tool list docker exec hermes hermes auth list # OAuth credential loaded? docker exec hermes hermes plugins list # plugins enabled/disabled docker exec hermes hermes memory status # active memory provider docker exec hermes hermes gateway status docker exec hermes hermes -z "ping" # one-shot LLM turn (consumes quota) ``` > **Danger - One container per data directory** > > Two Hermes containers mounting the same `/opt/data` corrupt the sessions. That applies to a test container left running too: this is the trap that followed the migration from PoC to production. --- ## 4. What if? — Outlook and limits ### Current limits | Limit | Impact | Mitigation | |--------|--------|------------| | **Shared ChatGPT quota** | The subscription serves Hermes, Codex CLI and the N8N workflows. Cap reached = the bot returns errors until reset | `codex-usage-alert` plugin on a threshold; route non-interactive usage to `gemini-flash` | | **Guardrails carried by the prompt** | `docker_manage` and deletions have no technical validation downstream | Action whitelist in `Docker Actions`; a real Telegram approval chain remains to be wired | | **Reactive v1** | No scheduled task: the agent does nothing unprompted | Deliberate until the guardrails are hardened | | **Hard-coded `employee_id`** | `timesheet_log_hours` always writes against employee 1 | Moot today (a single employee), to be parameterised if the team grows | | **CRM stage mapping** | Stage IDs are encoded in the `$fromAI` descriptions | `crm_list_stages` now provides the dynamic source | | **MCP sub-nodes cannot be disabled** | Disabling a tool in `Hermes MCP Tools` makes every subsequent tool execute the wrong node, silently | Delete the node rather than disable it ([detail](/en/hermes/outils-mcp/)) | ### Evolution scenarios **If the agent becomes proactive**: - Dropping jobs into `/opt/data/cron/` activates the scheduler (inactive while it is empty). - Prerequisite: a real approval chain on sensitive actions, not just a prompt rule. - Natural candidates: morning digest of Prometheus alerts, follow-up on dormant CRM leads. **If memory has to leave the VPS**: - Export Mnemosyne memories as markdown notes into the vault, to make them readable and versioned. - Then bidirectional sync VPS ↔ workstation, for a single memory shared between the agent and the local assistant. [Detail](/en/hermes/memoire-mnemosyne/). **If the write scope widens**: - The `research/` + `inbox/` confinement is a whitelist of prefixes: extending it is trivial, but every new prefix must come with its justification. - The commit + immediate push sequence already propagates to the Obsidian clients; raising the write throughput will raise the noise in the Git history. **If a second user needs access**: - The allowlist accepts several chat_ids, but memory and the user profile are global to the container. - It would take one container per user (separate data directories) rather than a widened allowlist. ### Troubleshooting commands ```bash # The bot is not responding docker logs hermes --tail 100 docker inspect hermes --format='{{.State.Health.Status}}' # Message ignored → allowlist docker logs hermes 2>&1 | grep -i "unauthorized" # MCP tools missing or stale docker exec hermes hermes mcp test n8n-tools docker compose -f ai-stack/docker-compose.yaml restart hermes # Memory unavailable docker exec hermes hermes memory status docker exec --user hermes -e HOME=/opt/data -e HERMES_HOME=/opt/data hermes \ /opt/data/.mnemosyne/venv/bin/mnemosyne stats # Check the MCP endpoint is still closed from outside curl -so /dev/null -w '%{http_code}\n' https://n8n.guigpap.com/mcp/hermes-tools # expected: 404 ``` --- ## Related pages ### Hermes - [The 32 MCP tools](/en/hermes/outils-mcp/) — Catalogue, gateways and confined writes - [Mnemosyne long-term memory](/en/hermes/memoire-mnemosyne/) — Wrapper mode, hybrid recall - [Custom skills](/en/hermes/skills/) — Twelve procedures written for this deployment - [Plugins and their hooks](/en/hermes/plugins/) — Four Python extensions of the gateway ### Infrastructure - [AI Stack](/en/infrastructure/ai-stack/) — Qdrant, CLI Ollama and the MCP gateway - [VPS architecture](/en/infrastructure/architecture-vps/) — Overview and network topology - [Security Stack](/en/infrastructure/security-stack/) — Caddy, external blocking of internal endpoints - [Database backup](/en/infrastructure/database-backup/) — Backing up the data directory ### Workflows - [Content Pipeline](/en/workflows/content-pipeline/) — The `vps-vault` Obsidian vault and its mirrors - [Conversational system](/en/workflows/systeme-conversationnel/) — The other Telegram agent, through CLI Ollama - [Codex CLI Integration](/en/workflows/codex-cli-integration/) — `cli-ollama`'s Codex login, distinct from Hermes' - [Error Handler](/en/workflows/error-handler/) — Error workflow for the gateways ### Reference - [Glossary](/en/reference/glossary/) — MCP, Autonomous agent, Long-term memory, LLM ## Métadonnées agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0