Tamper-evident memory audit log for Hermes Agent. Hash-chained, HMAC-sealed. Plain plugin observer that forwards every memory mutation to the psy-core audit chain.
Project description
psy-core-hermes
Tamper-evident memory audit log for your Hermes agent. Every memory write — to MEMORY.md, USER.md, or skills — hash-chained and HMAC-sealed. One
pip install.
psy-core-hermes is the Hermes Agent companion to psy-core. It registers as a plain Hermes plugin, observes every memory mutation, and forwards each one to a long-lived psy ingest subprocess that writes the canonical hash-chained, HMAC-sealed audit log to ~/.psy/audit.db.
Install
pip install psy-core-hermes
psy-core-hermes init --actor-id you@example.com
This adds a plugins.psy block to ~/.hermes/config.yaml. Then run Hermes as usual; the plugin loads via Hermes's hermes_agent.plugins entry-point group.
If psy is not on your PATH, the plugin falls back to npx -y psy-core@<exact-version> psy ingest. Most modern dev machines already have Node.js, so this Just Works; if you want to skip the npx round-trip, install psy-core globally with npm i -g psy-core.
What gets captured
Every memory mutation Hermes exposes, mapped to psy-core's canonical operation vocabulary.
| psy operation | Hermes source | Captured via |
|---|---|---|
create |
memory tool with action: "add" |
pre_tool_call (intent) + filesystem watcher (result) |
str_replace |
memory tool with action: "replace" |
pre_tool_call + filesystem watcher |
delete |
memory tool with action: "remove" |
pre_tool_call + filesystem watcher |
create |
skill_manage with action: "create" / "write_file" |
pre_tool_call + post_tool_call |
str_replace |
skill_manage with action: "edit" / "patch" |
pre_tool_call + post_tool_call |
delete |
skill_manage with action: "delete" / "remove_file" |
pre_tool_call + post_tool_call |
Every mutation produces a paired intent + result row, the same shape as every other psy-core adapter.
Hermes memory surface — what's captured and what's not
Hermes has more than one kind of memory. v0.4 deliberately covers the file-backed memory tool plus skill_manage, and explicitly stays out of the way of everything else. The boundary is pinned by tests at tests/test_real_hermes.py.
| # | Surface | Hookable? | v0.4 |
|---|---|---|---|
| 1 | memory tool — add/replace/remove × {memory,user} writing MEMORY.md / USER.md |
✅ via pre_tool_call + filesystem watcher |
Captured |
| 2 | skill_manage tool — SKILL.md + sub-files |
✅ via pre_tool_call + post_tool_call |
Captured |
| 3 | MemoryProvider plugins — Honcho, Mem0, Hindsight, Byterover, Holographic, OpenViking, RetainDB, Supermemory; each exposes its own write tools (honcho_conclude, mem0_conclude, hindsight_retain, fact_store, viking_remember, retaindb_remember/ingest_file, supermemory_store, brv_curate, …) |
✅ via pre_tool_call (verified at run_agent.py:9051's _invoke_tool block — the hook fires before memory_manager.handle_tool_call) |
Not captured in v0.4. Write-tool capture is the single largest v0.5 candidate; turn-on is one allowlist edit. |
| 4 | MemoryProvider lifecycle hooks (sync_turn, on_turn_start, on_session_end, on_pre_compress, on_memory_write, on_delegation) |
Subclass-only — MemoryManager.add_provider is single-select, so subclassing locks the user out of running Honcho/Mem0/Hindsight alongside psy |
Out of scope (architectural — would require psy-core-hermes to BE the user's MemoryProvider, which the plan explicitly rejected) |
| 5 | session_search (read-only SessionDB query) |
✅ via pre_tool_call (in _AGENT_LOOP_TOOLS, no post) |
Not captured (read-only) |
| 6 | todo tool |
✅ via pre_tool_call (in _AGENT_LOOP_TOOLS) |
Not captured (not memory) |
| 7 | SessionDB writes (cross-session summaries) | ❌ no upstream hook | Out of scope (would need an upstream PR) |
| 8 | Trajectory JSONL writes | ❌ no upstream hook | Out of scope (would need an upstream PR) |
| 9 | flush_memories() auxiliary writes |
❌ no upstream hook | Out of scope (would need an upstream PR) |
| 10 | Gateway transport events | Separate gateway/hooks.py registry |
Separate adapter scope |
Note on #3 ↔ #1: at run_agent.py:9098, when the file-backed memory tool runs, Hermes also calls memory_manager.on_memory_write(...) so any active external MemoryProvider can mirror the write semantically. That makes psy-core-hermes (audit) and Honcho/Mem0 (semantic recall) complementary observers of the same write, not competing writers — they're additive.
If you need Mem0/Letta/LangChain memory audited at the API level (rather than via Hermes's tool dispatch), psy-core ships dedicated adapters for those frameworks; see the adapter table in the root README.
Identity
actor_id is required unless allow_anonymous: true. Audit events must attribute the session to a principal. When actor_id is missing the plugin emits the F4 error template at session start and refuses to register hooks:
psy-core-hermes: actor_id is required.
Why: audit events must attribute the session to a principal.
Where: ~/.hermes/config.yaml -> plugins.psy.actor_id
Example:
plugins:
psy:
actor_id: alice@acme.com
Bypass: set allow_anonymous: true (not recommended in production).
Docs: https://github.com/jethros-projects/psy-core/blob/main/python/psy-core-hermes/README.md#identity
Configuration
# ~/.hermes/config.yaml
plugins:
enabled:
- psy
psy:
enabled: true
actor_id: alice@acme.com # REQUIRED unless allow_anonymous: true
tenant_id: acme # optional
purpose: production-debug # optional
db_path: ~/.psy/audit.db # optional; default <HERMES_HOME>/psy/audit.db
seal_key_path: ~/.psy/seal-key # optional
memories_dir: ~/.hermes/memories # filesystem-watched dir
psy_core_version: 0.4.0 # exact pin (used for npx fallback)
psy_binary: null # optional override
redactor: default # default | none | "<dotted_path>"
payload_capture: true # capture memory content (with redaction)
dry_run: false
log_level: info
allow_anonymous: false
schema_version_pin: "1.0.0"
Console scripts
psy-core-hermes init [--actor-id NAME] # idempotent config block insertion
psy-core-hermes doctor # config + paths + subprocess handshake test
psy-core-hermes status # one-line summary
psy-core-hermes dry-run < envelopes.jsonl # emit envelopes locally; never spawn ingest
psy-core-hermes skill-stats # skill-quality report from the audit chain
Skill-quality reporting (skill-stats)
The audit chain isn't just a write log — its tamper-evident ordering makes it a uniquely good signal for outcome attribution on skills. psy-core-hermes skill-stats reads the chain and reports per-skill churn, rapid-patch counts, and false-start patterns:
$ psy-core-hermes skill-stats
SKILL CREATE PATCH DEL CHURN RAPID STATUS
deploy-runbook 1 7 0 7.00 5 unstable
flaky-test-recovery 1 4 0 4.00 4 unstable
release-checklist 1 0 0 0.00 0 ok
legend: unstable = churn>=2.0 or 3+ rapid patches | short-lived = create+delete within 1 day
Why this matters: Hermes's curator at agent/curator.py:283-285 explicitly tells the model "DO NOT use usage counters as a reason to skip consolidation … 'use=0' is not evidence." The team knows usage counters are insufficient. The audit chain provides the complementary signal — a skill patched 5 times within an hour of creation is probably unstable, no matter how often it's been called. That's a fact the chain can prove via its hash-linked ordering and timestamps.
Useful flags:
psy-core-hermes skill-stats --json # machine-readable
psy-core-hermes skill-stats --since 7d # last week only
psy-core-hermes skill-stats --actor alice@acme # multi-tenant filter
psy-core-hermes skill-stats --top 10 # most-suspect 10 only
psy-core-hermes skill-stats --skill-md-only # ignore attached files
psy-core-hermes skill-stats --db-path ~/.psy/audit.db # explicit DB path
The metrics are also available as a Python library function for users who want to feed the signal into their own tools (Hermes curator integration, dashboards, Atropos quality filters):
from datetime import timedelta
from pathlib import Path
from psy_core.hermes.skill_stats import compute_skill_stats
metrics = compute_skill_stats(
Path.home() / ".psy" / "audit.db",
actor_id="alice@acme.com",
since=timedelta(days=7),
)
unstable = [m for m in metrics if m.status == "unstable"]
The DB handle is opened read-only (SQLite mode=ro URI), so the read path provably cannot mutate the chain.
Architecture
┌─────────────────────────────────────────┐ ┌─────────────────────┐
│ Hermes process (Python) │ │ psy ingest (Node) │
│ │ │ │
│ psy_core.hermes.register(ctx) │ │ Auditor.append() │
│ ├─ register_hook(pre_tool_call, │ │ Sealer.writeHead() │
│ │ filter: tool_name in │ JSONL │ ↓ │
│ │ {"memory","skill_manage"}) │ ──────▶│ ~/.psy/audit.db │
│ └─ register_hook(post_tool_call, │ stdio │ ~/.psy/head.json │
│ filter: tool_name=="skill_manage")│ ◀─ACK │ ~/.psy/seal-key │
│ │ │ │
│ Filesystem watcher (watchdog) │ └─────────────────────┘
│ ├─ ~/.hermes/memories/MEMORY.md │
│ ├─ ~/.hermes/memories/USER.md │ Spawn flow:
│ └─ on change: emit confirmed-result │ 1. shutil.which("psy") → use directly
│ │ 2. else npx -y psy-core@X psy ingest
│ IngestClient (thread-safe queue) │ 3. else error: install psy-core or Node
│ ├─ enqueue(envelope) │
│ ├─ background writer thread │
│ └─ atexit + SIGTERM cleanup │
└─────────────────────────────────────────┘
Distribution
- Primary: PyPI as
psy-core-hermes. Auto-discovered viahermes_agent.pluginsentry-point. - Secondary:
hermes plugins install jethros-projects/psy-core-hermes(Hermes's own installer; runsgit clone --depth 1against the in-repo path).
Verification
psy verify --all # full chain integrity check + sealed-tail verify
The TS-side psy verify reads ~/.psy/audit.db, walks the hash chain, validates the HMAC seal, and exits non-zero on any tampering.
Verified against hermes-agent v0.11.0
The plugin contract was source-verified against NousResearch/hermes-agent at v0.11.0:
- Entry-point group is
hermes_agent.plugins; the loader doesep.load()and thengetattr(module, "register"), so our entry-point value is the module pathpsy_core.hermes.register(not themodule:attrform — that returns the function fromep.load()and breaks thegetattr(module, "register")lookup). _AGENT_LOOP_TOOLS = {"todo", "memory", "session_search", "delegate_task"}— confirmedmemorybypassespost_tool_call; we use the filesystem watcher to confirm result envelopes for memory writes.memorytool args:{action, target, content, old_text}wheretarget ∈ {"memory", "user"}maps toMEMORY.md/USER.md.skill_manageargs:{action, name, content?, old_string?, new_string?, file_path?, file_content?, ...}. Skill key isname;file_pathis the optional sub-path under the skill directory.- Hook callback signature: keyword-only
(*, tool_name, args, task_id, session_id, tool_call_id, **_). hermes_cli.config.load_config()returns the parsed YAML as a dict.
A live integration test that loads our plugin into a real Hermes
PluginManager and asserts captured envelopes is part of the e2e
workflow at .github/workflows/cross-lang-e2e.yml.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file psy_core_hermes-0.1.0.tar.gz.
File metadata
- Download URL: psy_core_hermes-0.1.0.tar.gz
- Upload date:
- Size: 53.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dbd80f40f4c052a3c880fce4d1adf1f604a425e30593b4efd1d8f09c2bdb97f8
|
|
| MD5 |
00c34a727ba6e6eef807d7e159763464
|
|
| BLAKE2b-256 |
d8deb27fa70b743b5d2bc23481b7b185169722cbefbc7311848b1c5c04febc03
|
Provenance
The following attestation bundles were made for psy_core_hermes-0.1.0.tar.gz:
Publisher:
publish-pypi.yml on jethros-projects/psy-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
psy_core_hermes-0.1.0.tar.gz -
Subject digest:
dbd80f40f4c052a3c880fce4d1adf1f604a425e30593b4efd1d8f09c2bdb97f8 - Sigstore transparency entry: 1416120817
- Sigstore integration time:
-
Permalink:
jethros-projects/psy-core@825486ec7d4e00f2177ae1693687a3f77a81b8d7 -
Branch / Tag:
refs/tags/psy-core-hermes-v0.1.0 - Owner: https://github.com/jethros-projects
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@825486ec7d4e00f2177ae1693687a3f77a81b8d7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file psy_core_hermes-0.1.0-py3-none-any.whl.
File metadata
- Download URL: psy_core_hermes-0.1.0-py3-none-any.whl
- Upload date:
- Size: 36.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a40c3e3b78e567f8513b45ce0aadcb00e89877c09e816126e54e47196ea1bbca
|
|
| MD5 |
b2e5d80c7d2f6ad25397606c1fea0256
|
|
| BLAKE2b-256 |
f1953376f425e76d360025db2b8dc84d7485727eb18e48245b6309f55736b1e0
|
Provenance
The following attestation bundles were made for psy_core_hermes-0.1.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on jethros-projects/psy-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
psy_core_hermes-0.1.0-py3-none-any.whl -
Subject digest:
a40c3e3b78e567f8513b45ce0aadcb00e89877c09e816126e54e47196ea1bbca - Sigstore transparency entry: 1416120940
- Sigstore integration time:
-
Permalink:
jethros-projects/psy-core@825486ec7d4e00f2177ae1693687a3f77a81b8d7 -
Branch / Tag:
refs/tags/psy-core-hermes-v0.1.0 - Owner: https://github.com/jethros-projects
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@825486ec7d4e00f2177ae1693687a3f77a81b8d7 -
Trigger Event:
push
-
Statement type: