Skip to content
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.

From plugin
toonify-mcp
661 hook
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

  • MatchesRead|Grep|Glob|WebFetch|Bashnode ${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.mjs
Read hooks/hooks.json

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