{"slug": "claude-code-hooks-reject-a-broken-json-write-before-it-lands", "title": "Claude Code hooks: reject a broken JSON write before it lands", "summary": "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.", "body_md": "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.\n\nThe 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.\n\n## Build the check before connecting the hook\n\nIn a scratch project, save this as `.claude/hooks/validate-json-write.py`. It uses only Python's standard library.\n\n``` python\n#!/usr/bin/env python3\nimport json\nimport sys\n\ndef reject_constant(value):\n    raise ValueError(\"Non-JSON constant: \" + value)\n\ntry:\n    event = json.load(sys.stdin)\n    tool = event.get(\"tool_input\", {})\n    filename = tool.get(\"file_path\", \"\")\n    if event.get(\"tool_name\") != \"Write\" or not filename.lower().endswith(\".json\"):\n        raise SystemExit(0)\n    json.loads(tool.get(\"content\", \"\"), parse_constant=reject_constant)\nexcept (ValueError, TypeError, AttributeError):\n    print(\"JSON write rejected: provide valid JSON before writing this file.\", file=sys.stderr)\n    raise SystemExit(2)\n```\n\nThe 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.\n\n## Connect it to the Write event\n\nAdd 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`.\n\n```\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"Write\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"python3 \\\"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-json-write.py\\\"\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n`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.\n\n## Prove both the rejection and the pass\n\nRun this from the scratch project's root:\n\n``` python\npython3 - <<'CHECK'\nimport json, subprocess\ncases = [('valid', '{\"ok\":true}', 0), ('broken', '{bad', 2),\n         ('non-JSON number', '{\"score\":NaN}', 2)]\nfor name, content, expected in cases:\n    event = {\"tool_name\": \"Write\", \"tool_input\": {\n        \"file_path\": \"demo.json\", \"content\": content}}\n    result = subprocess.run(\n        [\"python3\", \".claude/hooks/validate-json-write.py\"],\n        input=json.dumps(event), text=True, capture_output=True)\n    assert result.returncode == expected, (name, result.stderr)\n    print(name, result.returncode)\nCHECK\n```\n\nExpected 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.\n\n## Know exactly what this hook does not guarantee\n\nA 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.\n\nKeep 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.\n\n## Keep failure messages actionable\n\n“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.\n\nWhen 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.\n\n**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.\n\nFor 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).\n\n## Sources and version notes\n\nChecked against the current documentation on September 24, 2026. Command availability can vary with your installed version; check `claude --version`.", "url": "https://wpnews.pro/news/claude-code-hooks-reject-a-broken-json-write-before-it-lands", "canonical_source": "https://somethingbig.ai/work/claude-code-hooks", "published_at": "2026-09-24 00:00:00+00:00", "updated_at": "2026-09-24 07:31:07.756724+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Claude Code", "Anthropic", "Write", "PreToolUse", "Python"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-code-hooks-reject-a-broken-json-write-before-it-lands", "markdown": "https://wpnews.pro/news/claude-code-hooks-reject-a-broken-json-write-before-it-lands.md", "text": "https://wpnews.pro/news/claude-code-hooks-reject-a-broken-json-write-before-it-lands.txt", "jsonld": "https://wpnews.pro/news/claude-code-hooks-reject-a-broken-json-write-before-it-lands.jsonld"}}