It said the job was created. The job never existed.


You ask the agent to set up a job that runs every morning. It answers: created, starts at 8am tomorrow. At 8am the next day, nothing happens. You open jobs.json — no such job. You check the execution records — empty. The log looks like nothing ever happened. It did not lie to you: it did call the tool. The result simply never landed, because context compaction replaced that whole turn with a summary, and with it went both “the call was issued” and “the job exists.”

What makes this class of failure nasty is not the lost operation. It is that nobody knows it was lost. The agent believes it never did the thing, you believe it never did the thing, and so nobody goes back to fix it. The community plugin agent-ledger, which entered the official plugin catalog this week, watches exactly that gap — and along the way exposes a more general question: what has your agent actually done that changed something?

What a ghost call is

Some background first. When a session approaches its context threshold, Hermes compacts: older turns are replaced with a summary so the conversation can keep going. That mechanism is necessary, but it has one edge case. When compaction fires in the middle of a tool call that has been issued but whose result is not yet committed, the turn is replaced wholesale. The consequences:

  • the operation never completed;
  • no error, no retry, no log line;
  • the session state says the tool was never called.

Upstream issue #90985 documents a real case: a cronjob_create was issued at 20:39:07Z and the job existed nowhere — not in jobs.json, not in executions.db, not in cron/output/. The plausible path in the issue: a turn issues several tool calls, their output pushes the context past the compaction threshold, the preflight check fires, history is rewritten to a summary, and the call result evaporates with it. No trace that anything was attempted.

Until the core closes that window (and arguably after), a ledger that knows what it might have lost earns its keep. That is agent-ledger’s premise.

How it detects them: a write-ahead ledger

The plugin attaches to four hooks and records every state-changing tool call in a local ledger:

Hook What it does
pre_tool_call writes a PENDING entry (tool, args, timestamp, session)
post_tool_call flips the entry to CONFIRMED, or records ERROR:<type>
pre_llm_call reconciles: a PENDING older than its grace period is a ghost
on_session_start aligns the books at session start

Only tools that change state are tracked: write_file, patch, terminal, cronjob, delegate_task, kanban mutations, and every MCP tool (recognised by the mcp__ prefix). Read-only tools cost nothing.

Grace periods are per tool rather than one global number — two minutes by default, but 30 minutes for delegate_task and 15 for terminal, so long-running calls are not mislabelled. A call wrongly reported as a ghost that later confirms is revived instead of duplicating its row. Entries are SQLite inserts in WAL mode, sub-10 ms according to the author, and the ledger never blocks and never rewrites arguments — it is a flight recorder, not a gate.

To look at it yourself:

sqlite3 ~/.hermes/plugin-data/agent-ledger/ledger.db "
  SELECT ts, tool_name, status, substr(args_json,1,60)
  FROM ledger ORDER BY id DESC LIMIT 20;"

Four statuses exist: PENDING, CONFIRMED, ERROR:<type>, and GHOST_REPORTED (already surfaced to the agent).

Why it tells the agent next turn instead of stopping the call

This is the design decision worth stealing.

By the time a ghost is detected, that turn is over — you cannot go back and redo it. So the plugin pushes the fact into the next turn instead: the pre_llm_call hook can return a context value, which is injected into the current turn’s user message rather than into the system prompt. The system prompt is cache-sensitive; touching it invalidates the cache. Put the warning in the user message, and the agent learns inside the conversation where it can still act: a call was issued and never confirmed — verify the state before trusting it.

The second decision is equally restrained: why not refuse suspicious calls in pre_tool_call? Because Hermes fails that hook closed after a 30-second timeout — a slow ledger would end up blocking legitimate tools. A flight recorder should not have that power. Blocking is a separate, opt-in feature discussed below.

Getting it running

Install from the official catalog (the entry landed this week):

hermes plugins install agent-ledger
hermes plugins enable agent-ledger

To run the maintainer’s repository version instead, the README clones it into ~/.hermes/plugins/agent-ledger/, enables it, and restarts the gateway so the plugin loads on the next session.

Then run the self-test to check the hook signatures against your version:

python3 selftest.py

It verifies issue-and-confirm pairing, fallback confirmation by tool name, ghost detection, context injection, and that only mutating tools are recorded.

The bonus: an OWASP posture score

The plugin also ships a script that scores the recorded activity against the OWASP Top 10 for Agentic Applications 2026:

python3 posture.py                        # score the live ledger
python3 -m posture ~/.hermes/plugin-data/agent-ledger/ledger.db

From ledger metadata alone it can cover tool misuse (ASI02), unexpected code execution (ASI05), cascading failures (ASI08) and rogue agents (ASI10), plus a community extension for excessive agency labelled ASI11 — the official list stops at ASI10, and the author says so everywhere it appears. The scoring is auditable: score = 100 − Σ severity_weight(finding), with CRITICAL 25 / HIGH 15 / MEDIUM 8 / LOW 3, findings never deduplicated, and the full finding list always returned so you can recompute it by hand.

Its trust model is stated plainly, and it is the sentence to remember: the ledger is written by the agent being scored. An observer cannot score an agent that does not log, so a score of 100 means “nothing suspicious in the recorded activity” — not “this agent behaved well.” Treat it as a hygiene signal, not proof.

Policy enforcement: start with a dry run

Observation is what the core does; enforcement is the optional layer this plugin adds. Install a policy pack and watch for a few days what would have been blocked:

cp policy.example.json ~/.hermes/plugin-data/agent-ledger/policy.json
echo dry_run > ~/.hermes/plugin-data/agent-ledger/policy_mode   # log only, block nothing

Once the report shows you things you genuinely want stopped, switch to enforce: matching calls are refused before they run, with the reason handed back to the agent.

The author’s honesty about coverage deserves to be passed on: matching is signature-based over the extracted command, so treat it as a seatbelt, not a wall. At least three bypasses are known — composing the destructive string with sh -c (the literal never appears in the command), indirection through environment variables (rm -rf $TARGET), and splitting one dangerous action into two individually harmless calls. What it does catch reliably is the accidental-disaster class: recursive deletes, pipe-to-shell, credential reads. That is exactly what the dry-run report is for.

Where the data lives, and how to treat it

  • The ledger is a local plaintext SQLite database at <hermes_home>/plugin-data/agent-ledger/ledger.db. It stores tool arguments up to 4000 characters (terminal commands, file contents, MCP arguments) plus a 300-character result digest. Anyone with filesystem access can read it — treat it like your shell history.
  • Ghost warnings are session-scoped: a session only ever sees its own unconfirmed calls.
  • The ledger is never pruned automatically. Set your own cap if you want one, for example keeping the newest 50,000 rows:
sqlite3 ~/.hermes/plugin-data/agent-ledger/ledger.db \
  "DELETE FROM ledger WHERE id NOT IN (SELECT id FROM ledger ORDER BY id DESC LIMIT 50000)"
  • For a daily posture report, posture_daily.py is scheduled by you, not by the plugin, and it uses its own bot credentials (POSTURE_TELEGRAM_BOT_TOKEN / POSTURE_TELEGRAM_CHAT_ID). It never reads Hermes’s .env and never reuses your agent’s bot token.

How it relates to what Hermes already does

Hermes has its own protections around scheduled work: which process claimed a job, what state it is stuck in, how long it has been quiet — we covered the mechanics in mid-turn compaction integrity and in the cron reference. agent-ledger approaches it from the other side: it does not care about a job’s state, it cares whether a state-changing action was issued without confirmation, and it covers far more than cron — every mutating tool. The two do not conflict, and installing it changes nothing about Hermes’s behaviour unless you explicitly turn enforcement on.

The short version

  • “The operation did not happen” and “the operation happened but nobody knows” are different failures, and the second is often more expensive because nothing surfaces it.
  • A PENDING → CONFIRMED write-ahead ledger, per-tool grace windows, and session-scoped context injection turn a ghost call into one line of advice: verify before trusting.
  • The posture score is a useful auditing lens, but the ledger is written by the party being measured — read it as hygiene, not a verdict.
  • Enforcement is optional and signature-based, with the bypasses documented by the author. Run it in dry-run mode first and see what it would actually catch.