Safeguard
Supply Chain Security•

Hermes Agent Architecture: Agent Loop, Tools, Backends, Gateway, MCP

Inside Hermes Agent: six entry points feed one AIAgent loop with a tool registry, sandboxed terminal backends, a messaging gateway and an MCP client.

Safeguard Security Team
42 min read

Hermes Agent, the open source agent from Nous Research, is built around a single AIAgent loop that every entry point shares: the CLI, the messaging gateway, the editor integration (ACP), the batch runner, the API server and the Python library. Each turn assembles a cached system prompt, resolves a model provider, calls the model, dispatches tool calls through a central registry, and runs shell commands on one of several terminal backends, from your own machine to a hardened container. MCP servers plug into the same registry as extra tools.

This post walks through each part as the Hermes docs describe it on 23 October 2026 (latest release v0.22.5), and what each design choice means for whoever runs, backs up and investigates the agent. Hermes ships several releases a week, so treat every count, default and key name as a dated snapshot.

Key takeaways

  • One loop, many doors. CLI, gateway, ACP, API server, batch runner and cron all create the same AIAgent; platform differences live in the entry point and its toolset preset.
  • The system prompt is built once per session and cached. Mid-session memory writes reach disk, not the running prompt, which keeps prompt caching effective and a session's instructions stable.
  • Provider choice is a config value. A shared resolver maps provider and model to one of three API modes and scopes each API key to its own base URL.
  • Messaging presets carry the full toolset. Most chat presets are "Same as hermes-cli", terminal included, so who can message the agent is a code execution question.
  • The terminal backend is the isolation boundary. local has none; container and cloud sandbox backends skip the dangerous command check, so their configuration is the control.
  • MCP servers are first-class tools with filtered environments. MCP sampling, where a server asks Hermes to run the model for it, is on by default with rate limits.
  • Everything lives under one directory. ~/.hermes/ holds config, secrets, memory, skills, a searchable SQLite transcript store, pairing approvals and hooks. Back it up, lock it down and treat it as evidence.

The architecture in one table

The developer guide's architecture page, flattened into a table:

LayerWhat it doesWhere it lives (per the docs)
Entry pointsCLI, messaging gateway, ACP editor server, batch runner, API server, Python librarycli.py, gateway/run.py, acp_adapter/, batch_runner.py
Agent loopBuilds the prompt, resolves the provider, calls the model, dispatches tools, retries, compresses, persistsrun_agent.py facade, loop in agent/conversation_loop.py and agent/turn_*.py
Prompt builderAssembles stable, context and volatile prompt tiersagent/system_prompt.py, agent/prompt_builder.py
Provider resolutionMaps provider and model to API mode, key and base URLhermes_cli/runtime_provider.py, providers/, plugins/model-providers/
Tool registrySelf-registering tools grouped into toolsets, availability checks, dispatchtools/registry.py, model_tools.py, toolsets.py
Tool backendsTerminal, browser, web, MCP, file, visiontools/environments/, tools/mcp_tool.py and siblings
Session storageSQLite with FTS5 full-text search, session lineagehermes_state.py and hermes_state_*.py
GatewayLong-running process with platform adapters, authorization, sessions, hooks, cron tickinggateway/, plugins/platforms/
PluginsUser, project and pip entry point plugins that add tools, hooks and commands~/.hermes/plugins/, .hermes/plugins/

Two of the page's design principles explain most of what follows: "Platform-agnostic core" (one AIAgent class serves every entry point) and "Prompt stability" (the system prompt does not change mid-conversation except through explicit user actions such as switching models).

How the architecture got here: a dated timeline

Dates below come from GitHub's releases API, with the date printed in the release notes where they differ; the launch date is from nousresearch.com/releases. Only releases that changed the architecture are listed.

Release (tag)PublishedArchitecture change, from the release notes
Repository created22 Jul 2025Pre-public foundation (v0.1.0 per later notes)
Public launch25 Feb 2026Listed on nousresearch.com/releases
v0.2.0 (v2026.3.12)12 Mar 2026First tagged release: multi-platform messaging gateway, native MCP client with stdio and HTTP transports and sampling, ACP server for editors, centralized provider router
v0.3.0 (v2026.3.17)17 Mar 2026First-class plugin architecture, native Anthropic provider, smart approvals, persistent shell for local and SSH backends
v0.5.0 (v2026.3.28)28 Mar 2026Plugin lifecycle hooks in the loop (pre_llm_call and others), a hosted sandbox backend moved to its provider's native SDK, a supply chain audit
v0.7.0 (v2026.4.3)3 Apr 2026Pluggable memory providers, API server session continuity, editor-provided MCP servers over ACP, secret exfiltration blocking
v0.13.0 Tenacity7 May 2026Providers become plugins, MCP SSE transport with OAuth, sessions survive gateway restarts, redaction on by default
v0.15.0 Velocity28 May 2026run_agent.py cut from 16,083 to 3,821 lines across 14 agent/* modules, curated MCP catalog
v0.16.0 Surface6 Jun 2026 (notes: 5 Jun)Native desktop app, desktop against a remote gateway, web dashboard as admin panel
v0.18.0 Judgment1 Jul 2026Mixture-of-Agents as a selectable model, background fan-out for subagents, gateway scale-to-zero
v0.19.0 Quicksilver20 Jul 2026Pluggable secret sources (password manager integrations), durable delivery ledger, profile-based routing in one multiplexed gateway
v0.21.031 Aug 2026Steering of running subagents, merged MCP management page, write approval always required for protected instruction files
v0.22.0 Threshold14 Oct 2026 (notes: 13 Oct)Tested SemVer stable releases, signed desktop packages, one gateway and one backend per machine serving every profile
v0.22.523 Oct 2026 (notes: 22 Oct)Patch roll-up; latest release on the day this post was checked

Tags moved from calendar versions (v2026.3.12 to v2026.9.24) to semantic versions at v0.21.6 on 8 October 2026. If you pin Hermes, pin the semantic version tag.

Entry points: six doors into one loop

The architecture page names six entry points: the CLI, the messaging gateway, ACP, the batch runner, the API server and the Python library. They differ in who is on the other end, which toolset preset they load, and what happens when a dangerous command needs a human.

Entry pointWho is on the other endDefault toolset preset (toolsets reference)When a dangerous command needs approval
CLI and TUI (hermes, hermes chat)You, at a terminalhermes-cli, the full toolsetInteractive prompt: approve, deny or allow permanently
Messaging gatewayWhoever is authorized on Telegram, Discord, Slack, email and the other adaptershermes-<platform>, mostly "Same as hermes-cli"Approval request sent into the chat; /approve and /deny bypass the busy guard
ACP (editor integration)VS Code, Zed or JetBrains over stdio JSON-RPChermes-acp, which drops clarify, cron, image and speech toolsRendered as an approval prompt in the editor
API serverAny OpenAI-compatible frontend or scripthermes-api-server, which drops clarify and a few interactive toolsapprovals.unattended_mode, default deny, unless the client can answer the approval card
Cron jobsNobody; the schedulerhermes-cron, "Same as hermes-cli"approvals.cron_mode, default deny
Batch runner and Python libraryYour own codeWhatever you pass inWhatever callbacks you wire up

Three details matter for a threat model. The API server "gives full access to hermes-agent's toolset, including terminal commands"; it binds to 127.0.0.1:8642 by default, API_SERVER_KEY is required even on loopback, and CORS is off unless you allowlist origins. ACP is the Agent Client Protocol, which its site describes as standardizing "communication between code editors/IDEs and coding agents"; since v0.7.0 an editor can hand Hermes its own MCP servers over ACP, so the editor's MCP config is part of the agent's tool surface. And the session storage page says the profile's gateway is "the only writer of canonical sessions": the CLI, desktop app, API server, ACP and cron submit input to it, so a stopped gateway affects every surface.

Inside the agent loop: one turn, step by step

The agent loop internals page describes each turn of run_conversation() like this, paraphrased:

  1. Append the user message to history and build (or reuse) the cached system prompt.
  2. Run preflight compression if the conversation exceeds 50 percent of the context window.
  3. Convert history to the provider's format and add ephemeral layers such as budget warnings, plus prompt caching markers where supported.
  4. Make an interruptible API call.
  5. If the response has tool calls, execute them, append the results and go back to step 3. If it is text, persist the session, flush memory if needed and return.

Every message is stored in the OpenAI-style role and content format whatever the provider, with strict alternation: only tool results may appear consecutively, which is why compression must keep tool call and result pairs together. The API call runs in a background thread; a new message or /stop abandons it and discards the partial response.

Tool execution and the four tools the loop keeps for itself

Several tool calls in one response run concurrently in a thread pool (interactive tools such as clarify force sequential execution), and results go back in the original order. Each call passes the pre_tool_call plugin hook, the dangerous command check, the handler and post_tool_call. Four tools are handled by the loop itself because they need agent state: the todo list, memory, session_search and delegate_task.

Iteration budgets: where the docs disagree

The agent loop page says agent.max_turns defaults to 500 and delegation.max_iterations to 50. The configuration page says agent.max_turns is "unlimited by default", and the delegation page gives 250 per subagent. We treat the user guide values as current, and recommend setting both explicitly.

Fallback providers

On rate limits, server errors or authentication errors, the loop walks a fallback_providers list, swapping model, provider, base URL and API mode in place (trying a credential refresh first on 401 and 403). Auxiliary tasks have their own chains under auxiliary.

How the system prompt is assembled

The prompt assembly page calls the split between cached prompt state and per-call additions "one of the most important design choices in the project". The cached system prompt has three ordered tiers.

TierContents, in order (prompt assembly page)Changes when
StableIdentity (SOUL.md or the built-in default), tool and model guidance, coding brief, platform hintsRarely; config changes take effect at the next rebuild
ContextCaller-supplied system message, the project context file, the git workspace snapshot, operator instructionsWhen you change directory or project
VolatileSkills index, MEMORY.md snapshot, USER.md snapshot, external memory provider block, timestamp, session and model line, runtime environment (host, home, working directory)At session start and on rebuild

The page is inconsistent about the skills index (the tier list says volatile, a later sentence says stable); either way it is in the cached prompt.

Context files: first match wins

Only one project context type loads, in priority order: .hermes.md or HERMES.md (searched up to the git root), AGENTS.md, CLAUDE.md, then .cursorrules. Each is scanned for injection patterns (instructions to ignore prior instructions, hidden HTML comments, attempts to read secrets, exfiltration through curl, invisible Unicode); a hit replaces it with a [BLOCKED: ...] marker. The docs say plainly: "These patterns are heuristics, not semantic intent detection." Files are truncated to a cap that scales with the context window (20,000 character floor). Your own SOUL.md only triggers a warning, because writes to it need your approval, but a SOUL.md installed by a profile distribution is still blocked on a hit.

Why the memory snapshot is frozen

The prompt assembly page says mid-session memory writes "update disk state but do not mutate the already-built cached system prompt" until a rebuild at a new session or after compression. The reasons: prefix caching only works if the prefix is byte-identical; a session runs under one fixed set of standing instructions; and per-call context (gateway overlays, pre_llm_call plugin context, external memory recall) goes into the current user message so it cannot become "persistent prompt state".

The defender's corollary: a poisoned memory entry acts from the next session onwards, every session, until someone removes it. The sessions table in state.db stores each session's system_prompt, so you can see exactly what a past session was told. Nous also reviews the rendered prompt and tool list for nine surfaces as committed CI fixtures that need core team approval to change, a pattern worth copying.

Provider resolution and the three API modes

One resolver serves the CLI, gateway, cron, ACP and auxiliary calls, mapping provider and model to an API mode, key and base URL. Precedence is an explicit runtime request, then config.yaml, then environment variables, then defaults, which "prevents a stale shell export from silently overriding the endpoint a user last selected".

API modeUsed for (agent loop page)Client
chat_completionsOpenAI-compatible endpoints, including OpenRouter, custom endpoints and most providersopenai.OpenAI
codex_responsesThe Responses API path the docs describe for OpenAI Codexopenai.OpenAI in Responses format
anthropic_messagesAnthropic's native Messages APIanthropic.Anthropic through an adapter

All three convert to the same internal format, so tools, sessions and compression behave the same whichever provider runs a turn. Providers are plugins, and a user plugin in the Hermes home overrides a bundled one of the same name. "No lock-in" therefore means switching provider is a config change (hermes model), history survives the switch, and any OpenAI-compatible server, including a local one, is first-class. It does not mean every model behaves the same: some guidance and compression thresholds are per model family.

Three details matter for security. Each API key is scoped to its own base URL, so a key is not sent to a different endpoint. When Hermes has no login of its own for the Anthropic or OpenAI Codex providers, it can adopt and refresh the logins of those vendors' own CLI tools; auth.adopt_external_logins: false turns that off. And auxiliary tasks (vision, compression summaries, memory flushes, MCP helpers) can be routed to a different provider, so conversation content may reach a second provider.

Tools, toolsets and platform presets

Registration and availability

Each file in tools/ calls registry.register() at import time with a name, toolset, schema, handler and optional availability check; model_tools.py finds them by parsing each file, then adds MCP and plugin tools. A registration that would shadow another toolset's tool is rejected, and a plugin overriding a built-in also needs an operator opt-in. If a tool's check_fn fails, the tool is left out of the schema list, and schemas that reference other tools are patched so the model cannot call what is not there. Errors come back as JSON strings, never exceptions.

The architecture page says "70+ registered tools across ~28 toolsets"; the README says "40+ tools". Run hermes tools for the real list on your version.

Toolset kinds and presets

The toolsets reference describes core toolsets (such as file), composites (such as debugging: file, terminal and web) and platform presets, plus a dynamic mcp-<server> toolset per MCP server.

PresetWhat the toolsets reference says
hermes-cliFull toolset, default for interactive sessions: file, terminal, web, browser, memory, skills, vision, delegation, code execution, cron, session search and more
hermes-acpDrops clarify, cron, image generation, speech, computer use and Kanban tools
hermes-api-serverDrops clarify, speech, computer use and Kanban tools; keeps everything else
hermes-cronSame as hermes-cli
hermes-telegram, hermes-slack, hermes-whatsapp, hermes-signal, hermes-email and most other chat presetsSame as hermes-cli
hermes-discordhermes-cli plus Discord and Discord moderation tools
hermes-webhookRestricted subset: web search, web extract, vision and clarify; no terminal, file or browser
safe (core)Read-only research and media generation; no file writes, no terminal

A chat preset gives whoever may message the bot the tools you have at your terminal. Narrow it per platform with hermes tools (which writes platform_toolsets) or globally with agent.disabled_toolsets, which always wins. Delegation cannot widen a toolset: "the model cannot grant a child capabilities that the parent does not have", and children lose clarify, memory writes, cross-platform messaging and cron.

The dangerous command check

Every terminal command is matched against regular expressions for recursive deletes, formatting, destructive SQL, system config writes, service changes, piping downloads into a shell and similar. Approvals last for the session, and "allow permanently" writes to command_allowlist. approvals.mode is smart (default: an auxiliary model approves low-risk matches, denies clearly dangerous ones, escalates the rest), manual, or off (the yolo mode). Below these sit a hardline blocklist nothing overrides and approvals.deny, a user glob list checked before yolo.

Terminal backends and the isolation each one gives

The README says "Six built-in terminal backends" plus plugins such as Singularity/Apptainer; the architecture diagram says seven. We use the security page's seven-row comparison, naming the three hosted cloud sandbox backends generically.

BackendIsolation (security page)Dangerous command checkThe docs' "best for"What it means for you
local (default)None; runs on hostYesDevelopment, trusted usersThe agent runs as your user with your files, keys and network
sshRemote machineYesRunning on a separate serverKeeps execution off the gateway host; the agent "can't modify its own code" per the tools page
dockerContainerSkipped (container is boundary)Production gatewayIsolation depends on the container settings, the image and what you forward in
singularity (plugin)ContainerSkippedHPC environmentsRootless containers; now an official plugin rather than built in
Three hosted cloud sandbox backendsCloud sandbox or cloud microVMSkippedScalable or persistent cloud workspacesExecution leaves your network; credentials and synced files go with it

What the Docker backend actually does

Per the security page, every container drops all Linux capabilities and adds back DAC_OVERRIDE, CHOWN and FOWNER, sets no-new-privileges, limits processes to 256, and mounts a size-limited /tmp and a noexec /var/tmp. Docker's security docs explain the capability model behind this. Three behaviors surprise people:

  • One container per process, not per command. The tools page says Hermes starts a single long-lived container on first use and routes every terminal, file and execute_code call into it with docker exec. Packages installed, directories changed and files written persist "across /new, /reset, and delegate_task subagents, for the lifetime of the Hermes process". With container_persistent: true (the default), /workspace and /root are bind-mounted from ~/.hermes/sandboxes/docker/<task_id>/ and survive restarts too.
  • Forwarded variables are exposed variables. terminal.docker_forward_env is empty by default. Anything you add is visible to every command in the container, which the docs say "can read and exfiltrate them". Skill-declared variables are now forwarded automatically when the skill loads.
  • The approval prompt goes away. Because the container is treated as the boundary, dangerous commands inside it are not checked. That is reasonable only if the container really is a boundary: no Docker socket mounted, no host home directory mounted, no broad network access to internal services.

Environment filtering per sandbox

The security page's filtering table is the best summary of credential exposure:

SandboxDefault filter
execute_codeBlocks variables with KEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, PASSWD or AUTH in the name; only safe-prefix variables pass
terminal (local)Blocks explicit Hermes infrastructure variables such as provider keys and gateway tokens
terminal (Docker)No host environment variables by default
terminal (SSH)No host environment variables by default; passthrough uses SendEnv and needs matching AcceptEnv on the server
MCP stdio serversEverything blocked except safe system variables and the server's own env block

Note that local only removes Hermes's own secrets; a cloud credential exported in your shell is still visible to the agent's commands.

The messaging gateway: users, pairing, sessions and cron

The gateway is a long-running process with "25+ platform adapters" (architecture page) that authorizes users, routes sessions, dispatches slash commands, runs hooks and ticks cron.

Who is allowed to talk to the agent

The security page checks a per-platform allow-all flag, the DM pairing list, platform allowlists, a global allowlist, a global allow-all, then denies; the gateway internals page puts platform allowlists before pairing. Both agree on the part that matters: with no allowlist and no allow-all, everyone is denied.

With DM pairing, an unknown user gets an eight-character code and you approve it with hermes pairing approve <platform> <code>. Codes come from secrets.choice(), expire in an hour, are rate limited, and five failed approvals cause a one-hour lockout. unauthorized_dm_behavior is pair by default on chat platforms (ignore for email), or decline. Approval is permanent until revoked, and a paired user gets the platform preset, so approving a code is closer to granting shell access than adding a contact.

Sessions and message flow

Each message becomes a MessageEvent, passes a two-level busy guard, is authorized and is routed by a session key of the form agent:{namespace}:{platform}:{chat_type}:{chat_id}. group_sessions_per_user: true (default) gives each group participant their own session. /approve, /deny, /stop and a few others bypass the busy guard so an approval can reach an agent blocked on it.

Hooks are trusted by placement

Plugins need plugins.enabled and MCP servers must be listed, but gateway event hooks are the documented exception: any valid directory under <profile home>/hooks/ is imported in-process at startup with no prompt. The security page's reasoning is that write access to the profile home already means code execution. Either way, write access to ~/.hermes/ is code execution as the gateway user.

Cron runs unattended, by design

Jobs live in ~/.hermes/cron/jobs.json. Each tick creates a fresh AIAgent with no history, injects attached skills, runs the prompt and delivers the result. An unpinned job runs on whatever the main model is when it fires. No-agent jobs run a script from HERMES_HOME/scripts with no model at all. approvals.cron_mode and approvals.unattended_mode (webhooks, API server) default to deny. The agent can create cron jobs itself (the tools page calls the tool cronjob, the cron page cronjob_manage), so it can schedule work that runs when you are not watching.

The MCP client

Hermes has had a native MCP client since v0.2.0. The MCP specification defines stdio and Streamable HTTP transports; Hermes supports stdio subprocesses, HTTP, SSE (since v0.13.0), OAuth and mTLS.

How MCP tools enter the registry

At startup Hermes connects to servers (four at a time by default) and registers each tool as mcp_<server>_<tool>, plus resource and prompt wrappers when supported. A server can change its tools at runtime with notifications/tools/list_changed, which Hermes applies automatically, and a running gateway picks up config.yaml edits within about a minute. Per-server filtering is the main exposure control:

mcp_servers:
  issues:
    command: "npx"
    args: ["-y", "@modelcontextprotocol/server-github"]
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PAT_READONLY}"
    tools:
      include: [list_issues, search_code]   # only these are registered
      prompts: false                         # no prompt wrappers
      resources: false                       # no resource wrappers
    sampling:
      enabled: false                         # this server cannot ask Hermes to run the model

include and exclude accept glob patterns, and when both are present, include wins. If filtering removes every tool and wrapper, Hermes creates no toolset for that server.

Credential handling

Stdio servers get only PATH, HOME, USER, LANG, LC_ALL, TERM, SHELL, TMPDIR and XDG_* from the host, plus their own env block. Error messages are scrubbed of common token formats, and invisible Unicode tag characters, "a classic prompt-injection smuggling channel", are stripped from results and descriptions. These controls reduce accidental leakage; they do not make an untrusted server safe, since its descriptions and results still reach the model. Our posts on tool poisoning through MCP tool descriptions and securing MCP servers and agent skills go deeper on that.

Sampling: a server asking Hermes to think for it

With sampling, a server sends sampling/createMessage and the client runs the model for it. The MCP specification says "there SHOULD always be a human in the loop with the ability to deny sampling requests". Hermes enables sampling by default with limits instead of a prompt: 10 requests per minute, 4,096 tokens per response, a 30 second timeout, five tool rounds, an optional model allowlist and audit logging. For servers you did not write, consider turning it off.

The catalog's trust model

Installing an entry from the Nous-reviewed MCP catalog "runs whatever the manifest specifies": git clone, bootstrap commands such as pip install, and the server's code. The docs still advise reading the manifest before installing.

Subagents, Mixture-of-Agents and execute_code

Subagents

delegate_task spawns child AIAgent instances that "Know Nothing" beyond the goal and context the parent writes (plus the workspace's project context file). Only the child's summary returns. Up to ten run concurrently by default, with no wall-clock timeout, and delegation is flat unless you raise delegation.max_spawn_depth. Since v0.19.0 each dispatch writes a live transcript file; for forensics those are the record of what a child did, because the parent only sees the summary.

Mixture-of-Agents

Mixture-of-Agents is a virtual provider: a preset appears as a model under moa. Reference models see only user and assistant text, without tools or the system prompt; their outputs become private context for the aggregator, "the acting model", which emits tool calls and is billed for the run. MoA changes who decides, not which tools exist.

execute_code: programmatic tool calling over a local socket

execute_code runs a model-written Python script that imports a generated hermes_tools module; each call goes over a Unix domain socket to the parent and through normal dispatch, so approvals still apply, and only printed output returns to the model. Scripts get web, file and foreground terminal tools, never delegation or MCP. Defaults: 300 seconds, 50 KB of stdout and 50 tool calls per run, with a persistent per-session kernel. The default project mode runs in your working directory and virtual environment; strict isolates scripts in a temporary directory.

What context compression loses

Gateway hygiene compresses at 85 percent of the context window and the agent's compressor at 50 percent. The default engine clears old tool outputs over 200 characters, protects the head and a token-budgeted tail, summarizes the middle with an auxiliary model into a structured template (goal, constraints, progress, decisions, files, next steps), and starts a child session. What gets lost:

  • Raw tool outputs outside the tail, beyond what the summary kept.
  • Loaded skill bodies, replaced with a marker telling the model to reload them.
  • The whole middle, if the summary call fails. If the summary model's context window is smaller than the main model's, the compressor "drops the middle turns without a summary, silently losing conversation context".

Memory is flushed first, but instructions typed many turns ago survive only as well as the summary records them. Put standing constraints in context files, skills or config.

Where state lives on disk

The home is HERMES_HOME if set, else ~/.hermes on macOS and Linux or %LOCALAPPDATA%/hermes on Windows; named profiles get their own copy under profiles/<name>/.

Path under the Hermes homeWhat it holds (configuration and security pages)Sensitivity
config.yamlAll non-secret settings: model, terminal backend, approvals, MCP servers, toolsetsHigh: controls what runs
.envAPI keys, tokens, allowlists, SSH backend detailsSecret
auth.jsonOAuth provider credentialsSecret
SOUL.mdAgent identity, first block of the system promptHigh: standing instructions
memories/MEMORY.md and USER.mdPersonal data and standing instructions
skills/Agent-created and installed skillsCode and instructions
state.dbSQLite in WAL mode: sessions (including each session's system prompt), full message history, FTS5 search tables, delivery and worker ledgersFull transcripts, often including tool output
cron/jobs.json scheduled jobsCode that runs unattended
pairing/Pending and approved users per platform, rate limit stateAccess control
hooks/Gateway hooks, loaded without opt-inCode that runs in the gateway
plugins/User plugins and provider overridesCode
sandboxes/docker/<task_id>/Persistent container workspace and root homeAgent-written files
cache/exec/Full output of large execute_code runsTool output
logs/gateway.log, errors.log, process result receipts (secrets redacted)Evidence

The database uses SQLite FTS5, "an SQLite virtual table module that provides full-text search", indexing message content, tool names and tool call arguments. That powers session_search and makes state.db the best forensic source on the machine: you can search every command the agent ran. For backups, hermes sessions recover deliberately skips runtime ledger tables because "a partial ledger could replay started effects", while a full hermes backup restore keeps them. If you copy files yourself, stop the gateway first.

Mapping the architecture to risks and controls

This maps each component to its failure mode, following OWASP's root causes of excessive agency: "excessive functionality; excessive permissions; excessive autonomy". See also our post on agent tool permission design.

ComponentWhat can go wrongHermes control (docs, 23 Oct 2026)What to add yourself
Gateway authorizationAnyone who can message the bot gets the full toolsetDefault deny, allowlists, DM pairing with rate limitsAllowlist IDs, narrow platform_toolsets, review hermes pairing list
Terminal backendCommands run on your host as youDocker hardening flags, SSH and cloud sandbox backendsUse docker or ssh for any gateway; no host mounts or Docker socket
ApprovalsA smart approver misjudges a commandmanual mode, hardline blocklist, approvals.deny, cron and unattended denymanual for shared deployments; audit command_allowlist
Context filesA repository's AGENTS.md steers the agentInjection pattern scan and truncationReview context files in repos you do not own
Memory and skillsA poisoned entry persists into future sessionsWrite scanning, optional write approvalTrack skills and memory in git and review diffs
MCP serversUntrusted tool descriptions, results or sampling requestsEnv filtering, Unicode tag stripping, per-server include and exclude, sampling limitsDisable sampling, least-privilege tokens, read manifests
Hooks and pluginsCode placed in the profile home runs with gateway privilegesPlugins need plugins.enabled; hooks do notPermission the Hermes home 0700, watch hooks/ for changes
API serverLocal processes or browsers drive the agentRequired bearer key, loopback bind, no CORS by defaultStrong key, keep the loopback bind
CompressionEarlier constraints vanish from contextStructured summary, memory flush firstSame-or-larger context window for the summary model
DependenciesThe agent's own Python environment carries a compromised or vulnerable packageBuilt-in advisory check at startup and in hermes doctor; lazy installs pinned by SHA-256Pin a SemVer release, inventory the environment, patch on a schedule

Hermes's own security record is short. GitHub's advisory API returned no published advisories for the repository on 23 October 2026, and CVE.org lists two published records, both on 3 September 2026. We covered CVE-2026-71963, the git core.fsmonitor configuration injection, and CVE-2026-85105, the session management authorization bypass, in their own posts.

Step by step: map and tighten a Hermes install

The steps below use commands and keys from the Hermes docs as of 23 October 2026. Run them on a test profile first if you can (hermes -p <name> gives each profile its own home). Check every key against hermes config check on your version.

1. Back up the Hermes home before changing anything

Warning: later steps change approval behavior, toolsets and pairing, and revoking a pairing removes a person's access. Take a backup first and keep it somewhere the agent cannot write.

# Stop the gateway so state.db is consistent
hermes gateway stop

# Archive the whole home, then restrict the archive
HH="${HERMES_HOME:-$HOME/.hermes}"
tar -czf "$HOME/hermes-home-$(date +%Y%m%d-%H%M).tar.gz" -C "$(dirname "$HH")" "$(basename "$HH")"
chmod 600 "$HOME"/hermes-home-*.tar.gz

2. See what is configured right now

hermes doctor                       # health checks and any active supply chain advisories
hermes config get terminal.backend
hermes config get approvals.mode
hermes config check                 # missing options and stale saved selections
hermes tools                        # enabled toolsets per platform (interactive)
hermes pairing list                 # pending and approved users per platform
ls -la "${HERMES_HOME:-$HOME/.hermes}/hooks" 2>/dev/null   # anything here runs in the gateway

3. Lock down file permissions

HH="${HERMES_HOME:-$HOME/.hermes}"
chmod 700 "$HH"
chmod 600 "$HH/.env" "$HH/auth.json" 2>/dev/null

The production checklist in the security docs also says to never run the gateway as root and to never commit .env to version control.

4. Move execution off the host and tighten approvals

A configuration fragment for a gateway that other people can message. Every key below appears in the Hermes configuration or security pages.

# ~/.hermes/config.yaml (fragment)
terminal:
  backend: docker
  docker_image: python:3.11-slim     # pick and pin an image you control
  docker_forward_env: []             # keep secrets out of the container
  container_cpu: 1
  container_memory: 2048
  container_persistent: false        # tmpfs workspace, discarded on cleanup
  cwd: "/workspace"

approvals:
  mode: manual                       # always ask on dangerous commands
  cron_mode: deny
  single_query_mode: deny
  unattended_mode: deny
  deny:
    - "git push*"                    # example: never push from the agent

agent:
  disabled_toolsets:
    - browser                        # remove what this deployment does not need

group_sessions_per_user: true
unauthorized_dm_behavior: pair

auth:
  adopt_external_logins: false       # do not borrow other CLIs' refresh tokens

Note that with the Docker backend, approvals.mode only applies to whatever still runs outside the container. The hardline blocklist and approvals.deny still apply everywhere.

5. Set explicit allowlists

# ~/.hermes/.env
TELEGRAM_ALLOWED_USERS=123456789
# Do not set GATEWAY_ALLOW_ALL_USERS or any per-platform allow-all flag in production

Warning: the next command removes a user's access immediately. Confirm the platform and ID against hermes pairing list first.

hermes pairing revoke telegram 123456789

6. Filter every MCP server

Use the per-server tools.include list, disable prompt and resource wrappers you do not need, turn off sampling for servers you did not write, and give each server its own least-privilege token in its env block, as in the YAML example in the MCP section above. Then reload:

/reload-mcp

7. Put agent-written instructions under version control

Skills and memory change as the agent works. Tracking them in git gives you a diff for every change and a way to roll one back.

Warning: do not commit .env, auth.json, state.db or pairing/. The ignore file below excludes them; check git status before every commit.

cd "${HERMES_HOME:-$HOME/.hermes}"
git init -q
cat > .gitignore <<'EOF'
.env
auth.json
state.db*
pairing/
logs/
cache/
sandboxes/
EOF
git add .gitignore config.yaml SOUL.md memories/ skills/ cron/ hooks/ 2>/dev/null
git commit -qm "Baseline of Hermes config, memory and skills"

# Later: see what the agent changed since the baseline
git diff --stat
git diff memories/ skills/

8. Know how to search the record

Work on a copy, opened read-only, so an investigation never writes to the live database.

cp "${HERMES_HOME:-$HOME/.hermes}/state.db" /tmp/hermes-state-copy.db
sqlite3 "file:/tmp/hermes-state-copy.db?mode=ro" <<'SQL'
.headers on
-- Sessions by source and size
SELECT id, source, model, datetime(started_at,'unixepoch') AS started, message_count, tool_call_count
FROM sessions ORDER BY started_at DESC LIMIT 20;
-- Every message whose content or tool calls mention a package install
SELECT m.session_id, m.role, m.tool_name, datetime(m.timestamp,'unixepoch') AS at
FROM messages m JOIN messages_fts f ON f.rowid = m.id
WHERE messages_fts MATCH '"pip install" OR "npm install"'
ORDER BY m.timestamp DESC LIMIT 50;
SQL

9. Restart and verify

hermes gateway start
hermes doctor

Send a message from an account that is not on the allowlist and confirm it gets a pairing code, a decline or silence, according to your setting. Then ask the agent to run a command that matches a dangerous pattern and confirm the approval prompt appears.

Common mistakes

  • Running the gateway on the local backend. A chat message becomes a command on your host as your user.
  • Treating "smart" approvals as a policy. It is a model's judgment, fine for one trusted user, not a control for shared setups.
  • Forwarding broad credentials into the container. Anything in docker_forward_env is readable by everything the agent runs.
  • Assuming one container per command. It is one per process, shared with subagents.
  • Approving pairing codes casually. Pairing is permanent until revoked, and a paired user gets the platform's toolset.
  • Installing MCP servers without reading the manifest.
  • Leaving MCP sampling on for servers you do not control.
  • Using a smaller model for compression summaries. If its context window is smaller than the main model's, the middle of the conversation can be dropped without a summary.
  • Ignoring ~/.hermes/hooks/. Anything placed there runs in the gateway at the next start without a prompt.
  • Trusting one page's numbers. Counts and defaults differ between Hermes pages; read them from your install.

FAQ

What is the architecture of Hermes Agent?

Six entry points (CLI, gateway, ACP, batch runner, API server, Python library) create the same AIAgent loop, which builds a cached system prompt, resolves a provider into one of three API modes, and dispatches tool calls through a central registry. Terminal commands run locally, over SSH, in Docker, in Singularity/Apptainer or in hosted cloud sandboxes. Sessions live in SQLite with full-text search.

Is Hermes Agent safe to run?

It can be, if configured for your exposure. The defaults (local backend, smart approvals) suit one trusted user. For anything others can message, the Hermes docs recommend a container backend, explicit allowlists, DM pairing, resource limits, a non-root gateway and regular updates. Two CVE records exist for it so far.

Which terminal backend should I use for Hermes Agent?

local for personal development only; docker for a production gateway; ssh to run commands on a separate server; Singularity/Apptainer on HPC clusters; a hosted cloud sandbox to run off your network. Container backends skip the dangerous command check, so configure them carefully.

How does Hermes Agent connect to MCP servers?

List servers under mcp_servers in config.yaml as a command (stdio) or a URL (HTTP or SSE). Hermes registers each tool as mcp_<server>_<tool>, filters with include and exclude, and gives stdio servers a minimal environment plus their own env block.

Does Hermes Agent support MCP sampling?

Yes, on by default per server with rate, token, timeout and tool-round limits. Disable it per server with sampling.enabled: false.

What is the difference between Hermes Agent and the Hermes models?

Hermes Agent is the agent software described here. The Hermes models are Nous Research's open-weight language models. The agent is provider-agnostic and does not require a Hermes model.

Where does Hermes Agent store its data?

In the Hermes home (~/.hermes, %LOCALAPPDATA%/hermes on Windows, or HERMES_HOME): config, secrets, memory, skills, state.db with full transcripts, cron jobs, pairing approvals, hooks and logs.

Can people on Telegram or Discord run shell commands through Hermes Agent?

Authorized users can, by default, because most messaging presets include the terminal tool. Unauthorized users are denied. Narrow tools with hermes tools or agent.disabled_toolsets.

What happens to my instructions when Hermes compresses the context?

Middle turns are summarized, old tool outputs cleared and loaded skills marked for reload; the system prompt and recent turns stay. If the summary call fails, the middle is dropped. Keep standing rules in context files, skills or config.

Where Safeguard fits, honestly

Hermes Agent is not a Safeguard product, integration or partner, and nothing in this post implies that Safeguard monitors or governs a Hermes install. The connection is indirect. An agent with a terminal, a file tool and a package manager produces ordinary software supply chain artifacts: dependency changes in the projects it edits, scripts and skills it writes, container images it builds in its sandbox, and a Python environment of its own that receives vulnerability advisories like any other. Those artifacts need the same inventory, provenance, policy gates and patching as code a person wrote, and that is the part Safeguard does for any codebase: SBOM generation and analysis in SPDX and CycloneDX, attestation and signing, policies and gates, and autonomous remediation through fix pull requests, which ships switched off. Safeguard's MCP server, with hundreds of MCP tools, is also one of the tool servers an MCP-capable assistant can use; for Claude or ChatGPT, install it from the assistant's connector directory.

An agent like Hermes installs packages, writes scripts and builds containers that end up in your repositories and images, so the dependency hygiene of agent-written code is your dependency hygiene. Hermes's own Python environment, plugins, MCP servers and skill scripts are open source components that receive CVEs too, and the fastest way to close that exposure is the same as for any other dependency.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

None of these demos involve Hermes Agent. They show how Safeguard handles the code and dependencies that any agent, or any person, leaves in a repository.

The first demo is a real Safeguard CLI session: asked to fix a lodash CVE, the assistant reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, and pushes nothing. It is the same approve-each-command pattern this post recommends for any agent with a terminal, applied to a dependency an agent may have added to your project.

Click through it at your own pace, or open it full screen.

The second demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. Choosing a narrow tool set per connection is the same idea as Hermes's per-server include list: an assistant should see only the tools its job needs.

Click through it at your own pace, or open it full screen.

The third demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. On a workstation where an agent writes code, that lets you scan the repositories and images it touched without sending them anywhere first.

Click through it at your own pace, or open it full screen.

To try the CLI, download Safeguard and follow the CLI documentation.

Quick check

Your Hermes gateway uses the Docker backend. Why do dangerous commands inside the container no longer trigger an approval prompt, and what does that mean for your configuration?

The Hermes security docs treat the container as the security boundary, so the dangerous command check is skipped for container and cloud sandbox backends. That makes the container's configuration the control: keep docker_forward_env empty or minimal, do not mount the Docker socket or your home directory, set resource limits, and limit the container's network access. The hardline blocklist and approvals.deny still apply.

The agent writes a new fact into MEMORY.md halfway through a session. When does that fact start influencing its behavior?

At the next session, or after a prompt rebuild such as compression. Memory is injected as a frozen snapshot when the system prompt is built, and mid-session writes only change the file on disk. The same rule means a bad entry keeps affecting every future session until someone removes it, which is why tracking memory and skills in git is worth doing.

Sources

Last reviewed: 23 October 2026, against Hermes Agent v0.22.5. Hermes ships several releases a week; check the linked docs for current defaults, key names and counts before you change a deployment.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.