cd /news/ai-agents/claude-code-hooks-reject-a-broken-js… · home topics ai-agents article
[ARTICLE · art-138912] src=somethingbig.ai ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Claude Code hooks: reject a broken JSON write before it lands

A Claude Code PreToolUse hook can reject malformed JSON before a Write tool call lands, using a standard-library Python script at .claude/hooks/validate-json-write.py that returns exit code 2 for invalid content. The script parses the proposed file content and rejects NaN and Infinity via an explicit constant check, while passing through non-JSON files and other tools; a test harness confirms exit code 0 for valid JSON and 2 for broken JSON and non-JSON numbers. The hook does not enforce an application schema and does not catch Edit calls, shell writes, or changes made outside Claude Code.

read4 min views1 publishedSep 24, 2026
Claude Code hooks: reject a broken JSON write before it lands
Image: Somethingbig (auto-discovered)

A Claude Code hook is useful when you can describe a check more precisely in code than in a reminder. This example prevents the Write tool from replacing a JSON file with malformed JSON. The check happens before the write, so the existing file does not have to become broken before you notice.

The scope is deliberately narrow: complete-file writes through Claude Code's Write tool. It does not validate every possible editor, shell command or patch. You can test it without asking a model to change a real project.

Build the check before connecting the hook #

In a scratch project, save this as .claude/hooks/validate-json-write.py. It uses only Python's standard library.

#!/usr/bin/env python3
import json
import sys

def reject_constant(value):
    raise ValueError("Non-JSON constant: " + value)

try:
    event = json.load(sys.stdin)
    tool = event.get("tool_input", {})
    filename = tool.get("file_path", "")
    if event.get("tool_name") != "Write" or not filename.lower().endswith(".json"):
        raise SystemExit(0)
    json.loads(tool.get("content", ""), parse_constant=reject_constant)
except (ValueError, TypeError, AttributeError):
    print("JSON write rejected: provide valid JSON before writing this file.", file=sys.stderr)
    raise SystemExit(2)

The script reads the proposed tool call from standard input. For a JSON-file write, it parses the proposed content. Invalid content returns exit code 2 with a short explanation. Other file types and other tools pass through. The explicit constant check rejects NaN and Infinity, which Python's parser otherwise accepts even though they are not JSON numbers.

Connect it to the Write event #

Add this entry to .claude/settings.json. If you already have settings or hooks, merge the entry; do not replace the whole file. These command examples use a macOS/Linux shell and python3.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-json-write.py\""
          }
        ]
      }
    ]
  }
}

PreToolUse is the timing; Write is the tool matcher. The command points to the project-local script and quotes the path so a project directory containing spaces still works. Review the configuration before enabling it: a hook runs a command on your machine, not merely a suggestion inside a prompt.

Prove both the rejection and the pass #

Run this from the scratch project's root:

python3 - <<'CHECK'
import json, subprocess
cases = [('valid', '{"ok":true}', 0), ('broken', '{bad', 2),
         ('non-JSON number', '{"score":NaN}', 2)]
for name, content, expected in cases:
    event = {"tool_name": "Write", "tool_input": {
        "file_path": "demo.json", "content": content}}
    result = subprocess.run(
        ["python3", ".claude/hooks/validate-json-write.py"],
        input=json.dumps(event), text=True, capture_output=True)
    assert result.returncode == expected, (name, result.stderr)
    print(name, result.returncode)
CHECK

Expected output is valid 0, broken 2 and non-JSON number 2. This proves the script's decision logic. To test the integration, open Claude Code in that scratch project and ask it to write demo.json with the deliberately invalid content {bad, without repairing it after rejection. Inspect the tool result and confirm the file was not written. Then try a valid document.

Know exactly what this hook does not guarantee #

A valid JSON document can still be the wrong configuration. This check accepts a JSON string, array or object; it does not enforce your application's schema. It also does not catch an Edit call that changes part of an existing file, a shell command that writes a file, or a change made outside Claude Code.

Keep your normal validation in tests or CI. If the real requirement is “every committed configuration matches our schema,” enforce that at the commit or build boundary. The hook is an earlier, faster feedback step for one common write path.

Keep failure messages actionable #

“Provide valid JSON before writing this file” names a fix. A long generic warning makes the tool result harder to use. If a check needs a schema, report the failing field and the schema command to rerun. Avoid printing file contents into a shared log just to explain a syntax error.

When a hook behaves unexpectedly, first send it a saved synthetic payload. Then inspect Claude Code's hook/debug output. That separates a bad matcher or path from a bug in the validation script. Disable or remove only this entry if you need to undo the experiment.

What we checked: Six direct hook fixtures passed, including malformed JSON, NaN and the intentionally uncovered Edit path. In Claude Code 2.1.281, an actual Write of malformed JSON was blocked and left no file; a subsequent valid JSON Write succeeded. All writes were confined to a disposable project.

For guidance rather than an executable check, use project memory. For a reusable procedure you choose to run, build a skill.

Sources and version notes #

Checked against the current documentation on September 24, 2026. Command availability can vary with your installed version; check claude --version.

── more in #ai-agents 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-code-hooks-re…] indexed:0 read:4min 2026-09-24 ·