Edit tool false-positive JSON validation failure on .claude/settings.json hook commands
Description
Editing .claude/settings.json via the Edit tool repeatedly fails with a false-positive JSON validation error when the edit touches a hook command string containing a single-quoted regex with . and $ metacharacters (e.g. '.ts$'). The error is:
Claude Code settings.json validation failed after edit:
Invalid JSON: JSON Parse error: Unterminated string
The resulting file (or the file prior to the failed edit) is valid JSON in every case I checked — verified independently with jq -e '<path>' .claude/settings.json, which succeeded both before and after. The Edit tool's own post-edit validator appears to trip during its diff/patch-application step rather than on the final serialized content, since the same string, written directly via a node -e "JSON.parse(...); ...; JSON.stringify(...)" + file write, passes validation immediately with no changes to the string's content.
Repro
- Have a
.claude/settings.jsonwith an existinghooks.PostToolUsearray. - Use
Editto add a new hook entry whosecommandvalue is:
````
if git diff --name-only | grep -q '.ts$'; then npx tsc --noEmit; fi 2>/dev/null || true
- The edit fails with "Unterminated string", and is rolled back (file left unchanged — no corruption).
- Retrying the identical Edit call fails again the same way.
- Writing the exact same target content via a Node script (
JSON.parsethe file, mutate thecommandfield,JSON.stringifyand write back) succeeds and passes the post-edit validator with no errors.
Impact
- Each failed attempt re-sends the full embedded JSON schema for
.claude/settings.json(very large) back to the model as part of the tool-error payload, burning a significant number of tokens per retry. - It also produces a confusing user-facing experience: repeated identical-looking failures on a file that is, in fact, valid JSON.
Workaround
Skip the Edit tool for .claude/settings.json hook edits involving shell metacharacters/regex in command strings; instead read the file, mutate it via a node -e script using JSON.parse/JSON.stringify, then verify with jq -e '<path>' .claude/settings.json.
Environment
- Claude Code CLI on Windows 11, Git Bash tool shell.
- Occurred while adding a
PostToolUsehook with matcherEdit|Writeto runtsc --noEmitconditionally based ongit diff --name-only | grep -q '.ts$'.