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}${/}integrations${/}gemini${/}hooks${/}session-start.mjs"
BeforeTool
- Matches
read_file|search_file_content|glob|replace|write_file|run_shell_node "${extensionPath}${/}integrations${/}gemini${/}hooks${/}pre-tool.mjs"
AfterTool
- Matches
replace|write_file|run_shell_commandnode "${extensionPath}${/}integrations${/}gemini${/}hooks${/}post-tool.mjs"
AfterAgent
node "${extensionPath}${/}integrations${/}gemini${/}hooks${/}stop.mjs"
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. Default-on large-Read redirect (built-in Read only). # 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). Match the native # hook contract: enforce by default, with an explicit advise/off # escape hatch for users who need the built-in tool temporarily. $optimizerMode = if ($env:TOKEN_OPTIMIZER_MODE) { $env:TOKEN_OPTIMIZER_MODE.Trim().ToLowerInvariant() } else { 'enforce' } $enforceLargeReads = $optimizerMode -notin @('advise', 'off') if ($toolName -eq "Read" -and $enforceLargeReads) { $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. Set TOKEN_OPTIMIZER_MODE=off to disable enforcement." } } } # 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", - 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
Measure token savings per AI coding agent, optimize context, and share a live local knowledge graph across 16 CLI clients.
Get the whole plugin
Stats
536
Stars
65
Forks
Active
Maintenance
JavaScript
Language
MIT
License
13h ago
Last commit
11mo ago
Created
Repo: ooples/token-optimizer-mcp

