Hermes Plugin¶
Memoir ships a memory-provider plugin for Hermes, the Nous Research personal-assistant agent. Activate it and Hermes gains versioned, semantic long-term memory: a git-like store (branch / commit / merge) over a Prolly-tree with semantic paths and cryptographic provenance. Unlike vector-only backends (Mem0, Zep, Letta), every write is a commit you can inspect (memoir log), attribute (memoir blame), and time-travel.
The plugin lives in the repo at plugins/hermes/.
How it fits Hermes¶
Hermes has a pluggable MemoryProvider slot — exactly one external provider is active at a time, selected by memory.provider in ~/.hermes/config.yaml. The built-in MEMORY.md / USER.md layer stays active alongside it. Memoir fills that slot.
It is a directory plugin, not a pip/entry-point package: Hermes loads memory providers by scanning $HERMES_HOME/plugins/<name>/, so the plugin is installed as a directory there. It holds no memoir code in-process — it shells out to the memoir CLI over a stdlib-only subprocess bridge, so the Prolly-tree self-resolves concurrent (fire-and-forget) writes with no locking.
Install¶
The plugin needs two things on the machine: the plugin directory in your Hermes home, and the memoir CLI reachable on PATH.
1. Install the memoir CLI¶
The bridge resolves
memoironPATHfirst, falling back to a pinneduvx --from memoir-ai==<pin> memoir(thenuv tool run) when it isn't installed. A direct install is faster (no warmup).
2. Install the plugin¶
Via the Hermes CLI (once the plugin is on the repo's default branch):
Hermes clones the repo, takes the plugins/hermes subdirectory, reads its plugin.yaml (name: memoir), and installs it to ~/.hermes/plugins/memoir/.
By copy (works today, from a local checkout):
cp -r plugins/hermes ~/.hermes/plugins/memoir
rm -rf ~/.hermes/plugins/memoir/tests ~/.hermes/plugins/memoir/__pycache__
3. Activate it¶
hermes memory setup writes memory.provider: memoir to ~/.hermes/config.yaml. (You can also set that key by hand.) Activating memoir displaces any other external provider (mem0, honcho, …) — only one runs at a time.
The store is created automatically on first run at <hermes_home>/memoir-store (override with store_path in config). You'll need a provider API key in the Hermes environment — see LLM backend.
What ships¶
| Component | Count | Role |
|---|---|---|
| Tools (model-facing) | 5 | memoir_recall, memoir_remember, memoir_forget, memoir_sync, memoir_status |
| Lifecycle hooks | 5 | Auto-capture, recall context, store mirroring |
| CLI subcommands | 2 | hermes memoir status, hermes memoir ui |
| Slash command | 1 (opt-in) | /memoir … — in-session passthrough to the memoir CLI |
The model-facing tools above are the primary in-chat surface (the model calls
them). There's also an opt-in /memoir slash command for power users —
see The /memoir command.
Tools¶
The model decides when to call these (guided by their descriptions and the injected system-prompt block); they are not typed by the user.
| Tool | Purpose |
|---|---|
memoir_recall |
Fetch stored facts about the user (preferences, people, schedule, standing instructions). LLM-free: summarize → batched get, ranked by lexical overlap with the query. |
memoir_remember |
Store an explicit durable fact. Classified into a semantic path and committed. Guarded against obvious secrets. |
memoir_forget |
Delete a fact by its exact taxonomy path (the model finds the path via memoir_recall first). Pre-checks existence so a wrong path can't create a no-op delete commit; prior versions remain in git history (recoverable). |
memoir_sync |
Promote a session fork's memories into main. No args → list fork branches with un-merged memories (preview counts); branch → merge it into main (dry_run to preview). Additive (insert/update, never delete). |
memoir_status |
Store status: branch, commit count, memory count. |
Lifecycle hooks¶
These are called by Hermes automatically — they don't depend on the model invoking a tool, so capture is guaranteed even if the model never calls memoir_remember.
| Hook | When | Purpose |
|---|---|---|
initialize |
agent startup | Derive + ensure the store, resolve the model, cache an overview. |
system_prompt_block |
prompt assembly | Inject recall guidance + a short store overview. |
sync_turn |
after every turn | Fire-and-forget memoir capture --profile assistant over the turn. |
on_pre_compress / on_session_end |
compression / session end | Capture the uncaptured message tail. |
on_session_switch |
/branch, /resume, /reset |
Route the store's branch to match the session — see Session branching. |
on_memory_write |
built-in memory edit | Mirror Hermes's MEMORY.md / USER.md writes into versioned memoir paths. |
Capture runs on a background thread and never blocks the response. Writes are skipped for non-primary agent contexts (subagent / cron / flush) so they can't corrupt the user's representation.
The /memoir command¶
An optional in-session slash command that passes through to the memoir CLI on your store — handy for inspecting memory without leaving the chat:
/memoir status
/memoir summarize --depth 3
/memoir recall "travel plans"
/memoir branch
/memoir blame profile.personal.identity
/memoir with no args prints usage. It's user-invoked (not the model), so it
simply runs memoir -s <store> <your args> and returns the output.
It's opt-in and separate from the memory provider. Hermes loads in-session
slash commands only for plugins listed in plugins.enabled, which is distinct
from memory.provider. So to get /memoir, enable the plugin as well:
hermes memory setup # activates the provider (tools + auto-capture)
hermes plugins enable memoir # additionally registers the /memoir command
Without the enable step everything else still works — you just won't have the
slash command. (Mechanism: the plugin ships kind: standalone so the general
plugin manager can load it for the command; its register() feature-detects
the load context and registers the provider or the command accordingly.)
Configuration¶
Config lives in <hermes_home>/memoir.json (all keys optional). Set it via hermes memory setup or by hand.
| Key | Default | Meaning |
|---|---|---|
store_path |
<hermes_home>/memoir-store |
Store location. |
capture |
true |
Auto-capture facts from each turn. false disables the sync_turn / boundary capture; recall and tools still work. |
model |
host's selected model | Pin the LLM model for capture/classification. Empty = follow Hermes model.default. See Model selection. |
base_url |
provider default | Custom provider endpoint (LLM gateway/proxy). Empty = call the provider directly. See Routing through a proxy. |
session_branching |
true |
Mirror Hermes session forks onto memoir branches. false keeps everything on main. See Session branching. |
scope |
off |
Isolate memory per chat/profile via a namespace: chat (per platform+chat), profile (per Hermes profile), off (one shared store). See Scoped memory. |
Example — pin cheap Haiku for per-turn capture regardless of your chat model:
Model selection¶
memoir resolves the capture/classification model in this order:
modelpin inmemoir.json— always wins.- Host-selected model — Hermes
model.default, tracked across mid-session model switches (on_turn_start). - memoir's built-in default —
claude-haiku-4-5.
So out of the box, capture runs on the same model your Hermes session uses. That can be expensive: if your session model is Opus, every per-turn extraction is an Opus call. Pin a cheaper model (e.g. claude-haiku-4-5) in memoir.json to decouple capture cost from your chat model.
Which provider key is used?¶
memoir routes by model name, not by which keys are present — so setting both OPENAI_API_KEY and ANTHROPIC_API_KEY is unambiguous:
| Model | Provider → key |
|---|---|
claude-* / anthropic/* |
Anthropic → ANTHROPIC_API_KEY |
gpt-* |
OpenAI → OPENAI_API_KEY |
gemini/* |
Gemini → GEMINI_API_KEY |
ollama/*, or any model with base_url set |
that endpoint (no key check) |
Each key is consulted only when a model of its provider is selected. memoir's default model is claude-haiku-4-5 (Anthropic), so with no model specified anywhere it uses ANTHROPIC_API_KEY.
Session branching¶
Hermes lets you fork a conversation (/branch), jump between conversations
(/resume), and start fresh (/reset / /new). With session_branching
enabled (the default), the plugin mirrors that onto memoir branches so a
fork's memories stay isolated from your main timeline:
/branch(fork) → creates and checks outhermes/<session-id>off the current branch. Everything captured in the fork lands there, not onmain./resume→ checks out that session's branch if it has one, otherwise returns tomain. So resuming a normal (never-forked) conversation always comes back to your main timeline./reset//new→ back tomain.- context compression → no branch change (it's the same conversation continuing under a rolled-over id, so captures stay on the current branch).
The mapping is stateless — it's derived from the session id and the branches
that exist in the store — so it survives Hermes restarts. Forks branch off
whatever is currently checked out, so nested forks work too. Inspect or merge
branches with the memoir CLI (memoir branch, memoir merge,
memoir -s <store> summarize per branch).
Set session_branching: false in memoir.json to disable this and keep all
captures on main.
Promoting a fork's memories back to main. A fork's captures stay isolated
on its branch. When the user wants to keep them, the memoir_sync tool
promotes them into main: with no arguments it lists fork branches that have
un-merged memories (with a preview of what each would add); with a branch it
merges that branch into main. The merge is additive — it inserts/updates
keys and never deletes — and the model drives the choose/confirm flow in
conversation. (This mirrors the Claude Code plugin's /memoir:sync.) On exit
the store is left on main, so forks don't accumulate as the active branch.
Single-store caveat: branch state is the store's checked-out branch, so this assumes one active session per store at a time (the personal-assistant CLI case). Concurrent sessions sharing one store would contend on the checkout.
Scoped memory¶
If you run Hermes across multiple chats/platforms/profiles (e.g. a private DM
and a public group on the same gateway), global memory is a privacy foot-gun:
a fact captured in the DM would otherwise be injected into the group. Set
scope in memoir.json to isolate memory structurally:
scope |
Each scope is… | Namespace |
|---|---|---|
off (default) |
one shared store | default |
chat |
a platform + chat/channel | scope_<platform>_<chat_id> |
profile |
a Hermes profile | scope_profile_<name> |
How it works:
- Hermes already passes
platform,chat_id, andagent_identity(profile) into the provider'sinitialize(), so the scope key needs no extra config. - Captures and
memoir_rememberwrite to the scope's namespace. memoir_recallreads the scope namespace ⊕default— so global (unscoped) facts are visible everywhere, but a scoped fact never appears in another scope. Cross-scope leakage is impossible by construction (default-deny), not dependent on an injection-time filter.memoir_forgetdeletes from the scope namespace, falling back todefault.
Namespace vs. branch. Scoping uses memoir namespaces (parallel,
composable partitions), which is the right primitive here — distinct from
session branching, which uses branches (divergent
timelines you can merge). They compose: a scoped session can still /fork.
Namespaces are also routed per call (no shared checkout), so concurrent
scoped sessions don't contend.
v1 resolves the scope once at
initialize()(the session's chat/profile). Per-message re-scoping for a single provider serving many concurrent gateway sessions is future work — the namespace primitive supports it.
LLM backend¶
The plugin drives memoir with its litellm (direct provider API) backend only — it never shells out to the claude CLI, even when a claude binary is on PATH. A Hermes host has its own configured provider, and a typical Hermes box has no Claude Code install.
memoir reads the provider credentials from the Hermes process environment (e.g. ANTHROPIC_API_KEY in <hermes_home>/.env). If the selected model's provider key is missing, capture fails loudly ("ANTHROPIC_API_KEY required") rather than silently switching backends.
Recall is LLM-free, so it works even without a provider key. Auto-capture and explicit memoir_remember classification do need one.
Routing through a proxy¶
By default memoir calls the provider directly (e.g. api.anthropic.com) and does not route through any gateway Hermes itself uses — Hermes keeps its gateway/base-URL internal to its own client and never exports it to the environment. To send memoir's calls through a proxy/gateway too, set base_url in memoir.json:
The bridge exports this as MEMOIR_LLM_BASE_URL; the credential in the environment must be valid for that endpoint.
Verify¶
hermes memory status # provider: memoir
hermes memoir status # store branch / commit count / memory count
memoir capture --help # confirms the CLI on PATH has `capture`
End-to-end: start a Hermes session and state a durable fact ("from now on call me Captain; my dog Rex sees the vet every March"). Capture runs in the background (a few seconds), then:
memoir -s <hermes_home>/memoir-store summarize --depth 3 # captured paths
memoir -s <hermes_home>/memoir-store blame <path> # provenance
In a new session, ask "what's my name?" — the agent should call memoir_recall and answer from memory.
Environment variables¶
The bridge sets these for the memoir subprocess automatically from memoir.json; you normally don't set them by hand.
| Variable | Set from | Effect |
|---|---|---|
MEMOIR_LLM_BACKEND |
always litellm |
Forces direct provider APIs; suppresses the claude-cli fallback. |
MEMOIR_LLM_MODEL |
resolved model | Model for capture/classification. |
MEMOIR_LLM_BASE_URL |
base_url config |
Custom provider endpoint, when set. |
Provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, …) are inherited from the Hermes process environment — set them in <hermes_home>/.env.
Manifest¶
plugins/hermes/plugin.yaml:
name: memoir
version: 0.1.0
description: "Memoir — versioned, semantic long-term memory with git-like branch/commit/merge and cryptographic provenance. Shells out to the memoir CLI; no Python deps."
No pip_dependencies: the provider talks to the memoir CLI over a subprocess bridge (PATH memoir, else a pinned uvx --from memoir-ai).
Limitations (v1)¶
- Local store only. Multi-device sync (the merge-based wedge) is future work; it needs a
memoir remotestory. - One external provider at a time. Activating memoir displaces any other Hermes memory provider.
See also¶
- CLI — the underlying
memoircommands the plugin wraps (includingcapture). - Claude Code — the coding-agent plugin (slash commands, hooks).
- Architecture — how memoir is structured under the hood.