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. 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. python /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: python 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 https://somethingbig.ai/work/claude-code-memory . For a reusable procedure you choose to run, build a skill https://somethingbig.ai/work/claude-code-skills . Sources and version notes Checked against the current documentation on September 24, 2026. Command availability can vary with your installed version; check claude --version .