Agent Memory
Hook
Hooks
What engramory runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add tinqiao-oss/engramory --agent claude-codeShips with engramory. Installing the plugin gets these hooks.
Where it lives
- hooks/engramory_index_guard.pyGitHub
Read the script
#!/usr/bin/env python3 """ Engramory index-size guard — a Claude Code PreToolUse hook. Keeps the memory index (MEMORY.md) from growing past the window the host actually loads. Claude Code loads the first 200 lines / 25 KB of MEMORY.md at the start of every conversation (documented behavior; configurable below for other hosts), so anything past EITHER limit silently stops being recalled. Lines alone are not enough: an index can be well under the line cap yet over the byte cap because its lines are long (content leaked into the index). This hook predicts the index size *after* the pending edit on BOTH dimensions, then: - over a HARD cap AND the edit GROWS the index -> DENY (compact first; SKILL §6). - over a cap but the edit SHRINKS/keeps size (compaction in progress), or only over the WARN cap -> inject a non-blocking nudge to compact via additionalContext with NO permissionDecision (omitting the decision = "no opinion -> normal permission flow", the documented way to add context without affecting the decision; unambiguous across interactive & non-interactive modes). It never returns "allow" (which AUTO-APPROVES and bypasses the user's permission prompt) and avoids "defer" (whose effect differs between modes). - otherwise -> silent pass-through. Shrinking edits are always allowed even while still over a cap, so the index can be compacted incrementally (e.g. 210 -> 205 -> 198) instead of in one edit. Edit/MultiEdit are predicted by *simulating* the sub-edits sequentially on a copy of the current text (honouring uniqueness / replace_all, skipping sub-edits whose old_string is absent or non-unique), because Claude Code applies MultiEdit sub-edits in order, each on the previous result. Fail-SAFE, not fail-open, for the things that matter. The stdin payload is read as raw bytes and decoded as UTF-8 (the encoding Claude Code actually writes), never via the locale text stream — on a cp936/cp1252 Windows console the locale decode of a real CJK payload either crashed (guard silently off) or inflated the predicted byte size ~1.5x (false denies). A NON-UTF-8 index is decoded lossily for the line/edit math while its size is taken from the raw on-disk bytes, so a growing write still counts as growth and stays gated. A malformed numeric env var falls back to its default. An index that EXISTS but cannot be read is a separate case: substituting an empty base would make every Edit/MultiEdit `old_string` look absent, so the simulation would predict an empty result and a large growing edit would pass unnoticed. The hook therefore reports that the cap was NOT verified (and points at engramory_check.py) instead of passing silently. It still does not deny — a transient lock must not brick editing. A missing index (no file yet) legitimately starts from empty. Only a genuine unexpected exception falls open. Wire it up in settings.json (PreToolUse, matcher "Edit|Write|MultiEdit"). Use the EXEC form so the path is passed literally — no shell, so no quoting / backslash pitfalls: {"type": "command", "command": "python3", "args": ["/ABSOLUTE/PATH/TO/engramory/hooks/engramory_index_guard.py"]} (macOS/Linux usually have only `python3`, not `python`; on Windows use "python" or the absolute interpreter path. See hooks/settings.snippet.json and hooks/INSTALL.md.) Config via environment variables (all optional): ENGRAMORY_HARD hard line ceiling, default 200 ENGRAMORY_WARN soft line warning, default 150 ENGRAMORY_HARD_BYTES hard byte ceiling, default 25600 (25 KB) ENGRAMORY_WARN_BYTES soft byte warning, default 20480 (20 KB) ENGRAMORY_LINE_WARN_BYTES per-line prose budget, default 200: an index line whose text — with its `](path.md)` targets and `[[wikilinks]]` removed — exceeds this is content leaking into the index. The hook nudges (context only, never denies) when an edit ADDS such a line; existing ones are the doctor's job. ENGRAMORY_INDEX_NAME index filename to guard, default "MEMORY.md" ENGRAMORY_INDEX_PATH absolute path of the one index to guard (overrides name matching; use when several MEMORY.md exist) """ import json import os import re import sys def _allow_silently(): # No output + exit 0 == "no opinion", tool proceeds through normal flow. sys.exit(0) def _emit(decision=None, reason=None, context=None): # decision="deny" blocks the tool call. For a non-blocking nudge we pass NO # decision and only additionalContext: omitting permissionDecision means "no # opinion -> normal permission flow", the documented way to surface context to # the model without affecting the decision. We never emit "allow" (auto-approve) # and deliberately avoid "defer", whose runtime effect differs between Claude # Code's interactive and non-interactive modes. hso = {"hookEventName": "PreToolUse"} if decision is not None: hso["permissionDecision"] = decision if reason is not None: hso["permissionDecisionReason"] = reason if context is not None: hso["additionalContext"] = context print(json.dumps({"hookSpecificOutput": hso})) sys.exit(0) def _envint(name, default): # A bad/empty env var must fall back to the default, not disable the guard. # A non-positive cap (0 / negative) is nonsensical: it would make every growing # edit "over cap" and brick all index editing, so it also falls back to the # default rather than being taken at face value. raw = os.environ.get(name) if raw is None: return default try: val = int(raw.strip()) except (TypeError, ValueError): return default return val if val > 0 else default def _lines(text): # Logical lines counted by newline only. NOT str.splitlines(): that also # breaks on Unicode line boundaries (U+0085, U+2028, U+000B, ...) common in # C
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withengramory
**An opinionated, zero-infrastructure memory protocol for small-scale, local, file-based agent memory** — a strict curation discipline plus a validator (tools/engramory_doctor.py), loaded as standing rules (CLAUDE.md / AGENTS.md / your host's rules file).
Get the whole plugin
Stats
195
Stars
12
Forks
Maintained
Maintenance
Python
Language
MIT
License
1mo ago
Last commit
4mo ago
Created
4d ago
Added

