Skip to content
Development
Hook

Hooks

What token-optimizer-mcp 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-mcp
4732 skills2 hooks1 MCP
Install
> /plugin marketplace add ooples/token-optimizer-mcp
> /plugin install token-optimizer@token-optimizer

Ships with token-optimizer-mcp. Installing the plugin gets these hooks.

What fires, and when

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.

  • node "${extensionPath}${/}hooks${/}gemini-token-optimizer-advisor.mjs" session-start

AfterTool

  • Matchesread_filenode "${extensionPath}${/}hooks${/}gemini-token-optimizer-advisor.mjs" after-read
Read hooks/hooks.json

Where it lives

  • hooks/dispatcher.ps1GitHub
    Read the script
    # Claude Code Hooks Dispatcher - Token Optimizer Edition
    # Minimal dispatcher focused on token optimization via MCP
    # Replaces 400+ line mess with clean architecture
    
    [CmdletBinding()]
    param([string]$Phase = "")
    
    # Resolve every path relative to THIS script so the hooks work for any user
    # and any install location. NEVER hardcode a developer profile — that breaks
    # the hooks for everyone but the original dev machine. dispatcher.ps1 lives
    # at the hooks root, so $PSScriptRoot is the hooks root.
    $HANDLERS_DIR = Join-Path $PSScriptRoot "handlers"
    $LOG_FILE = Join-Path $PSScriptRoot "logs\dispatcher.log"
    $ORCHESTRATOR = Join-Path $HANDLERS_DIR "token-optimizer-orchestrator.ps1"
    
    # Load the shared logging helper defensively: a missing/malformed helper
    # must not kill the dispatcher for every hook phase. Fall back to a
    # minimal Write-Log shim so the rest of the script still runs.
    $loggingHelperPath = "$PSScriptRoot\helpers\logging.ps1"
    try {
        if (Test-Path $loggingHelperPath) {
            . $loggingHelperPath
        } else {
            throw "logging helper not found at $loggingHelperPath"
        }
    } catch {
        function Write-Log { param([string]$Message, [string]$Level = 'INFO') $null = $Message; $null = $Level }
        function Handle-Error { param($Exception, [string]$Message) $null = $Exception; $null = $Message }
    }
    
    
    
    function Block-Tool {
        param([string]$Reason)
    
        Write-Log "[BLOCK] $Reason"
    
        $blockResponse = @{
            continue = $false
            stopReason = $Reason
            hookSpecificOutput = @{
                hookEventName = $Phase
                permissionDecision = "deny"
                permissionDecisionReason = $Reason
            }
        } | ConvertTo-Json -Depth 10 -Compress
    
        Write-Output $blockResponse
        exit 2
    }
    
    try {
        # Read JSON input from stdin
        $input_json = [Console]::In.ReadToEnd()
    
        if (-not $input_json) {
            Write-Log "No JSON input"
            exit 0
        }
    
        # DEBUG: Log raw stdin length and first 100 chars
        Write-Log "DEBUG: stdin length=$($input_json.Length), preview=$($input_json.Substring(0, [Math]::Min(100, $input_json.Length)))"
    
        $data = $input_json | ConvertFrom-Json
        $toolName = $data.tool_name
    
        Write-Log "Tool: $toolName"
    
        # Write JSON to temp file to avoid command-line length limits
        $tempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "hook-input-$([guid]::NewGuid().ToString()).json")
        [System.IO.File]::WriteAllText($tempFile, $input_json, [System.Text.Encoding]::UTF8)
    
        # ============================================================
        # PHASE: PreToolUse
        # ============================================================
        if ($Phase -eq "PreToolUse") {
    
            # NOTE: transparent smart_read substitution for the BUILT-IN Read tool
            # is impossible — PreToolUse cannot replace output, and PostToolUse
            # `updatedToolOutput` is ignored for built-in tools
            # (anthropics/claude-code#32105). Transparent substitution only works
            # for the MCP file-read tools (handled in PostToolUse below).
    
            # 1. OPT-IN large-Read redirect (built-in Read only, OFF by default).
            #    Since we cannot compress a built-in Read's output, the next best
            #    thing is to steer big reads to the smart_read MCP tool, whose
            #    output IS compressed (cached/diffed/truncated). Enable by setting
            #    TOKEN_OPTIMIZER_REDIRECT_LARGE_READS=true. Off by default so it
            #    never disrupts normal edit workflows.
            if ($toolName -eq "Read" -and $env:TOKEN_OPTIMIZER_REDIRECT_LARGE_READS -eq 'true') {
                $readPath = $data.tool_input.file_path
                if ($readPath -and (Test-Path -LiteralPath $readPath -PathType Leaf)) {
                    # Threshold in bytes (default 51200 = 50KB); configurable.
                    $thresholdBytes = 51200
                    if ($env:TOKEN_OPTIMIZER_LARGE_READ_BYTES) {
                        [int]::TryParse($env:TOKEN_OPTIMIZER_LARGE_READ_BYTES, [ref]$thresholdBytes) | Out-Null
                    }
                    $sizeBytes = (Get-Item -LiteralPath $readPath).Length
                    if ($sizeBytes -ge $thresholdBytes) {
                        Write-Log "[REDIRECT] Large Read ($sizeBytes bytes) -> smart_read: $readPath"
                        Block-Tool -Reason "This file is large ($([Math]::Round($sizeBytes/1KB)) KB). Use the smart_read MCP tool (smart_read with path='$readPath') for a token-optimized, cached/diffed read instead of the built-in Read."
                    }
                }
            }
    
            # 2. Context Guard - Check if we're approaching token limit
            & powershell -NoProfile -ExecutionPolicy Bypass -File $ORCHESTRATOR -Phase "PreToolUse" -Action "context-guard" -InputJsonFile $tempFile
            if ($LASTEXITCODE -eq 2) {
                Block-Tool -Reason "Context budget exhausted - session optimization required"
            }
    
            # 3. Track operation
            & powershell -NoProfile -ExecutionPolicy Bypass -File $ORCHESTRATOR -Phase "PreToolUse" -Action "session-track" -InputJsonFile $tempFile
    
            # 4. MCP Enforcers - Force usage of MCP tools over Bash/Read/Grep
    
            # Git MCP Enforcer - Allow local operations, enforce MCP for remote operations
            if ($toolName -eq "Bash" -and $data.tool_input.command -match "git\s") {
                # Allow local git operations that GitHub MCP cannot perform
                $localGitOps = @(
                    "git\s+status",           # Check working directory status
                    "git\s+branch",           # List/create/delete branches
                    "git\s+checkout",         # Switch branches
                    "git\s+worktree",         # Manage worktrees (critical for agent coordination)
                    "git\s+add",              # Stage files
                    "git\s+commit",           # Create commits
                    "git\s+diff",             # Show changes
                    "git\s+log",              # View history
                    "git\s+stash",            # Stash changes
                    "git\s+p
  • hooks/gemini-token-optimizer-advisor.mjsGitHub
    Read the script
    #!/usr/bin/env node
    
    import { statSync } from 'node:fs';
    import { isAbsolute, resolve } from 'node:path';
    
    const mode = process.argv[2];
    const threshold =
      Number(process.env.TOKEN_OPTIMIZER_LARGE_READ_BYTES) || 25_600;
    const redirect = process.env.TOKEN_OPTIMIZER_REDIRECT_LARGE_READS === 'true';
    
    const guidance =
      'Use the token-optimizer MCP for large or repeated operations: smart_read for large/repeated files, smart_glob or smart_grep for noisy searches, optimize_text for bulky output, optimize_session before context gets tight, and get_optimization_report for savings. Built-in tools remain appropriate for small one-off operations.';
    
    function readStdin() {
      return new Promise((resolveInput) => {
        let input = '';
        process.stdin.setEncoding('utf8');
        process.stdin.on('data', (chunk) => (input += chunk));
        process.stdin.on('end', () => resolveInput(input));
        process.stdin.on('error', () => resolveInput(input));
      });
    }
    
    function isPartialRead(args) {
      return ['offset', 'limit', 'start_line', 'end_line'].some(
        (key) => args[key] !== undefined
      );
    }
    
    const raw = await readStdin();
    let payload;
    try {
      payload = JSON.parse(raw);
    } catch {
      process.exit(0);
    }
    
    if (mode === 'session-start') {
      process.stdout.write(
        JSON.stringify({ hookSpecificOutput: { additionalContext: guidance } })
      );
      process.exit(0);
    }
    
    if (mode !== 'after-read' || payload?.tool_name !== 'read_file') {
      process.exit(0);
    }
    
    const args = payload.tool_input ?? {};
    if (isPartialRead(args)) process.exit(0);
    
    const requestedPath = args.file_path;
    if (typeof requestedPath !== 'string' || requestedPath.length === 0) {
      process.exit(0);
    }
    
    const absolutePath = isAbsolute(requestedPath)
      ? requestedPath
      : resolve(payload.cwd || process.cwd(), requestedPath);
    
    let size;
    try {
      const stats = statSync(absolutePath);
      if (!stats.isFile() || stats.size < threshold) process.exit(0);
      size = stats.size;
    } catch {
      process.exit(0);
    }
    
    const kb = Math.round(size / 1024);
    const message = `${absolutePath} is ${kb} KB. Token Optimizer can cache it and return only diffs on repeat reads.`;
    
    if (redirect) {
      process.stdout.write(
        JSON.stringify({
          hookSpecificOutput: {
            tailToolCallRequest: {
              name: 'mcp_token-optimizer_smart_read',
              args: { path: absolutePath },
            },
          },
        })
      );
    } else {
      process.stdout.write(
        JSON.stringify({
          hookSpecificOutput: {
            additionalContext: `Token Optimizer suggestion: ${message} Use smart_read for this file on the next read.`,
          },
        })
      );
    }
    
  • hooks/read-cache-interceptor.ps1GitHub
    Read the script
    # Read Cache Interceptor Handler
    # Implements real-time caching for Read tool operations to save 250-350K tokens
    # Strategy: Two-tier caching with in-memory hashtable + persistent JSON file
    # Cache invalidation: LastWriteTime check on every cache hit
    
    param([string]$Phase = "PreToolUse")
    
    $CACHE_FILE = "C:\Users\yolan\.claude-global\hooks\data\read-cache.json"
    $CACHE_DIR = "C:\Users\yolan\.claude-global\hooks\data"
    $LOG_FILE = "C:\Users\yolan\.claude-global\hooks\logs\read-cache.log"
    
    # Initialize global cache if not already loaded
    if (-not $global:ReadCache) {
        $global:ReadCache = @{}
        $global:CacheDirty = $false
        $global:CacheStats = @{
            Hits = 0
            Misses = 0
            Stale = 0
            TokensSaved = 0
        }
    }
    
    function Write-CacheLog {
        param([string]$Message, [string]$Level = "INFO")
        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        $logEntry = "[$timestamp] [$Level] $Message"
        try {
            $logEntry | Out-File -FilePath $LOG_FILE -Append -Encoding UTF8 -ErrorAction SilentlyContinue
        } catch {
            # Silently fail if log write fails
        }
    }
    
    function Get-CanonicalPath {
        param([string]$Path)
        try {
            # Resolve-Path handles relative paths, symlinks, etc.
            # ProviderPath ensures we get filesystem path
            # ToLower() standardizes case for Windows
            $resolved = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).ProviderPath.ToLower()
            return $resolved
        } catch {
            # If path doesn't exist yet, normalize what we have
            return [System.IO.Path]::GetFullPath($Path).ToLower()
        }
    }
    
    function Load-PersistentCache {
        if (Test-Path $CACHE_FILE) {
            try {
                $jsonContent = Get-Content $CACHE_FILE -Raw -ErrorAction Stop
                $deserialized = $jsonContent | ConvertFrom-Json
    
                # Convert to hashtable for fast lookups
                $newCache = @{}
                foreach ($item in $deserialized) {
                    # Parse LastWriteTime as DateTime
                    $lastWriteTime = [DateTime]::Parse($item.Value.LastWriteTime)
    
                    $newCache[$item.Key] = @{
                        Content = $item.Value.Content
                        LastWriteTime = $lastWriteTime
                        Tokens = $item.Value.Tokens
                        OriginalSize = $item.Value.OriginalSize
                        AccessCount = $item.Value.AccessCount
                        FirstAccessed = [DateTime]::Parse($item.Value.FirstAccessed)
                    }
                }
    
                $global:ReadCache = $newCache
                Write-CacheLog "Loaded $($global:ReadCache.Count) items from persistent cache"
            } catch {
                Write-CacheLog "Failed to load persistent cache: $($_.Exception.Message)" "WARN"
                $global:ReadCache = @{}
            }
        }
    }
    
    function Save-PersistentCache {
        if ($global:CacheDirty) {
            try {
                # Ensure directory exists
                if (-not (Test-Path $CACHE_DIR)) {
                    New-Item -ItemType Directory -Path $CACHE_DIR -Force | Out-Null
                }
    
                # Convert hashtable to array for JSON serialization
                $savable = $global:ReadCache.GetEnumerator() | ForEach-Object {
                    @{
                        Key = $_.Key
                        Value = @{
                            Content = $_.Value.Content
                            LastWriteTime = $_.Value.LastWriteTime.ToString("o")
                            Tokens = $_.Value.Tokens
                            OriginalSize = $_.Value.OriginalSize
                            AccessCount = $_.Value.AccessCount
                            FirstAccessed = $_.Value.FirstAccessed.ToString("o")
                        }
                    }
                }
    
                $savable | ConvertTo-Json -Depth 10 | Out-File $CACHE_FILE -Encoding UTF8
                $global:CacheDirty = $false
                Write-CacheLog "Saved cache to disk ($($global:ReadCache.Count) items)"
            } catch {
                Write-CacheLog "Failed to save persistent cache: $($_.Exception.Message)" "WARN"
            }
        }
    }
    
    function Get-TokenCount {
        param([string]$Content)
        # Approximate token count: ~4 chars per token for English text
        return [Math]::Ceiling($Content.Length / 4)
    }
    
    try {
        # Read JSON input from stdin
        $input_json = [Console]::In.ReadToEnd()
    
        if (-not $input_json) {
            Write-CacheLog "No JSON input received" "ERROR"
            exit 0
        }
    
        $data = $input_json | ConvertFrom-Json
        $toolName = $data.tool_name
    
        # Only handle Read tool
        if ($toolName -ne "Read") {
            exit 0
        }
    
        # Load persistent cache on first Read operation
        if ($global:ReadCache.Count -eq 0) {
            Load-PersistentCache
        }
    
        # Extract file path from tool input
        $filePath = $data.tool_input.file_path
    
        if (-not $filePath) {
            Write-CacheLog "No file_path in Read tool input" "WARN"
            exit 0
        }
    
        $canonicalPath = Get-CanonicalPath -Path $filePath
        Write-CacheLog "Processing Read for: $canonicalPath"
    
        if ($Phase -eq "PreToolUse") {
            # ===== CACHE CHECK LOGIC =====
    
            if ($global:ReadCache.ContainsKey($canonicalPath)) {
                $cachedItem = $global:ReadCache[$canonicalPath]
    
                # Check if file still exists
                if (-not (Test-Path -LiteralPath $filePath)) {
                    Write-CacheLog "CACHE INVALID: File no longer exists: $canonicalPath" "WARN"
                    $global:ReadCache.Remove($canonicalPath)
                    $global:CacheDirty = $true
                    exit 0  # Allow Read to proceed and fail naturally
                }
    
                $fileInfo = Get-Item -LiteralPath $filePath -ErrorAction Stop
    
                # ===== CACHE INVALIDATION CHECK =====
                if ($fileInfo.LastWriteTime -le $cachedItem.LastWriteTime) {
                    # CACHE HIT - Return cached content and BLOCK Read tool
                    $global:CacheStats.Hits++
                    $cachedItem.AccessCount++
                    $global:CacheDirty = $true
    
                    $tokensSaved =

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-mcp

Intelligent token optimization for Claude Code - achieving 95%+ token reduction through caching, compression, and smart tool intelligence

Get the whole plugin
Stats
475
Stars
51
Forks
Active
Maintenance
JavaScript
Language
MIT
License
4h ago
Last commit
10mo ago
Created

Repo: ooples/token-optimizer-mcp