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.
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
- Matches
read_filenode "${extensionPath}${/}hooks${/}gemini-token-optimizer-advisor.mjs" after-read
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

