Skip to content

Hermes Agent: an autonomous agent on Telegram

Hermes Agent is an autonomous conversational agent released by Nous Research 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.

Building blockDetail
Containernousresearch/hermes-agent:latest — no published port, n8n-internal + mcp-backend networks
LLMChatGPT/Codex subscription through the native openai-codex provider (gpt-5.5)
Business tools32 MCP tools served by the N8N workflow Hermes MCP Tools
Debug tools4 read-only N8N tools (n8n-mcp), reserved for workflow diagnostics
MemoryExternal Mnemosyne provider — local SQLite, hybrid vector + FTS5 search
Plugins3 in-house plugins hooked into the gateway lifecycle

N8N

hermes container · no published port

Bearer · normal flow · 32 tools

Bearer · debug only

MCP stdio · outside N8N

Business targets

Odoo · projects, tasks, contacts, CRM, timesheets

Docker · status, logs, start/stop/restart

Prometheus · alerts, PromQL

Obsidian vault vps-vault · read + confined write

Telegram · dedicated bot · strict allowlist

Hermes gateway · gpt-5.5 via openai-codex

Mnemosyne · SQLite + FTS5 · /opt/data

Plugins · voice, message editor, usage alert

MCP Server Trigger · /mcp/hermes-tools

n8n-mcp · 4 diagnostic tools

Plaud · official MCP server · stdio

This article covers the deployment: container, configuration, security model and operations. Three articles detail what plugs into it.

ArticleContent
The 32 MCP toolsFull catalogue, typed-input gateways, confined vault writes, pitfalls of the N8N MCP layer
Mnemosyne long-term memoryWrapper mode, hybrid search, 20 memory tools, consolidation and day-to-day use
Custom skillsThe skill system and the twelve procedures written for this deployment
Plugins and their hooksFour Python plugins hooked into the gateway lifecycle

Taken in a scoping session before the first line of configuration, they explain almost the entire shape of the deployment.

DecisionReason
LLM direct through openai-codex, bypassing cli-ollamaThe 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 accessOne channel of curated business tools, one read-only diagnostic channel. The separation is explicit, not implicit
Telegram bot onlyNo port, no Caddy route, no dashboard: the HTTP attack surface is nil
Service in ai-stack/docker-compose.yamlCohabits with Qdrant and cli-ollama, same internal networks, same lifecycle
Reactive only in v1The scheduler and automations stay disabled: an agent that acts unprompted is hard to audit until you trust its guardrails
Data dir in the daily backupAll accumulated value (memory, sessions, skills) lives in a single directory — losing it means starting over

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.

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.


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.

Hermes generates and migrates its own config.yaml on first start; only this deployment’s keys are carried over by hand.

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:
- "<CHAT_ID>" # LITERAL value required

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.

Three extension mechanisms coexist, and the distinction between them structures the whole deployment.

MechanismNatureWhere it livesDetail
MCP toolsN8N nodesHermes MCP Tools workflow32 tools across 8 domains
SkillsMarkdown/opt/data/skills/145 active, 12 of them custom
PluginsPython/opt/data/plugins/4 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 — installs through the third mechanism, in wrapper mode.

SurfaceProtection
N8N MCP endpointBearer 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 botStrict fail-closed allowlist — an unknown chat_id is blocked without a reply
Destructive Docker actionsAction whitelist in Docker Actions, security-stack not actionable
docker_manage, crm_delete_lead, timesheet_deleteNo downstream approval chain → confirmation required by the system prompt
Vault writesConfined to research/ and inbox/, .md extension, anti-traversal, 150,000-byte cap
SecretsPassed 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.

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.

Fenêtre de terminal
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)

LimitImpactMitigation
Shared ChatGPT quotaThe subscription serves Hermes, Codex CLI and the N8N workflows. Cap reached = the bot returns errors until resetcodex-usage-alert plugin on a threshold; route non-interactive usage to gemini-flash
Guardrails carried by the promptdocker_manage and deletions have no technical validation downstreamAction whitelist in Docker Actions; a real Telegram approval chain remains to be wired
Reactive v1No scheduled task: the agent does nothing unpromptedDeliberate until the guardrails are hardened
Hard-coded employee_idtimesheet_log_hours always writes against employee 1Moot today (a single employee), to be parameterised if the team grows
CRM stage mappingStage IDs are encoded in the $fromAI descriptionscrm_list_stages now provides the dynamic source
MCP sub-nodes cannot be disabledDisabling a tool in Hermes MCP Tools makes every subsequent tool execute the wrong node, silentlyDelete the node rather than disable it (detail)

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.

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.
Fenêtre de terminal
# 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

  • Glossary — MCP, Autonomous agent, Long-term memory, LLM