Development
Hook
Hooks
What toonify-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 PCIRCLE-AI/toonify-mcp > /plugin install toonify-mcp@pcircle-ai
Ships with toonify-mcp. Installing the plugin gets these hooks.
What fires, and when
PostToolUse
- Matches
Read|Grep|Glob|WebFetch|Bashnode ${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.mjs
Where it lives
- hooks/post-tool-use.mjsRunsGitHub
Read the script
#!/usr/bin/env node /** * Toonify PostToolUse Hook * * Intercepts tool results from Read, Grep, Glob, WebFetch, Bash and: * - Converts structured data (JSON/YAML) to TOON format * - Collapses repetitive debug output (test failures, stack traces, diagnostics) * * Source code is NOT compressed: the comment-stripping heuristics truncated * regex literals such as /^https?:\/\// at the `//`, producing code that no * longer parses. See src/optimizer/compressors/code.ts for the full rationale. * * CSV is not converted: TOON measured as a net loss on CSV at every size. * * tool_response is a structured object for the tools this hook matches, not * a plain string — see extractText() below for the shapes verified (Read, * WebFetch, Grep's 'content'/'count' modes, and Bash's combined `stdout` * field, each per the tool's own schema). * * Compressed content ALWAYS goes out via hookSpecificOutput.updatedToolOutput, * which REPLACES what Claude sees — that is what actually shrinks context. * The hook never uses additionalContext: it APPENDS (verified live that * `suppressOutput` doesn't hide the original), so it would grow total context * instead of shrinking it. For an object tool_response the replacement is the * object with its text field swapped; for a (compat-only) string * tool_response it's the compressed string itself. Where tool_response is an * OBJECT with no compressible field for that tool (Grep's default * 'files_with_matches' mode, Glob, an unmatched tool), the hook passes * through untouched — there is nothing to safely replace. * * Input: JSON on stdin with { tool_name, tool_response, ... } * Output: JSON on stdout with { continue, hookSpecificOutput } */ import { encode as toonEncode } from '@toon-format/toon'; import { parse as yamlParse } from 'yaml'; import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; // --- Configuration --- /** * Maximum content size to attempt JSON.parse/yamlParse/detection on (10 MB). * Mirrors MAX_CONTENT_SIZE in src/optimizer/token-optimizer.ts — this hook * has its own detection pipeline (see detectStructuredData/detectDebugOutput * below) and needs the same DoS-prevention ceiling; without it, arbitrarily * large Read/WebFetch content gets parsed synchronously with no bound. */ const MAX_CONTENT_SIZE = 10 * 1024 * 1024; const DEFAULT_CONFIG = { enabled: true, minTokensThreshold: 50, minSavingsThreshold: 30, // Write/Edit are mutations — their result (success flag / diff) isn't // compressible content and altering it could mislead Claude. Bash is NOT // skipped: its stdout is a large token sink (curl/jq JSON, test-runner and // build logs) that the same selective detect→compress path handles safely // (see the Bash branch in extractText). Small command output stays below // the token threshold and passes through untouched. skipToolPatterns: ['Write', 'Edit'], }; function loadConfig() { const configPath = join(homedir(), '.claude', 'toonify-config.json'); let fileConfig = {}; if (existsSync(configPath)) { try { fileConfig = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { // Ignore invalid config } } // Environment variable overrides const envConfig = {}; if (process.env.TOONIFY_ENABLED !== undefined) { envConfig.enabled = process.env.TOONIFY_ENABLED !== 'false'; } if (process.env.TOONIFY_MIN_TOKENS) { envConfig.minTokensThreshold = parseInt(process.env.TOONIFY_MIN_TOKENS, 10); } if (process.env.TOONIFY_MIN_SAVINGS) { envConfig.minSavingsThreshold = parseInt(process.env.TOONIFY_MIN_SAVINGS, 10); } if (process.env.TOONIFY_SKIP_TOOLS) { envConfig.skipToolPatterns = process.env.TOONIFY_SKIP_TOOLS.split(','); } return { ...DEFAULT_CONFIG, ...fileConfig, ...envConfig }; } // --- Structured Data Detection --- function detectStructuredData(content) { // Try JSON try { const data = JSON.parse(content); if (typeof data === 'object' && data !== null) { return { type: 'json', data }; } } catch { // Not JSON } // Try YAML — gated on STRUCTURE (looksLikeYAML), then accept any non-null // object. The old gate (`Object.keys(data).length > 1`) silently skipped the // most valuable and most common YAML shape: a single top-level key holding a // large uniform list (`services:`, a k8s `spec:`, `data:`), which is exactly // what TOON compresses best. The structural density check (looksLikeYAML) is // what keeps prose out, so the key-count gate isn't needed for that job. // Mirrors Detector.tryYAML / looksLikeYAML in src/optimizer/pipeline/detector.ts. if (looksLikeYAML(content)) { try { const data = yamlParse(content); if (typeof data === 'object' && data !== null) { return { type: 'yaml', data }; } } catch { // Not YAML } } return null; } // Structural YAML heuristic — density of `key: value` / list / indented lines. // This is what keeps prose (which yaml.parse would happily coerce into some // object) from being misdetected, so detectStructuredData can drop the old // top-level-key-count gate. Ported from looksLikeYAML in // src/optimizer/pipeline/detector.ts — keep the two in sync. Each scanned line // is length-capped first (capLine), matching the hook's ReDoS discipline for // every other line-scanning predicate here. function looksLikeYAML(content) { const lines = content.split('\n').filter(l => l.trim()); if (lines.length < 3) return false; const yamlLinePattern = /^\s*[\w][\w\s.-]*:\s*.+/; const listItemPattern = /^\s*-\s+/; const indentedPattern = /^\s{2,}\S/; let yamlLines = 0; let listItems = 0; let indented = 0; for (const raw of lines.slice(0, 20)) { const line = capLine(raw); if (yamlLinePattern.test(line)) yamlLines++; if (listItemPattern.test(line)) listItems++; if (indentedPattern.test(line)) indented++; } const hasStructure = (
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 withtoonify-mcp
Context compression plugin for Claude Code. Automatically trims large tool output—JSON, YAML, stack traces, and logs—before it enters the context window. Works as a Claude Code plugin (automatic, zero-config) or as an MCP server (on-demand).
Get the whole plugin
Stats
66
Stars
11
Forks
Maintained
Maintenance
TypeScript
Language
MIT
License
1mo ago
Last commit
8mo ago
Created
Repo: PCIRCLE-AI/toonify-mcp

