Hooks
What token-optimizer runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add alexgreensh/token-optimizer > /plugin install token-optimizer@alexgreensh-token-optimizer
Ships with token-optimizer. Installing the plugin gets these hooks.
What fires, and when
PreToolUse
- Matches
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/read_cache.p - Matches
Bashfor 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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/bash_hook.py - Matches
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/measure.py c - Matches
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/refetch_guar
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/measure.py dfor 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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/measure.py cfor 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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/read_cache.p
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" hooks/sessionstart_runner.py; done; exit 0
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" hooks/stop_runner.py; done; exit 0
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" hooks/stop_runner.py; done; exit 0
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/measure.py c
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" hooks/userpromptsubmit_runner.py; done; exi
PostToolUse
- Matches
Bash|Read|Glob|Grep|Agent|Edit|Write|MultiEdit|NotebookEdit|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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" hooks/posttooluse_runner.py; done; exit 0
PostToolUseFailure
- Matches
Bashfor 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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" hooks/posttooluse_runner.py; done; exit 0
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/measure.py q
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; L="${CLAUDE_PLUGIN_ROOT}/hooks/python-launcher.sh"; [ -r "$L" ] || exit 0; exec "$b" "$L" "${CLAUDE_PLUGIN_ROOT}/hooks/run.py" skills/token-optimizer/scripts/read_cache.p
Where it lives
- hooks/module_runner.pyGitHub
Read the script
#!/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 (35k+ lines) as a script recompiles it from source every single time: ~0.3s of pure CPython parse/compile on top of whatever the hook actually does. Module-mode goes through the import system, which does check/write __pycache__, cutting that to ~0.1s after the first call. 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 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 def main() -> int: if len(sys.argv) < 3: return 0 scripts_dir = sys.argv[1] module_name = sys.argv[2] script_args = sys.argv[3:] # Defense-in-depth: don't rely solely on run.py sanitizing these. Refuse a # module_name that isn't a bare Python identifier (blocks path separators, # dots, and traversal), and require the resolved target to actually exist in # scripts_dir. Fail OPEN (return 0) rather than raise — a hook must never # crash the tool call. Without this, any future caller that skipped run.py's # own validation would hand runpy an arbitrary-module-execution primitive. if not module_name.isidentifier(): return 0 if not os.path.isfile(os.path.join(scripts_dir, module_name + ".py")): return 0 _warn_readonly_scripts_dir_once(scripts_dir) sys.path = [p for p in sys.path if p not in ("", ".")] sys.path.insert(0, scripts_dir) sys.argv = [module_name, *script_args] # Per-event entry budget first (PreToolUse / PostToolUse / Stop). These are # self-imposed deadlines that land far under the host's hooks.json timeout, # so an over-budget hook exits 0 with NO output instead of being killed # mid-write by the host. hook_runtime.resolve_entry_budget owns the table # and the evidence behind each number. When the entry point isn't budgeted # (consolidated Claude runners, or entries with no precise argv rule) we # fall back to the 110s orphan backstop described below. # # Generic Windows orphan-grandchild backstop, NOT a collect/dashboard fossil bound. # # When the host TerminateProcess-es run.py on Windows it bypasses run.py's # SIGTERM handler, orphaning this in-process grandchild while it still # holds the hook stdout pipe. HookDeadline's daemon thread calls # os._exit(0) so the pipe EOFs even when no parent is left to reap us. # The 110s sits a few seconds under run.py's 120s wait so a normally # reaped hook never trips it. # # This does NOT bound the collect/dashboard fossil: that fossil is a # raw `measure.py collect && dashboard` in settings.json that invokes # measure.py directly via the launcher, never through run.py/module_runner, # so this layer never sees it. The fossil is bounded by the 20s dispatch # budget in measure.py._dispatch_collect/_dispatch_dashbo - hooks/posttooluse_runner.pyGitHub
Read the script
#!/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 cost per entry with ``__pycache__`` wiped before every call (the container steady state -- module_runner.py warns that a read-only scripts dir means bytecode is never cached): entry point warm cold bash_compress_hook.py --quiet 128ms 122ms archive_result.py --quiet 204ms 218ms context_intel.py --quiet 204ms 223ms read_cache.py --invalidate 205ms 218ms measure.py quality-cache -to 226ms 798ms Of each of those, 127ms is FIXED dispatch overhead (bash launcher -> run.py -> a second interpreter -> module_runner) paid before any hook work happens, and the 798ms cold outlier is the one entry that imports measure.py (682ms cold / 99ms warm on its own). 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 - hooks/python-launcher.shGitHub
Read the script
#!/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 # 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 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=$(dirname "$p") || return 1 b=$(basename "$p") || return 1 ( cd "$d" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$b" ) || return 1 } # 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 [ -n "$mode" ] || return 1 while [ "${#mode}" -lt 3 ]; do mode="0$mode"; done printf '%s\n' "$mode" } # True IFF the interpreter (after symlink resolution) AND its containing dir are # both owned by the effective uid and writable by NOBODY else (not group, not # other) -- the trust boundary ssh/sudo/git(safe.directory) use. This is a pure # stat check; it never runs the target (deciding trust by executing the binary # would mean running attacker code to find out whether it is safe). It lets any # version-manager shim (mise/pyenv/asdf/rbenv/custom dir, under any home root) # be trusted generically, while a hijack `python3` in a world-writable or # foreign-owned dir stays refused. POSIX only -- the caller gates out Windows, # where stat ownership/mode is faked under Git-Bash/MSYS. _to_owned_unwritable() { local p="$1" real dir mode g o real=$(_to_realpath "$p") || return 1 [ -n "$real" ] || return 1 dir=$(dirname "$real") || return 1 # Ownership: `-O` == owned by the effective uid (portable, no stat parse; # dereferences symlinks so the resolved target's owner is what's checked). [ -O "$real" ] || return 1 [ -O "$dir" ] || return 1 # Not group- or other-writable, on BOTH the file and its dir. The low 3 # octal digits are user/group/other; a set write bit makes the digit 2,3,6,7. mode=$(_to_mode "$real") || return 1 mode=${mode: -3}; g=${mode:1:1}; o=${mode:2:1} case "$g" in 2|3|6|7) return 1 ;; esac case "$o" in 2|3|6|7) return 1 ;; esac mode=$(_to_mode "$dir") || return 1 mode=${mode: -3}; g=${mode:1:1}; o=${mode:2:1} case "$g" in 2|3|6|7) return 1 ;; esac case "$o" in 2|3|6|7) return 1 ;; esac return 0 } _is_safe_prefix() { local IFS=$' \t\n' local binpath="$1" prefix # Reject path traversal FIRST: a '..' component lets a textual prefix match # (e.g. /usr/bin/../../tmp/evil/python3) pass the allow-list globs below yet # resolve OUTSIDE a trusted dir at exec time. Interpreter paths are absolute # and never legitimately contain a '..' path component. case "$binpath" in *"/../"*|*"/..") return 1 ;; esac for prefix in $_SAFE_PREFIXES; do case "$binpath" in "$prefix"/*) return 0 ;; esac done # Windows install locations (git-bash/MSYS path form, e.g. /c/...). # Drive-letter-anchored to preserve the anti-PATH-hijack intent. # Version-number suffixes block directory-name spoofing (e.g. Python3-evil). case "$binpath" in # +([0-9]) anchors the version suffix to digits-only so a spoofed # dir name like Python3-evil cannot pass (previously * crossed / and # matched Python3-evil/python.exe). The trailing /* requires a path # separator after the version component. /[a-zA-Z]/Program\ Files/Python[23]+([0-9])/*) return 0 ;; /[a-zA-Z]/Program\ Files\ \(x86\)/Python[23]+([0-9])/*) return 0 ;; /[a-zA-Z]/Python3+([0-9])/*) return 0 ;; - hooks/run.pyRunsGitHub
Read the script
#!/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 only fires for console Ctrl+C or in-process os.kill; 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 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 trends.db lock without walking the PPID tree and killing the detached session-end-flush worker (the one CREATE_BREAKAWAY_FROM_JOB exists to keep alive). On POSIX, the child is started with ``start_new_session=True`` so it leads its own process group; killing the group reaps any grandchildren (the launcher chain uses ``exec``, so run.py's PID is the one the host tracks). Falls back to ``proc.kill()`` when the group is already gone. """ if proc.poll() is not None: return if os.name == "nt": try: proc.kill() except OSError: try: sys.stderr.write("run.py: nt reap kill failed\n") sys.stderr.flush() except (OSError, ValueError): pass elif hasattr(os, "killpg"): try: os.killpg(os.getpgid(proc.pid), posix_sig) except (ProcessLookupError, OSError): try: proc.kill() except OSError: try: sys.stderr.write("run.py: posix reap kill failed\n") sys.stderr.flush() except (OSError, ValueError): pass else: try: proc.kill() except OSError: try: sys.stderr.write("run.py: fallback reap kill failed\n") sys.stderr.flush() except (OSError, ValueError): pass def _forward_and_exit(signum, frame): """Forward SIGTERM/SIGINT to the child, then exit. On Windows this handler only fires for console Ctrl+C or an in-process os.kill; an external TerminateProcess from the host bypasses Python handlers entirely. """ global _child_proc if _child_proc is not None: _reap(_child_proc, signal.SIGTERM) os._exit(0) # Diagnostics log for unexpected errors in the consent gate. Mirrors the # pattern in the hook runners: write to SNAPSHOT_DIR (never stderr, which # the host captures into the model's session context). Capped at 256 KB. _DIAGNOSTICS_LOG_NAME = "consent_diagnostics.log" _DIAGNOSTICS_LOG_CAP = 256 * 1024 def _consent_diagnostics_log_path(): """Resolve the diagnostics log path under measure.SNAPSHOT_DIR, or None.""" try: import measure base = getattr(measure, "SNAPSHOT_DIR", None) if base is None: return None from pathlib import Path as _P return _P(base) / _DIAGNOSTICS_LOG_NAME except Exception: return None def _consent_log_diagnostics(message): """Append a diagnostics chunk to the consent diagnostics log, capped.""" path = _consent_diagnostics_log_path() if path is None: return try: path.parent.mkdir(parents=True, exist_ok=True) with open(path, "a", encoding="utf-8") as f: f.write(message) if path.stat().st_size > _DIAGNOSTICS_LOG_CAP: data = path.read_bytes()[-_DIAGNOSTICS_LOG_CAP:] path.write_bytes(data) except OSError: pass def _check_consent(plugin_root: Path | None = None) -> bool: """Return True if consent is given or assumed. Fail-open on any error.""" try: home = Path.home() # Resolve config path from env (set by Claude Code before hook invocation) plugin_data = os.environ.get("CLAUDE_PLUGIN_DATA", "") config_path = None if plugin_data: pd = Path(plugin_data).resolve() if not str(p - hooks/sessionstart_runner.pyGitHub
Read the script
#!/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`` [matcher compact] (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. # --------------------------------------------------------------------------- # _DIAGNOSTICS - hooks/stop_runner.pyGitHub
Read the script
#!/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``, async, timeout 60) joins this runner too. It is a separate hooks.json entry (different event, ``async: true``, timeout 60) but dispatches 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. - The ``async: true`` flag on the SessionEnd hooks.json entry is a host-level semantic (fire-and-forget); the runner itself is the same for both events. - 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 _is_session_end(hook_input: - hooks/userpromptsubmit_runner.pyGitHub
- hooks/windows-launcher.pyGitHub
All 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.
Repo: alexgreensh/token-optimizer

