Skip to content
Agent Memory
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
3k5 skills2 commands12 hooks

Claude Code

> /plugin marketplace add alexgreensh/token-optimizer
> /plugin install token-optimizer@alexgreensh-token-optimizer

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; 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)";
  • 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; 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)";
  • 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; 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

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

  • MatchesBash|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)";
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 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
  • 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 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
  • hooks/python-launcher.shRunsGitHub
    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
    # 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
        [
  • hooks/run.pyGitHub
    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 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
  • 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``    [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
  • 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``, 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 t
  • 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.

@alexgreensh
Ships withtoken-optimizer

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

Stars on GitHub

Momentum

stars in total
3.0k
Star history of alexgreensh/token-optimizer on GitHub, a chart by star-history.com

More plugins that ship hooks.

mempalace

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.

3 skills+361k
@mempalace@mempalace
Install Plugin
mempalace

mempalace

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.

3 skills+360k
@milla-jovovich@milla-jovovich
Install Plugin
context-mode

context-mode

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.

4 skills4 hooks34k
@mksglu@mksglu
Install Plugin
mksglu-context-mode

mksglu-context-mode

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.

9 skills+233k
@mksglu@mksglu
Install Plugin
ai-memory

ai-memory

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.

7 skills+216k
@akitaonrails@akitaonrails
Install Plugin
letta-code

letta-code

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.

26 skills8 hooks3.8k
@letta-ai@letta-ai
Install Plugin
claude-subconscious

claude-subconscious

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.

4 hooks2.9k
@letta-ai@letta-ai
Install Plugin
claude-reflect

claude-reflect

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.

1 skill+21.9k
@bayramannakov@bayramannakov
Install Plugin

Questions about the hooks in token-optimizer.

Which coding agents can use the hooks in token-optimizer?
The hooks in token-optimizer install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.