Skip to content

Instantly share code, notes, and snippets.

@maman
Created October 27, 2025 09:35
Show Gist options
  • Save maman/de31d48cd960366ce9caec32b569d32a to your computer and use it in GitHub Desktop.
Save maman/de31d48cd960366ce9caec32b569d32a to your computer and use it in GitHub Desktop.

Claude Code JSON-to-TOON Hook

Automatically converts JSON in your Claude Code prompts to TOON format for 30-60% token savings.

What is TOON?

TOON (Token-Oriented Object Notation) is a compact data format optimized for LLMs. Instead of repeating field names in every object, TOON declares them once and streams values as rows.

Example:

JSON (117 tokens):

{
  "products": [
    {"sku": "A123", "name": "Widget", "price": 9.99},
    {"sku": "B456", "name": "Gadget", "price": 19.99}
  ]
}

TOON (49 tokens - 58% reduction):

products[2]{sku,name,price}:
  A123,Widget,9.99
  B456,Gadget,19.99

Features

  • ✅ Automatically detects and converts JSON in prompts
  • ✅ Handles JSON code blocks (```json)
  • ✅ Handles inline JSON objects/arrays
  • ✅ Smart detection: preserves JavaScript/TypeScript code
  • ✅ Safe: fails gracefully, never breaks prompts
  • ✅ Zero configuration after setup

Prerequisites

Installation

1. Create hooks directory

mkdir -p ~/.claude/hooks

2. Download the TOON library

curl -sL https://esm.sh/@byjohann/toon@latest/es2015/toon.mjs -o ~/.claude/hooks/toon.mjs

3. Download the hook script

curl -sL https://gist.githubusercontent.com/maman/de31d48cd960366ce9caec32b569d32a/raw/json-to-toon.mjs -o ~/.claude/hooks/json-to-toon.mjs
chmod +x ~/.claude/hooks/json-to-toon.mjs

4. Configure Claude Code

Add the following to your ~/.claude/settings.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node ~/.claude/hooks/json-to-toon.mjs"
          }
        ]
      }
    ]
  }
}

If your settings.json already has a hooks section, merge the UserPromptSubmit configuration into it.

How It Works

The hook runs automatically before each prompt is sent to Claude:

  1. Detects JSON in your prompt:

    • JSON code blocks with ```json identifier
    • Plain code blocks containing valid JSON
    • Inline JSON objects/arrays (30+ characters)
  2. Converts to TOON format using the toon library

  3. Preserves non-JSON content:

    • JavaScript/TypeScript code (detected via keywords)
    • Plain text
    • Other code blocks
  4. Sends modified prompt to Claude with reduced token count

Examples

Example 1: JSON Code Block

You type:

Analyze this data:
\```json
{
  "users": [
    {"id": 1, "name": "Alice", "active": true},
    {"id": 2, "name": "Bob", "active": false}
  ]
}
\```

Claude receives:

Analyze this data:
\```
users[2]{id,name,active}:
  1,Alice,true
  2,Bob,false
\```

Example 2: Inline JSON

You type:

Process this: {"products": [{"sku": "A123", "name": "Widget", "price": 9.99}], "total": 1}

Claude receives:

Process this: products[1]{sku,name,price}:
  A123,Widget,9.99
total: 1

Example 3: JavaScript Code (Not Converted)

You type:

function getData() {
  return { users: [] };
}

Claude receives: (unchanged)

function getData() {
  return { users: [] };
}

Troubleshooting

Hook not running?

  1. Verify Node.js is installed: node --version
  2. Check script exists: ls -lh ~/.claude/hooks/json-to-toon.mjs
  3. Check script is executable: chmod +x ~/.claude/hooks/json-to-toon.mjs
  4. Verify settings.json syntax is valid JSON

Test the hook manually:

echo '{"prompt": "Test: {\"key\": \"value\", \"items\": [{\"a\": 1}, {\"a\": 2}]}"}' | node ~/.claude/hooks/json-to-toon.mjs

Expected output:

Test: key: value
items[2]{a}:
  1
  2

Credits

License

MIT - Use freely, no attribution required.

#!/usr/bin/env node
import { encode } from './toon.mjs';
// Read input from stdin
let inputData = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
inputData += chunk;
});
process.stdin.on('end', () => {
try {
const input = JSON.parse(inputData);
const prompt = input.prompt || '';
// Process the prompt to find and replace JSON blocks
const processedPrompt = replaceJsonWithToon(prompt);
// Output the modified prompt
console.log(processedPrompt);
process.exit(0);
} catch (error) {
// If there's an error, output the original prompt unchanged
console.error(`Error in json-to-toon hook: ${error.message}`, { file: import.meta.url });
try {
const input = JSON.parse(inputData);
console.log(input.prompt || '');
} catch {
console.log('');
}
process.exit(0);
}
});
/**
* Replaces JSON blocks and inline JSON with TOON format
*/
function replaceJsonWithToon(text) {
// Pattern 1: Code blocks with json/JSON language identifier
text = text.replace(/```(?:json|JSON)\s*\n([\s\S]*?)\n```/g, (match, jsonContent) => {
return convertJsonToToon(jsonContent.trim(), true);
});
// Pattern 2: Code blocks without language identifier that contain valid JSON
text = text.replace(/```\s*\n([\s\S]*?)\n```/g, (match, content) => {
const trimmed = content.trim();
if (looksLikeJson(trimmed)) {
return convertJsonToToon(trimmed, true);
}
return match; // Keep original if not JSON
});
// Pattern 3: Inline JSON objects/arrays (be conservative to avoid false positives)
// Use a more robust approach: try to find complete JSON structures
text = text.replace(/(\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}|\[[^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*\])/g, (match) => {
// Only convert if it's valid JSON, looks like data (not code), and is substantial
if (match.length >= 30 && looksLikeJson(match) && !looksLikeCode(match)) {
return convertJsonToToon(match, false);
}
return match;
});
return text;
}
/**
* Attempts to parse and convert JSON to TOON format
*/
function convertJsonToToon(jsonString, isCodeBlock) {
try {
const parsed = JSON.parse(jsonString);
const toonEncoded = encode(parsed);
// If it was in a code block, return it in a code block
if (isCodeBlock) {
return '```\n' + toonEncoded + '\n```';
}
return toonEncoded;
} catch (error) {
// If parsing fails, return original
return isCodeBlock ? '```json\n' + jsonString + '\n```' : jsonString;
}
}
/**
* Checks if a string looks like JSON
*/
function looksLikeJson(str) {
const trimmed = str.trim();
if (!trimmed) return false;
// Must start with { or [
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
return false;
}
try {
JSON.parse(trimmed);
return true;
} catch {
return false;
}
}
/**
* Checks if JSON looks like code rather than data
* (heuristic to avoid converting JavaScript/TypeScript code)
*/
function looksLikeCode(str) {
// If it contains function keyword, arrow functions, or common JS patterns
const codePatterns = [
/function\s*\(/,
/=>\s*{/,
/\bconst\b/,
/\blet\b/,
/\bvar\b/,
/\bif\s*\(/,
/\bfor\s*\(/,
/\bwhile\s*\(/,
];
return codePatterns.some(pattern => pattern.test(str));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment