Skip to content
Development
Hook

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.

From plugin
token-optimizer
2.3k5 skills2 commands11 hooks
Install
> /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

  • MatchesReadfor 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
  • MatchesBashfor 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
  • MatchesAgent|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
  • Matchesmcp__.*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 d
  • 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
  • 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

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

  • MatchesBash|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

  • MatchesBashfor 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
Read hooks/hooks.json

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.

Ships withtoken-optimizer

Find the ghost tokens. Fix them. Survive compaction. Avoid context quality decay.

Get the whole plugin
Stats
2,359
Stars
182
Forks
Active
Maintenance
Python
Language
13h ago
Last commit
6mo ago
Created

Repo: alexgreensh/token-optimizer