Skip to content
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.

From plugin
engramory
1951 skill1 hook
Install
$ npx -y skills add tinqiao-oss/engramory --agent claude-code

Ships 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

Repo: tinqiao-oss/engramory