mempalace
Local-first memory for Claude Code. It stores conversations verbatim in a private palace you can search before answering about past work, people, projects, or decisions, with optional shared-brain hub setup.
What token-optimizer runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Claude Code
> /plugin marketplace add alexgreensh/token-optimizer> /plugin install token-optimizer@alexgreensh-token-optimizer
Cursor
Cursor also runs Claude Code hooks from .claude/settings.json, so copy these hooks there to use them in Cursor.
What fires, and when
PreToolUse
Readfor b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";Agent|Taskfor b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";mcp__.*for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";PreCompact
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";SubagentStart
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";Stop
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";SessionEnd
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";StopFailure
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";PostToolUse
Bash|PowerShell|Read|Glob|Grep|Agent|Edit|Write|MultiEdit|Notebofor b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";PostToolUseFailure
Bash|PowerShellfor b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";PostCompact
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";CwdChanged
for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 || continue; D="${CLAUDE_PLUGIN_ROOT}"; [ -r "$D/hooks/python-launcher.sh" ] || D="$(ls -d "${D%[/\\]*}"/*/ 2>/dev/null | grep -E "/[0-9]+[.][0-9]+[.][0-9]+/$" | sort -V | tail -n1)";Where it lives
#!/usr/bin/env python3
"""Runs a hook script as a module instead of as `__main__`, so CPython reuses
its __pycache__ bytecode across invocations.
Script-mode execution (`python foo.py`) never checks or writes __pycache__ for
the file being run as __main__ -- only for things it imports. Since every hook
call is a fresh process, running measure.py as a script would recompile it from
source every single time. Module-mode goes through the import system, which
checks and writes __pycache__, so only the first call pays the compile.
That first compile is not small. measure.py is ~2.7 MB / 58k lines (5.13.51).
Compiling it takes ~1.7 s on an Apple-silicon Mac and ~4 s under WSL2 Linux on
a 2015 i7, but 20-28 s with Windows CPython 3.12/3.13 on that same i7 (#264;
compile time grows faster than linearly with file size there). That is longer
than the host's hook timeouts (Stop 15 s, UserPromptSubmit/SessionStart 20 s),
and Python writes the .pyc only once the compile finishes, so a hook killed
mid-compile never warms the cache and every later call starts cold again.
_measure_bytecode_ready() therefore moves the cold compile into ONE detached
background process and lets the hook wait for it only as long as the host
deadline allows; a warm cache costs one stat and a 16-byte read. The
structural fix is splitting measure.py so each hook compiles only what it uses.
sys.path is stripped of '' and '.' (the cwd-equivalent entries the interpreter
would otherwise add) before inserting scripts_dir explicitly, so a same-named
file in the invoking project's own working directory (e.g. a project that
happens to have its own measure.py at its root) can never shadow the plugin's
module.
"""
from __future__ import annotations
import os
import runpy
import sys
def _warn_readonly_scripts_dir_once(scripts_dir: str) -> None:
"""If scripts_dir isn't writable, __pycache__ can't be written and CPython
recompiles the target from source on every call — the perf win silently
evaporates. Surface it, but at most once per day per user so we don't spam
stderr on the per-tool-call hot path. The marker lives in the OS temp dir
(writable even when the plugin install dir is read-only, which is the whole
failure mode). Best-effort: any error here must never break the hook."""
try:
if getattr(sys, "pycache_prefix", None):
return # bytecode goes to a cache prefix, scripts_dir needn't be writable
if os.access(scripts_dir, os.W_OK):
return # normal case — bytecode cache works, stay silent
import hashlib
import tempfile
import time
tag = hashlib.sha1(scripts_dir.encode("utf-8", "replace")).hexdigest()[:12]
marker = os.path.join(tempfile.gettempdir(), f".token-optimizer-ro-pyc-{tag}")
try:
fresh = (time.time() - os.path.getmtime(marker)) < 86400
except OSError:
fresh = False
if fresh:
return
# This marker lives at a predictable, world-shared temp path, so another
# local user can pre-plant something hostile there (CWE-377). Defend
# against all of it: unlink any existing entry first (drops a planted
# symlink, FIFO, or hardlink by name -- a hardlink's target is left
# untouched), then create our own EXCLUSIVELY. O_EXCL means we only ever
# open a file we just created; O_NOFOLLOW + O_NONBLOCK close the tiny
# unlink->open race (a re-planted symlink fails ELOOP, a re-planted FIFO
# fails ENXIO instead of blocking the hook forever). Every failure here
# is swallowed by the outer `except Exception` -> the hook stays
# fail-open and just skips the once-a-day warning. O_NOFOLLOW/O_NONBLOCK
# are absent on Windows; the getattr fallback of 0 makes them no-ops.
try:
os.unlink(marker)
except OSError:
pass
flags = (os.O_WRONLY | os.O_CREAT | os.O_EXCL
| getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0))
fd = os.open(marker, flags, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(str(time.time()))
sys.stderr.write(
f"[Token Optimizer] note: {scripts_dir} is not writable, so Python "
"bytecode (__pycache__) can't be cached — hooks will be slower than "
"expected. Make the plugin scripts dir writable to restore the speedup.\n"
)
except Exception:
pass
# --- Cold-compile gate (#264) ------------------------------------------------
# Entry points that import measure.py before doing anything, and how long each
# may wait for a background compile of it. Each wait leaves 8 s of the host's
# hook timeout (4.5 s for the Codex subagent bridge, whose own budget is 2.5 s)
# for interpreter start-up, loading the fresh .pyc and the hook's own work
# (1.0-1.5 s warm on the #264 machine):
# UserPromptSubmit / SessionStart runners: host 20 s (Claude Code and Codex)
# Stop runner: host 15 s (SessionEnd runs it async with 60 s)
# measure.py subcommands: StopFailure is the tightest at 12 s
# codex_hook_bridge: Codex SubagentStart/Stop, host 6 s
# A compile that finishes inside the wait gives exactly the old behaviour (the
# hook runs, warm). One that does not makes the hook exit 0 with no output, the
# host's "nothing to add", instead of being killed with the cache still cold.
_COLD_COMPILE_WAIT_S = {
"userpromptsubmit_runner": 12.0,
"sessionstart_runner": 12.0,
"stop_runner": 7.0,
"measure": 4.0,
"codex_hook_bridge": 1.5,
}
# The PreCompact save (measure.py compact-capture --trigger auto) is the one
# measure.py hook whose skip loses data: no save means the restore after the
# compaction falls back to an older checkpoint. Its host timeout is 30 s and it
# runs async, so it can wait far longer than the 4 s the tightest measure.py
# hook allows. 12 s + its own 15 s budget (hook_runtime.BUDGET_PRECOMPAC#!/usr/bin/env python3
"""Single-import PostToolUse dispatcher.
Replaces the SIX separate ``PostToolUse`` hooks.json entries that each spawned
``python-launcher.sh -> run.py -> module_runner.py -> runpy(<script>)`` --
eighteen processes per tool call at worst, on the single hottest path in the
product (PostToolUse fires on EVERY tool call):
1. ``bash_compress_hook.py --quiet`` [Bash] (15s, sync)
2. ``archive_result.py --quiet`` [mcp__.*] (15s, async)
3. ``archive_result.py --quiet`` [Bash|Read|Glob|Grep|Agent] (15s, async)
4. ``context_intel.py --quiet`` [Bash|Read|Grep|Glob|mcp__.*] (15s, async)
5. ``read_cache.py --invalidate --quiet`` [Edit|Write|MultiEdit|NotebookEdit] (10s, sync)
6. ``measure.py quality-cache --quiet --throttle-only`` [the union matcher] (10s, sync)
Combined declared budget: 80s. Measured per entry with ``__pycache__`` wiped
before every call (the container steady state): 122-223ms each, 798ms for the one
entry that imports measure.py, of which 127ms is FIXED dispatch overhead (bash
launcher -> run.py -> a second interpreter -> module_runner). Under a sustained
container workload, ``PostToolUse:Bash`` was CANCELLED 372 times and succeeded 9.
This runner is invoked ONCE per tool call, pays the dispatch overhead ONCE,
imports each hook module at most once, and runs every subcommand in-process
under ONE shared deadline. It is the same consolidation the ``UserPromptSubmit``
group received in ``hooks/userpromptsubmit_runner.py`` and the
``SessionStart`` group received in ``hooks/sessionstart_runner.py``; the
structure here deliberately mirrors those two files.
FOUR THINGS THAT ARE DIFFERENT HERE, and how each is handled:
1. MATCHERS. Unlike the other two groups, these six entries carry FIVE distinct
tool matchers (and ``archive_result`` is registered TWICE under two of
them). The consolidated hooks.json entry uses the UNION matcher -- which is
byte-identical to entry 6's, because entry 6 already matched every tool any
of the others did -- and each subcommand re-checks its OWN original matcher
in-process via ``_matches``. See the ``_MATCHER_*`` block for the exact
equivalence argument, including why ``re.search`` is the right replication
whichever way the host anchors its own regex.
2. ASYNC. Entries 2, 3 and 4 carried ``"async": true``. A hook group cannot be
half-async, and entries 1, 5 and 6 CANNOT be async (tests/
test_async_hook_wiring.py documents why for each), so the consolidated entry
is SYNCHRONOUS. See the "KNOWN CHANGE" block below for what that costs and
what it buys.
3. THE HOT PATH. ``measure.py`` is NOT imported at module scope here -- the
single deviation from the other two runners, and a deliberate one. It is
imported lazily, only by the quality-cache subcommand, and only after the
throttle question has been asked. That question is asked through ONE seam
(``_throttle_tick_due``) so that an import diet which learns to answer it
without importing measure.py plugs straight in and this consolidation does
not undo it. See ``_throttle_tick_due``.
4. THE INVARIANT. ``tests/test_hook_runtime_parity.py::
test_throttle_only_cache_miss_never_parses_transcript`` encodes that a
throttle-only cache MISS must never parse a transcript. It is enforced
INSIDE ``measure.quality_cache`` (``if pure_time_throttle and not force and
not cache_path.exists(): return None``), and this runner calls that function
with exactly ``pure_time_throttle=True, force=False``, so the invariant is
preserved by construction, not by re-implementation. There is deliberately
no bootstrap branch here: the UserPromptSubmit runner owns cache recovery
precisely because THIS path fires on every tool call.
KNOWN CHANGE -- the three async entries become synchronous:
* ``context_intel.py`` writes to the session store and emits NOTHING on
stdout, so nothing it produced is gained or lost. What changes is that the
turn now waits for it. In-process that is ~96ms of real work (223ms cold
minus the 127ms dispatch overhead it no longer pays).
* ``archive_result.py`` is the interesting one. Its ``mcp__.*`` registration
prints ``{"hookSpecificOutput": {"updatedMCPToolOutput": ...}}`` to replace
an oversized MCP result with a preview plus an archive pointer -- and as an
ASYNC hook that stdout was DISCARDED ENTIRELY on Claude Code, so the
replacement never happened there. It already happens on Codex, whose mirror
strips every async flag because Codex skips async hooks outright. Making
the group sync therefore does not invent a behaviour; it makes Claude Code
match the Codex mirror and the code's own documented intent. Large MCP
results will now actually be replaced by the pointer on Claude Code. That
is a real, user-visible change and it is called out here rather than buried.
* What could NOT be preserved: fire-and-forget. Under the old wiring a stalled
archive/intel write could not delay the turn at all. Now it can, bounded by
``_RUNNER_TOTAL_BUDGET``.
Key properties (shared with the other two runners):
- ONE shared ``HookDeadline`` replaces six independent host timeouts. Its
``os._exit(0)`` is the ONLY kill switch in the process, so an early
subcommand hang can never preemptively kill later ones. Remaining time is
budgeted fairly across the subcommands still pending, seeded from
``hook_runtime.resolve_entry_budget`` so the evidence-based per-entry
numbers keep governing each subcommand after consolidation. Each handler
gets an interruptible per-handler deadline; an overrun is reported on
stderr and dispatch continues with the next handler.
- stdin is read ONCE (six processes each got their own copy from the host;
one process has one stdin) and the SAME payload is handed to every
subcommand by patching ``hook_io.read_stdin_hook_inpu#!/usr/bin/env bash
# Locate a usable Python 3 interpreter and exec it with the given arguments.
# Survives:
# - macOS / Linux (python3 on PATH)
# - Windows python.org installs at spaced paths like "C:\Program Files\Python313\"
# - Windows py-launcher-only installs (py -3)
# - Windows Store Python (real installs proven alive via a flash-free
# GUI-twin proof-of-life probe, console --version probe as the fallback
# authority; non-functional AppExecutionAlias stubs skipped automatically)
# On Windows (Git Bash/MSYS), exec prefers the GUI-subsystem twin over the
# console binary to avoid the per-hook console-window flash and orphaned
# conhost.exe: python.exe/python3.exe swap to pythonw.exe, and py.exe (the
# py-launcher) swaps to pyw.exe beside it, so py-launcher-only installs no
# longer flash. See _maybe_swap_to_pythonw for the constraints.
# Exits 127 with a diagnostic message if none found.
set -eu
# A failed `exec` (bad shebang, file gone between discovery and exec, ENOEXEC)
# must return to the caller instead of terminating the shell non-zero, so
# discovery can advance to the next candidate and the launcher keeps its
# exit-0 guarantee (see HOOK-SAFETY at the bottom). `execfail` is half of that;
# _try_exec below is the other half.
shopt -s execfail
# _try_exec <interpreter> [args...] -- exec, returning 1 if the exec fails.
# bash 3.2 (macOS /bin/bash) exits the shell on a failed `exec` whenever errexit
# is on, even with execfail and even when the call sits in an `||` list. So
# errexit is dropped for the exec itself and restored on the only path that
# returns (a failed exec). A successful exec never returns.
_try_exec() {
set +e
exec "$@"
set -e
return 1
}
# Extglob enables +([0-9]) in the version-number case patterns below so
# the glob is anchored to the path-component boundary. Without it, * in a
# case pattern crosses / and Python[23]* matches Python3-evil/python.exe.
shopt -s extglob
# Known-safe prefixes for Python interpreter binaries.
# Binaries outside these directories are rejected even if on PATH.
# This prevents a compromised PATH entry from hijacking the interpreter.
# All prefixes are hardcoded (not derived from PATH-controlled binaries
# like `brew --prefix`, which would be circular trust).
_SAFE_PREFIXES="/usr/bin /usr/local/bin /opt/homebrew/bin /opt/homebrew/opt /home/linuxbrew/.linuxbrew/bin"
# _canonicalize_dir <dir> -> canonical absolute path via $_CANON_DIR
#
# `cd -P` resolves every symlink level portably (no realpath/readlink -f, which
# are absent or inconsistent on older macOS and MSYS). The old $(cd ...; pwd -P)
# form forked a bash subshell per call -- a whole process each on MSYS -- so
# this runs the builtin `cd` in a plain if-block (no subshell). That makes the
# jump visible to the whole launcher, and the launcher then `exec`s the hook
# interpreter, so the caller's state is put back before returning: the working
# directory (the hook must run in the project the host launched it from, not in
# the plugin's hooks/ dir), $PWD (logical, as the host had it) and OLDPWD
# (restored, or left unset if it was unset -- never exported as an empty string).
#
# `builtin cd` everywhere: a user who exports a `cd` shell function (the classic
# "cd then ls" wrapper; bash imports it from the environment) would otherwise
# have it run here, and whatever it prints would land in the hook's stdout ahead
# of the hook's own output (it used to be captured by the old $(...) form).
#
# Never leave a directory we cannot return to: if the start dir was removed
# (worktree cleanup), lost its +x bit, or its path no longer leads back to the
# same directory, the jump would be one-way and the interpreter would wake up
# inside the plugin's hooks/ dir with a PWD that points there. In that case skip
# the canonicalisation (callers treat failure as "cache off for this run").
# `-ef` is a builtin test (no fork) that follows symlinks, so a logical $PWD
# through a symlink still passes.
_canonicalize_dir() {
local _start=$PWD _had_old=${OLDPWD+x} _oldpwd=${OLDPWD:-} _rc=1
if [ -n "$_start" ] && [ -d "$_start" ] && [ -x "$_start" ] && [ "$_start" -ef . ]; then
:
else
return 1
fi
if CDPATH='' builtin cd -P -- "$1" 2>/dev/null; then
_CANON_DIR=$PWD
_rc=0
# Best effort: if the start dir vanished or lost +x there is nothing
# better to restore, and the launcher must still never abort (set -e).
CDPATH='' builtin cd -- "$_start" 2>/dev/null || :
fi
if [ -n "$_had_old" ]; then OLDPWD=$_oldpwd; else unset OLDPWD; fi
return "$_rc"
}
# Canonicalize a file path (resolve symlinks). exec follows symlinks, so a
# user-owned symlink pointing at a hostile target must be judged by the TARGET.
# realpath/`readlink -f` are absent on older macOS; fall back to a `pwd -P` walk
# of the parent (the leaf's own ownership is still checked via `-O`, which
# dereferences symlinks). Prints the resolved path, or fails.
_to_realpath() {
local p="$1" d b
if command -v realpath >/dev/null 2>&1; then
realpath "$p" 2>/dev/null && return 0
fi
if readlink -f "$p" >/dev/null 2>&1; then
readlink -f "$p" 2>/dev/null && return 0
fi
d=${p%/*}
if [ "$d" = "$p" ]; then d=.; elif [ -z "$d" ]; then d=/; fi
b=${p##*/}
_canonicalize_dir "$d" || return 1
printf '%s/%s\n' "$_CANON_DIR" "$b"
}
# Print a path's permission bits as octal, ZERO-PADDED to at least 3 digits.
# GNU stat first, then BSD/macOS stat. Fails if neither works (caller refuses
# trust). The pad matters: GNU `stat -c %a` prints "2" for mode 0002, and the
# caller's last-3-digit slice would then read EMPTY group/other digits -- a set
# write bit reading as clean, a false-accept. Flooring to 3 digits makes the
# group/other-writable check robust to stat's short formatting.
_to_mode() {
local mode
mode=$(stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1" 2>/dev/null) || return 1
[#!/usr/bin/env python3
"""Cross-platform hook dispatcher.
Invoked from hooks.json via a small bash launcher that locates a usable
Python 3 interpreter on macOS, Linux, and Windows:
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh\" \"${CLAUDE_PLUGIN_ROOT}/hooks/run.py\" <script-relative-path> [args...]"
The launcher handles Windows-specific gotchas (Program Files spaced paths,
Microsoft Store zero-byte stubs in WindowsApps, py launcher fallback) so
this file can assume it's running under a real Python 3.9+.
This dispatcher resolves the target script under CLAUDE_PLUGIN_ROOT,
checks it exists, and runs it with the same interpreter (sys.executable).
On timeout we kill the child (Popen.kill) to avoid leaking a process
holding the trends.db SQLite lock. Always exits 0 so hook failures never
block the user's tool call.
Windows reap note: module_runner.py runs measure.py IN-PROCESS via
runpy.run_module, so the child proc IS measure.py (the trends.db lock
holder), not a grandchild. On Windows we reap with plain proc.kill()
(TerminateProcess of proc.pid only), NOT taskkill /F /T which would walk
the PPID tree and wrongly kill the detached session-end-flush worker
(the one CREATE_BREAKAWAY_FROM_JOB exists to keep alive). The SIGINT/
SIGTERM handler is best effort on Windows: the detached child has no console
and never sees Ctrl+C, Popen.wait() there is a plain WaitForSingleObject that a
Python handler cannot interrupt (the handler runs once the wait returns), and
an external TerminateProcess from the host bypasses Python handlers entirely.
"""
from __future__ import annotations
import json
import os
import signal
import subprocess
import sys
import traceback
from pathlib import Path
# Defense in depth: the launcher script already filters interpreters, but
# if a user's PATH has a stale Python 3.7 that slipped through, bail early
# so later imports don't explode with confusing SyntaxError noise.
if sys.version_info < (3, 9):
sys.exit(0)
# Module-level handle so the signal handler can reach the active child when
# Claude Code (or any parent) sends SIGTERM/SIGINT to run.py itself. Without
# this, an external kill reaps run.py but orphans the measure.py grandchild,
# which keeps the inherited stdout pipe open and makes the parent hang waiting
# for EOF (the multi-minute stop-hook hang).
_child_proc: subprocess.Popen | None = None
# Hard ceiling on one hook child, seconds. One name for both the POSIX reap
# timer and the Windows wait so tests can shorten it in a single place.
_CHILD_WAIT_TIMEOUT_S = 120
_PYCACHE_PREFIX_ENV = "PYTHONPYCACHEPREFIX"
def _remote_vm_env() -> bool:
"""True in a hosted Claude Code VM (cloud or Cowork). Env-only, no imports:
this runs before anything from the plugin's scripts dir is imported."""
if os.environ.get("CLAUDE_CODE_REMOTE", "").strip().lower() in ("1", "true", "yes", "on"):
return True
if os.environ.get("CLAUDE_CODE_REMOTE_SESSION_ID", "").strip():
return True
if os.environ.get("TOKEN_OPTIMIZER_CLOUD_KIT_VERSION", "").strip():
return True
return os.environ.get("TOKEN_OPTIMIZER_REMOTE_HOST", "").strip().lower() in ("cloud", "cowork")
def _inside_git_work_tree(path: Path) -> bool:
"""True when ``path`` or any ancestor holds a ``.git`` entry (dir or file).
A bounded walk of cheap stats; any error reads as False."""
try:
current = Path(path)
for _ in range(64):
if (current / ".git").exists():
return True
parent = current.parent
if parent == current:
return False
current = parent
except (OSError, ValueError):
pass
return False
def _bytecode_cache_dir() -> Path:
"""Per-user bytecode cache: %LOCALAPPDATA% on Windows, $XDG_CACHE_HOME or
~/.cache elsewhere, then token-optimizer/pycache."""
base = None
if os.name == "nt":
raw = os.environ.get("LOCALAPPDATA", "").strip()
if raw and os.path.isabs(raw):
base = Path(raw)
else:
raw = os.environ.get("XDG_CACHE_HOME", "").strip()
if raw and os.path.isabs(raw):
base = Path(raw)
if base is None:
base = Path.home() / ".cache"
return base / "token-optimizer" / "pycache"
def _bytecode_prefix_for(plugin_root: Path) -> str | None:
"""Where Python bytecode for the plugin should go, or None for "beside the
scripts" (the default, unchanged on a normal desktop install).
Bytecode is moved out of the plugin tree when the tree sits inside a git
work tree (a plugin vendored into a repo, a dev checkout) or when running
in a hosted VM. Otherwise every hook drops ``__pycache__`` dirs into the
user's repo: untracked files that Claude Code cloud's own Stop hook then
nags the model to commit. A cache PREFIX keeps the warm-start speedup
(module_runner's whole point); disabling bytecode would lose it. An
existing PYTHONPYCACHEPREFIX is the user's choice and is left alone.
"""
try:
if os.environ.get(_PYCACHE_PREFIX_ENV, "").strip():
return None
if not (_remote_vm_env() or _inside_git_work_tree(plugin_root)):
return None
return str(_bytecode_cache_dir())
except Exception:
return None
def _apply_bytecode_prefix(prefix: str | None, child_env: dict | None = None) -> None:
"""Point this process (sys.pycache_prefix) and the hook child (env) at
``prefix``. A no-op for None. Never raises."""
if not prefix:
return
try:
sys.pycache_prefix = prefix
if child_env is not None:
child_env[_PYCACHE_PREFIX_ENV] = prefix
except Exception:
pass
def _reap(proc, posix_sig):
"""Reap the child process. Never raises.
On Windows, the child proc IS measure.py (module_runner.py runs it
in-process via runpy.run_module), so a plain ``proc.kill()``
(TerminateProcess of proc.pid only) releases the t#!/usr/bin/env python3
"""Single-import SessionStart dispatcher.
Replaces the FIVE separate ``SessionStart`` hook commands in hooks.json that
each spawned ``python-launcher.sh -> run.py -> module_runner.py ->
runpy(measure.py)`` and repeated the interpreter and module startup work:
1. ``measure.py ensure-health --once-mark`` (timeout 15, async)
2. ``measure.py quality-cache --force --quiet --once-mark`` (timeout 20)
3. ``measure.py compact-restore --compact`` [matcher compact] (timeout 20)
4. ``read_cache.py --clear-compacted --quiet`` [source compact|clear] (timeout 10)
5. ``measure.py compact-restore --new-session-only --once-mark`` (timeout 20)
This runner is invoked ONCE per session start, imports ``measure.py`` ONCE, and
runs all five subcommands in-process under ONE shared deadline. That removes
repeated startup work while keeping the host-specific timeout as a backstop.
This is the same consolidation the ``UserPromptSubmit`` group received in
``hooks/userpromptsubmit_runner.py``; SessionStart never got it. The
structure here deliberately mirrors that file.
Key properties:
- ONE shared ``HookDeadline`` (18s, 2s margin under the hooks.json timeout of
20, and 7s under Codex's hard 25s SessionStart ceiling) replaces the five
independent per-entry timeouts. Remaining time is budgeted fairly across the
subcommands still pending (``_runner_budget``); the shared deadline's
``os._exit(0)`` is the ONLY kill switch in the process, so an early
subcommand hang can never preemptively kill later ones.
- ``--once-mark`` latching semantics are preserved EXACTLY: subcommands 1, 2
and 5 call ``measure._mark_ran_this_session`` (WRITE, never check), so the
SessionStart work always runs -- including on the second SessionStart of a
session (resume / post-compaction keep the same session_id) --
while the ``--once-per-session`` UserPromptSubmit copies stay latched out.
No unlink-on-failure here: the ``--once-mark`` semantics differ from the
retryable ``--once-per-session`` flow and must remain distinct.
- The ``matcher: "compact"`` gate on subcommands 3 and 4 is replicated
in-process by ``_is_compact_start`` (the SessionStart ``source`` field the
host matches on), so a non-compact start skips them exactly as the matcher
did.
- The consent gate that run.py used to apply per-entry is applied per
subcommand HERE: ensure-health was consent-EXEMPT (it bootstraps the consent
flags), the other four were consent-gated. run.py exempts this runner path
wholesale, same as the UserPromptSubmit runner.
- stdout from the two compact-restore subcommands is captured and emitted in
dispatch order at the end of ``main()`` as ONE host-valid JSON envelope
(raw text, systemMessage JSON, additionalContext JSON). ensure-health and
quality-cache ``{"systemMessage": ...}`` JSON lines are user-facing (shown
to the USER's terminal, NOT injected into the model's context) and also
feed the envelope; their plain-text diagnostics are routed to a log file.
All subcommand stderr is also routed to the log file -- the host captures
both stdout and stderr into the model's session context, so diagnostics
must reach neither stream.
- One subcommand throwing/aborting never aborts the others (each is wrapped in
``_run_safely``); the hook always exits 0. Failure notices and tracebacks
go to the diagnostics log file, not stderr.
- ``run._check_consent`` is imported by explicit path so a future
``skills/.../run.py`` on ``sys.path`` cannot shadow the real gate.
The runner calls the same module-level entrypoints used by the ``__main__``
dispatch, preserving their arguments and behavior while only changing how
they are scheduled.
Run: ``hooks/sessionstart_runner.py`` (via run.py -> module_runner.py).
"""
from __future__ import annotations
import importlib.util as _importlib_util
import io
import json
import os
import sys
import traceback
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
def _resolve_measure_dir() -> str:
"""Locate the directory holding measure.py so ``import measure`` works.
module_runner.py puts THIS file's parent (``hooks/``) on ``sys.path[0]``;
measure.py lives in ``skills/token-optimizer/scripts/``. Resolve it from
``CLAUDE_PLUGIN_ROOT`` (set by the host before hook invocation) with a
``__file__``-relative fallback (the plugin root is this file's
grandparent), and insert it ahead of ``hooks/`` so measure.py and its
sibling modules (runtime_env, plugin_env, hook_io, hook_runtime) resolve.
"""
candidates: list[Path] = []
pr = os.environ.get("CLAUDE_PLUGIN_ROOT", "").strip()
if pr:
candidates.append(Path(pr) / "skills" / "token-optimizer" / "scripts")
try:
candidates.append(
Path(__file__).resolve().parent.parent
/ "skills" / "token-optimizer" / "scripts"
)
except Exception:
pass
for c in candidates:
try:
if (c / "measure.py").is_file():
return str(c.resolve())
except OSError:
continue
# Last resort: assume CWD-relative scripts layout (manual/dev invocation).
return str((Path.cwd() / "skills" / "token-optimizer" / "scripts").resolve())
_MEASURE_DIR = _resolve_measure_dir()
if _MEASURE_DIR and _MEASURE_DIR not in sys.path:
sys.path.insert(0, _MEASURE_DIR)
import measure # noqa: E402 (path bootstrapped above)
# --------------------------------------------------------------------------- #
# Diagnostics log: the host captures BOTH stdout and stderr from SessionStart
# hooks into the model's session context, where they are re-billed on every
# turn. The only correct destination for hook diagnostics is therefore a log
# file, never stdout and never stderr.
# --------------------------------------------------------------------------- #
_DIAGNO#!/usr/bin/env python3
"""Single-import Stop + SessionEnd dispatcher.
Replaces the THREE separate ``Stop`` hook commands in hooks.json that each
spawned ``python-launcher.sh -> run.py -> module_runner.py -> runpy(measure.py)``
and caused each subcommand to start a separate interpreter and import
``measure.py`` independently:
1. ``measure.py compact-capture --trigger stop --quiet`` (timeout 12)
2. ``measure.py session-end-flush --trigger stop --quiet --defer`` (timeout 8)
3. ``measure.py keepwarm-arm --quiet`` (timeout 5)
The separate processes also repeat cold imports when a read-only plugin
installation cannot retain ``__pycache__``. Consolidation pays that startup
cost once, then runs the real work under one shared deadline.
The ``SessionEnd`` hook (a fourth entry: ``session-end-flush --trigger end
--defer``, timeout 60) joins this runner too. It synchronously marks the
session ended before dispatching the SAME
runner file, branching on ``hook_event_name`` so the trigger value ``end`` is
preserved exactly. This saves one more measure.py import per session end.
This is the same consolidation the ``SessionStart`` and ``UserPromptSubmit``
groups received (``hooks/sessionstart_runner.py``,
``hooks/userpromptsubmit_runner.py``); the structure deliberately mirrors those
files.
Key properties:
- ONE shared ``HookDeadline`` (13s for Stop, 2s margin under the hooks.json
timeout of 15; 58s for SessionEnd, 2s margin under timeout 60) replaces the
three independent per-entry timeouts (12 + 8 + 5 = 25s). Remaining time is
budgeted fairly across the subcommands still pending (``_runner_budget``);
the shared deadline's ``os._exit(0)`` is the ONLY kill switch in the
process, so an early subcommand hang can never preemptively kill later ones.
- The ``--defer`` behaviour is preserved EXACTLY: ``session-end-flush`` calls
``measure._dispatch_session_end_flush`` with the same args list the
``__main__`` dispatch received, so the detached worker gets the same
``--trigger`` / ``--defer`` / ``--quiet`` flags.
- The trigger values ``stop`` and ``end`` are preserved EXACTLY so entry
budget rules can distinguish the Stop and SessionEnd work.
- SessionEnd runs synchronously to prevent keep-warm from pinging ended sessions.
- No once-per-session latching exists in the Stop/SessionEnd chain (none of
the three subcommands use ``_mark_ran_this_session`` or
``_ran_once_this_session``), so there is nothing to preserve there.
- stdout from all subcommands is captured through one buffered emitter and
emitted in dispatch order at the end of ``main()``. Under ``--quiet`` (the
Stop path) all three subcommands produce no stdout; the SessionEnd path
likewise produces none (``_dispatch_session_end_flush`` only spawns a
detached worker). The buffer is a safety net for any unexpected output.
- One subcommand throwing/aborting never aborts the others (each is wrapped in
``_run_safely``); the hook always exits 0.
- No consent gate: the Stop and SessionEnd subcommands are all data
collection, none bootstrap the consent flags (unlike SessionStart /
UserPromptSubmit which contain ensure-health). run.py's consent gate
handles them: when consent is False, run.py returns 0 and the runner never
fires. The runner is NOT in run.py's exempt list, preserving the
consent-gated semantics of the three legacy entries.
The runner calls the same module-level entrypoints used by the ``__main__``
dispatch, preserving their arguments and behavior while only changing how
they are scheduled.
Run: ``hooks/stop_runner.py`` (via run.py -> module_runner.py).
"""
from __future__ import annotations
import io
import os
import sys
import traceback
from contextlib import redirect_stdout
from pathlib import Path
def _resolve_measure_dir() -> str:
"""Locate the directory holding measure.py so ``import measure`` works.
module_runner.py puts THIS file's parent (``hooks/``) on ``sys.path[0]``;
measure.py lives in ``skills/token-optimizer/scripts/``. Resolve it from
``CLAUDE_PLUGIN_ROOT`` (set by the host before hook invocation) with a
``__file__``-relative fallback (the plugin root is this file's
grandparent), and insert it ahead of ``hooks/`` so measure.py and its
sibling modules (runtime_env, plugin_env, hook_io, hook_runtime) resolve.
"""
candidates: list[Path] = []
pr = os.environ.get("CLAUDE_PLUGIN_ROOT", "").strip()
if pr:
candidates.append(Path(pr) / "skills" / "token-optimizer" / "scripts")
try:
candidates.append(
Path(__file__).resolve().parent.parent
/ "skills" / "token-optimizer" / "scripts"
)
except Exception:
pass
for c in candidates:
try:
if (c / "measure.py").is_file():
return str(c.resolve())
except OSError:
continue
# Last resort: assume CWD-relative scripts layout (manual/dev invocation).
return str((Path.cwd() / "skills" / "token-optimizer" / "scripts").resolve())
_MEASURE_DIR = _resolve_measure_dir()
if _MEASURE_DIR and _MEASURE_DIR not in sys.path:
sys.path.insert(0, _MEASURE_DIR)
import measure # noqa: E402 (path bootstrapped above)
def _read_hook_input() -> dict:
"""Read the hook stdin JSON once, non-blocking, shared across subcommands.
Uses measure's own shared reader (Windows pipe-peek + Unix select) so the
behavior matches what each ``__main__`` handler saw individually. 1 MB cap
is generous: the largest handler (compact-capture) reads the default 65536,
and none of the three Stop handlers reads more.
"""
try:
return measure._read_stdin_hook_input(max_bytes=1_000_000) or {}
except Exception:
return {}
def _record_payload_shape(hook_input: dict) -> None:
"""Remember WHICH identity fields the host sent this Stop hook (never their
values), so tAll 8 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
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.
Find the ghost tokens. Fix them. Survive compaction. Avoid context quality decay.
Local-first memory for Claude Code. It stores conversations verbatim in a private palace you can search before answering about past work, people, projects, or decisions, with optional shared-brain hub setup.
Gives Claude Code a private memory that stays on your machine. It stores past conversations word for word, so you can ask what was decided, what happened before, or who was involved and get the original context back. It can also hand tasks to other agents.
Keeps large command outputs, logs and data files out of your AI assistant's context by running processing in a sandbox and returning only the relevant parts. You also get a searchable index of local files and diagnostics for checking the setup.
Keeps large tool outputs, logs, and files out of Claude Code's context window by processing them in a sandbox and indexing them for focused search. Includes diagnostics and a knowledge base indexer.
Gives coding agents a git-backed wiki for long-term project memory, with skills for retrieval, durable notes, session handoffs between agents, cross-project messaging, and memory consolidation.
A coding agent harness whose agents keep memory and identity across sessions, so they learn and improve over time. You can run it in a terminal, in the browser, or through Telegram, Slack and Discord.
A background agent for Claude Code that watches your sessions and reads your files, builds up memory over time, and offers guidance back during your work.
A self-learning system for Claude Code that captures your corrections during sessions and reminds you to run /reflect to save them to CLAUDE.md as permanent memory. It also discovers recurring workflow patterns.