All plugins

read-once

On a repeated identical local read_file, tells the model to answer from the earlier result when READ_ONCE_HITS=1, the file bytes are unchanged, and that exact result is still in the latest model request. Observe-only otherwise. /read-once stats shows hits. Disclosure — reads and SHA-256-hashes regular files under READ_ONCE_ROOT (1 MiB max), inspects the model request in memory for that result digest, and keeps ephemeral metadata only. No network, no telemetry, no disk cache, and no background watcher. It does not hash dotenv names, auth.json, or paths under vault, mcp-tokens, browser-profile, webapp, or pairing. Hits stay off unless READ_ONCE_HITS=1. A repeat of the same read can be Hermes's unchanged-stub rather than the file bytes.

batmnnnToolsv0.2.0Updated 2 hours ago

About

On a repeated identical local read_file, tells the model to answer from the earlier result when READ_ONCE_HITS=1, the file bytes are unchanged, and that exact result is still in the latest model request. Observe-only otherwise. /read-once stats shows hits. Disclosure — reads and SHA-256-hashes regular files under READ_ONCE_ROOT (1 MiB max), inspects the model request in memory for that result digest, and keeps ephemeral metadata only. No network, no telemetry, no disk cache, and no background watcher. It does not hash dotenv names, auth.json, or paths under vault, mcp-tokens, browser-profile, webapp, or pairing. Hits stay off unless READ_ONCE_HITS=1. A repeat of the same read can be Hermes's unchanged-stub rather than the file bytes.

Read Once 0.2

A native Hermes plugin. When a model repeats an identical read_file call, Read
Once can return a short note addressed to the model. The model then answers
from the earlier result. The user still receives that answer. Read Once does
this only when all of these hold:

  1. Hits are explicitly enabled (READ_ONCE_HITS=1).
  2. The file bytes are unchanged. The plugin re-reads and re-hashes the file
    immediately before every hit.
  3. The exact result that was delivered earlier is still visible in the most
    recent model request for the same session, task, and context epoch.

By default the plugin is observe-only: it records counts and never
substitutes anything.

The plugin is Python stdlib only and does not monkey-patch anything. It holds
state in process memory only: digests, ids, lengths, epochs, and visibility
flags. It never stores document or prompt text, writes no disk cache, makes no
network calls, and runs no background watcher.

The plugin does not open or hash a file whose name is a dotenv name (.env,
.env.local, .envrc, and the other .env.* names Hermes denies), auth.json,
auth.lock, .anthropic_oauth.json, webhook_subscriptions.json,
google_oauth.json, or a Bitwarden/1Password/Photon cache file, or any file
whose path has a vault, mcp-tokens, browser-profile, webapp, or
pairing component. Those reads pass straight through to Hermes. This is a
fixed name list. It does not mean the plugin never reads every file Hermes's
own denylist might refuse.

The note is for the model

A hit is not a message for the user. The tool result is a note that names the
earlier read_file call, the path, and the first 12 hex digits of the file's
SHA-256. It tells the model that the full content is already in the conversation,
and to answer the user from that content. It tells the model not to quote the
note and not to read the file again unless the user says it changed.

The note does not suggest another line range. It does not describe itself as a
stub. It does not start with a [read-once] tag.

A second identical read in the same model turn, immediately after that note,
calls Hermes once. That stops a loop if the model ignores the note. A later
turn, while the entry is still visible and the bytes are unchanged, receives
the note again.

Read Once does not store a Hermes unchanged-stub, and it does not store a
truncated page. A different offset or limit is a different key, so that read
passes through.

Support surface

  • Tool: read_file only, with arguments path (required, absolute),
    offset (default 1), and limit (default 2000, the Hermes 0.23 read_file default). The effective offset and
    limit are part of the cache key, so an explicit offset=1, limit=2000 matches
    a call that omits them.
  • Files: regular files only, not symlinks, at most 1 MiB, and resolved
    (realpath) to a location strictly under READ_ONCE_ROOT.
  • Scope: profile (if supplied), session_id, and task_id. Scopes never share
    entries. Each scope holds at most 128 entries, and the oldest is evicted
    first. At most 256 scopes are kept, also evicting the oldest.
  • Backend: local only. See non_local_backend below.
  • Hooks:
    • post_compaction, on_session_reset, on_session_finalize, and
      on_session_delete wipe entries. A hook with a session id clears that
      session and increments its epoch. The ids are session_id,
      old_session_id, and every id in on_session_delete's deleted_ids.
      A hook with no session id clears all sessions and increments the global
      epoch. A malformed deleted_ids also clears all sessions.
    • on_session_end is turn-scoped, so it only counts and does not wipe.
    • post_tool_call only counts.

Configuration

| Variable | Effect |
| --- | --- |
| READ_ONCE_ROOT | Directory that eligible files must resolve under. If unset, the plugin never substitutes. |
| READ_ONCE_HITS | Must be exactly 1 to allow substitution. Any other value, or unset, means observe-only. |
| READ_ONCE_NOTICE | off (default), quiet, or verbose. Any other value is off. Hermes does not expose a dim status line. PluginContext.post_message would insert a conversation message, so Read Once does not post one. The approximate count is only in /read-once stats. |

All three variables are read on every call. READ_ONCE_NOTICE does not change the tool result.

How a hit happens

  1. First read (miss). The plugin hashes the file, calls next_call
    exactly once, then reads the file again through the same root fd walk. If
    both hashes match, the backend is still local, and the result is exactly
    the requested window of those bytes, it stores the SHA-256 of the result
    as a candidate.
  2. Model request (llm_execution). For the same session, task, and
    profile, and the current epoch, the plugin checks whether the request
    contains the exact result string as one string leaf. An entry is visible
    only if it appears in the latest request, so a later request without it
    clears the flag. A request with a session id but a missing or empty
    task_id, or with a parent id, clears visibility for that whole session.
    The request is passed to next_call unchanged, exactly once, and the
    response is returned unchanged.
  3. Repeat read. The plugin checks the scope, key, and epoch, confirms the
    entry is visible, and confirms no refresh is pending for the path or its
    inode. It then re-reads and re-hashes the file and compares SHA-256, size,
    mtime_ns, st_dev, and st_ino. If everything matches and hits are
    enabled, it returns the model note without calling next_call. Another
    repeat in a later turn does the same. An immediate repeat in the same
    turn_id calls next_call once.

Bypass list (always passes through to Hermes)

| Reason | Meaning |
| --- | --- |
| root_unset, root_invalid | READ_ONCE_ROOT is missing or not a directory |
| relative_path | Path is not absolute. The plugin cannot know Hermes' working directory. |
| outside_root | The resolved path is not under the root, including escapes through symlinked directories |
| symlink, not_regular, too_large | Not an eligible file |
| stat_failed, open_failed, hash_failed, changed_during_hash | The file could not be checked reliably |
| unknown_arg, bad_args | Any argument key other than path, offset, or limit, or values that are not positive ints |
| missing_identity | session_id or task_id is missing or empty |
| parent_id | parent_session_id or parent_task_id is present. v1 does no parent/child sharing. |
| missing_tool_call_id | No id to cite in a note |
| non_local_backend | A backend signal says something other than local: process TERMINAL_ENV set to anything but local, a bound Hermes terminal scope whose TERMINAL_ENV is not local, or a backend kwarg. Checked before dispatch and again before seeding, because Hermes may bridge config.yaml into TERMINAL_ENV during the first dispatch. |
| terminal_policy_refused | Hermes bound its refusal scope (the profile's terminal policy could not be resolved) |
| credential_path | The path has one of the credential names or directory components listed above |
| refresh | The first read after /read-once refresh <path> |

Results are never seeded if they are not a str, look like an error, are a Hermes unchanged-stub, or are JSON with truncated, truncated_lines, or a next_offset field. Otherwise a result is stored only if it is exactly the requested window of the bytes the plugin read itself. The window is lines offset through min(offset + limit - 1, line count), split on \n. Three forms are accepted: the raw bytes of that window, the N|line form Hermes builds, or a Hermes JSON page whose content is that N|line form and whose total_lines and file_size, when present, match the file. A page that starts at another offset, skips or reorders lines, stops early, or differs in any byte is not stored (content_mismatch), so it can never hit. A plain source file that merely mentions next_offset can still be stored.

Fail open to Hermes, fail closed to optimization. If the plugin raises before
dispatch, it calls next_call once and returns that result. If it fails after
next_call succeeded, it returns that result and never calls again. If
next_call itself raises, the exception propagates unchanged and nothing is
retried.

Command

/read-once status
/read-once stats
/read-once refresh <path>
  • status and stats are the same report. The report shows the mode, the
    notice setting, the root, the epoch, the hit count, the approximate tokens
    not resent, the entry and visibility counts, and the counters for bypass
    reasons, misses by reason, and not-seeded reasons. It never shows file
    contents. The token line is approx tokens not resent this session: N (approx).
    N is the sum of round(result length / 4) over hits. It is an approximation.
    It is not a percentage and it is not a savings claim.
  • refresh <path> removes that path from every scope and forces the next
    matching read through normal dispatch. The reply is
    read-once: next read of <path> will be fresh (invalidated N entries).
    It matches entries by realpath and by (st_dev, st_ino), so a hardlink or
    a case variant on a case-insensitive volume invalidates the same file. It
    calls stat on the path but never opens or reads it.

Known limits and races

  • Concurrent writer. The plugin hashes the file immediately before a hit and
    before and after a miss's dispatch. A writer that changes the file after
    the hit check, or that changes and restores it between those hashes, is not
    detected. The note is true at the check, not afterwards.
  • Visibility matching is exact. If Hermes transforms a tool result after the
    middleware runs (for example, transform_tool_result), the delivered text
    won't match and the entry never becomes eligible. This fails closed.
  • Two different files with byte-identical results can confirm each other's
    visibility. The note still cites the original call id.
  • Parallel misses for the same key may both dispatch. There is no request
    coalescing.
  • Whole-file reads that share one (path, offset, limit) key can hit. A line
    range uses another key and passes through.
  • Stats report an approximate token count from result length divided by 4.
    The word approx is always on that line. The plugin makes no percentage claim
    and no dollar claim.
  • Hermes has no dim status printer. Read Once does not post a chat notice.
  • Backend residuals. Hermes passes no backend to middleware. The plugin
    reads the two signals Hermes 0.23's read_file resolves from: process
    TERMINAL_ENV, and the per-turn hermes_terminal_scope ContextVar. It
    finds the ContextVar by name in contextvars.copy_context() and does not
    import Hermes. If a later Hermes renames that variable, the plugin falls
    back to process TERMINAL_ENV only. test_real_loader.py catches that
    rename. Hermes also caches each task's file-ops environment in a private
    dict. If TERMINAL_ENV changes mid-process, Hermes can keep using a cached
    environment of the old backend, and the plugin cannot see it without a
    private import.

How to disable

Leave READ_ONCE_HITS unset, or set it to anything other than 1. The plugin
then only observes. To stop it entirely, remove or disable the plugin in Hermes.

Tested runtime

Hermes 0.23.0 only, commit 2bd378ed1b8d6bf2192cb26e0a55ff3091e6bd19.
requires_hermes is >=0.23.0. No earlier release was tested.

License: MIT. See LICENSE.

Tests

python -m pytest plugin/read-once/tests

test_read_once.py uses tmp_path and does not import Hermes.
test_real_loader.py imports Hermes 0.23 middleware. Run that file with the
Hermes 0.23 virtualenv, or the suite skips those cases on an interpreter that
cannot import it.

README from the reviewed commit e75af16; it updates when the author re-pins.

Developer Mode

Explore built-in Hermes toolsets and actions.

HermesAgent

Developer?

Create a Hermes extension for Desktop or agent workflows.

Developer Section