Add Memory to Hermes¶
Hermes is the only harness here whose hooks are Python. The plugin runs
inside the gateway process, so the read path is a single round trip to the
memory store (Redis, or Postgres with POPOTO_BACKEND=postgres; see
On Postgres) with no
interpreter startup at all -- the fastest of the four.
Two hook systems, not one
Hermes 0.19.0 ships two independent mechanisms: gateway hooks
(~/.hermes/hooks/, shell-invoked) and plugins
(~/.hermes/plugins/, Python, loaded in-process). This integration is a
plugin. If you set up an earlier version of this guide that installed
into ~/.hermes/hooks/popoto-memory, remove it first:
rm -rf ~/.hermes/hooks/popoto-memory.
Pre-release
popoto[mcp] is not on PyPI yet. Until a release ships, install from a
checkout: pip install -e '.[mcp]'.
Install¶
pip install 'popoto[mcp]'
popoto-memory doctor
mkdir -p ~/.hermes/plugins/popoto-memory
cp plugins/hermes/plugin.yaml plugins/hermes/__init__.py ~/.hermes/plugins/popoto-memory/
hermes plugins enable popoto-memory
hermes mcp add popoto-memory --command popoto-memory --args mcp
Two files, no config file to edit -- but the enable step is not optional.
Plugins are opt-in via plugins.enabled in ~/.hermes/config.yaml
(read by _get_enabled_plugins(), hermes_cli/plugins.py:243-270); a plugin
present on disk but absent from that list is recorded as enabled=False
and never loaded, with no startup warning. Confirm with:
plugin.yaml declares the plugin's identity and the events it registers
for (pre_llm_call, post_llm_call); __init__.py builds one
MemoryService at first use and keeps it for the life of the gateway, so
there is no per-turn setup cost.
Why in-process matters here¶
On Claude Code and Codex the read hook is a subprocess: p95 200 ms,
essentially all of it Python startup. On Hermes none of that applies. The
same assembly measures 1-2 ms in-process, which is what popoto-memory
doctor reports as hook read.
The tradeoff is that a bug in the plugin is a bug in the gateway process.
It is written accordingly: every callback catches everything, returns
None on any failure, and lets the reason land in ~/.popoto/memory.log
and in popoto-memory doctor. Hermes also swallows callback exceptions a
second time at the invoke site, so nothing a broken plugin does can break a
user's turn -- and nothing it does surfaces there either. See
Troubleshooting below.
Injection lands in the user message¶
Hermes places pre_llm_call context in the user message rather than the
system prompt, confirmed against the installed 0.19.0 source: pre_llm_call
results are joined into plugin_user_context (agent/turn_context.py:708-741)
and appended by compose_user_api_content (agent/turn_context.py:44-73),
whose docstring records that the injections reach "the API copy of the user
message only -- the stored content stays clean". That is the same placement
Claude Code's additionalContext gets, and it is why per-turn injection does
not invalidate a cached prefix.
MemoryService returns a context string and never touches a message array,
so this stays the harness's decision.
Environment¶
POPOTO_MEMORY_AGENT_ID-- set this explicitly. Plugin hook payloads carry no working directory at all (only the separate shell-hook subsystem serializes one), so without it popoto falls back to the Hermes gateway process's ownos.getcwd()-- stable for the life of the gateway, but an arbitrary scope for a long-lived process serving multiple projects.POPOTO_MEMORY_MAX_TOKENS-- Hermes caps per-hook injected context at roughly 10,000 characters (~2,400 tokens) and spills any overflow to$HERMES_HOME/hook_outputs/<session_id>/<uuid>.txt, substituting a head/tail preview plus that file path in the injected text. popoto's default of 800 tokens (~3,200 characters) has comfortable headroom; raising it much past ~2,400 tokens risks injecting a file path instead of memories, silently.
Verify¶
After a few turns, last assemble and last capture carry recent
timestamps and records climbs.
If they stay at never, walk these in order -- Hermes swallows every
plugin error, so no single place shows all of it:
hermes plugins list-- the only place a load failure surfaces (usually: the enable step above was skipped).~/.hermes/logs/agent.log-- the only place a callback exception surfaces.popoto-memory doctor/~/.popoto/memory.log-- popoto's own failures (a misconfiguredPOPOTO_MEMORY_URLorPOPOTO_POSTGRES_URL, an unreachable server).
You can also test the adapter directly, bypassing Hermes entirely:
echo '{"hook_event_name":"pre_llm_call","session_id":"t","user_message":"how do deploys work?"}' \
| popoto-memory hook
If that returns {"context": "..."} but Hermes still shows nothing, the
problem is plugin registration or the payload field names, not the memory
layer.
MCP tools¶
hermes mcp add popoto-memory --command popoto-memory --args mcp registers
memory_search, memory_save, memory_feedback, and memory_status for
deliberate use. Recall and capture do not depend on them.
Verification status¶
Verified against the installed hermes-agent==0.19.0 package (pinned
2026-09-08; re-check by 2027-03): the real
plugin loader (hermes_cli.plugins.PluginManager) loads this plugin from a
scratch HERMES_HOME, both hooks register against the real invoke_hook
dispatcher, and the payload shapes documented above were read verbatim from
the 0.19.0 invoke sites and captured through that dispatcher into
tests/fixtures/harness_payloads/hermes_*.json. No live model turn has
been run -- that needs provider credentials this repo's CI cannot hold.
Configuration¶
Identical to every other harness, and all environment-driven. See Harness Integration for the table. The gateway must have these in its environment, not just your interactive shell, since the plugin runs in the gateway process.
The store can be Redis, Valkey or PostgreSQL 18+. For Postgres, set
POPOTO_BACKEND=postgres and POPOTO_POSTGRES_URL in the gateway's environment; see
On Postgres.