[BUG] Hook stdin contains unescaped U+0000–U+001F control characters in JSON string fields, causing jq parse error and silent bypass of security hooks
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Claude Code occasionally sends JSON to hook stdin where string fields (.prompt for UserPromptSubmit, .tool_input.command for PreToolUse(Bash)) contain literal control characters U+0000–U+001F instead of proper escape sequences (\n, \t, \^@).
Strict JSON parsers (e.g. jq) reject this with parse error: Invalid string: control characters from U+0000 through U+001F must be escaped.
Because the hook script aborts on parse error, its security checks are silently skipped for that prompt/command — a defense-in-depth gap for hooks that filter secrets, dangerous file reads, command injection patterns, etc.
This appears to be a request-side serialization bug: the JSON envelope written to hook stdin doesn't pass user-provided string content through proper JSON string serialization (e.g. JSON.stringify, json.dumps).
What Should Happen?
JSON sent to hook stdin should escape control characters in string fields per RFC 8259 §7 (\n, \t, \^@, etc.), so any conformant JSON parser (jq, Python's json, Node's JSON.parse) accepts it.
Hook authors should be able to rely on standard JSON parsing without sanitization workarounds. Defense-in-depth filters (secret detection, dangerous-path blocking) should run for every prompt/command, not silently skip on inputs containing control characters.
Error Messages/Logs
UserPromptSubmit hook error
Failed with non-blocking status code: jq: parse error: Invalid string: control characters from U+0000 through U+001F must be escaped at line 2, column 210
Steps to Reproduce
- Configure a
UserPromptSubmithook that parses.promptviajq:
``jsonc``
// ~/.claude/settings.json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "$HOME/.claude/hooks/warn-token-in-prompt.sh" }
]
}
]
}
}
``zsh``
# ~/.claude/hooks/warn-token-in-prompt.sh
#!/usr/bin/env zsh
set -euo pipefail
input=$(cat)
prompt=$(echo "$input" | /usr/bin/jq -r '.prompt // ""')
# ... regex checks for sk-ant-, ghp_, AKIA, etc. ...
- Submit a prompt to Claude Code. The bug occurs occasionally — common triggers seem to be paste from PDF, specific terminal apps, or multi-line prompts with embedded whitespace. The exact reproducer is non-deterministic from the user's side because the malformed JSON is generated by Claude Code itself.
- Observe the error in Claude Code UI:
````
UserPromptSubmit hook error
Failed with non-blocking status code: jq: parse error: Invalid string: control characters from U+0000 through U+001F must be escaped at line 2, column 210
- Confirm that the prompt was submitted to the model without hook checks running (in my case, the secret-pattern filter was bypassed for that prompt — a defense-in-depth gap).
- The same pattern affects
PreToolUse(Bash)hooks parsing.tool_input.command.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.112 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Environment (additional)
| Component | Version |
|:---|:---|
| macOS | 26.4.1 (Tahoe), build 25E253 |
| Architecture | arm64 (Apple Silicon) |
| Kernel | Darwin 25.4.0 |
| jq (Apple-shipped) | 1.7.1-apple (/usr/bin/jq) |
| Shell | zsh |
Impact
- Security: hooks that filter sensitive content (secrets, dangerous file reads, command injection patterns) are silently skipped on affected prompts/commands. Users relying on hook-based defense-in-depth get an inconsistent, non-deterministic guarantee.
- Reliability: error visible to the user as
non-blocking status code, no clear remediation path documented. - Workaround burden: every hook author must independently discover and implement a sanitization step.
Workaround (current)
Strip control characters from the raw JSON before parsing:
input=$(cat | LC_ALL=C /usr/bin/tr -d '\000-\037')
prompt=$(echo "$input" | /usr/bin/jq -r '.prompt // ""')
This is forward-compatible: once Claude Code emits properly escaped JSON, the tr step strips nothing (escape sequences like \n are printable ASCII pairs \\ + n, not control chars).
Proposed fix
Ensure all string field values written to hook stdin pass through proper JSON string serialization (e.g. JSON.stringify in Node, json.dumps in Python) before being placed into the JSON envelope. Avoid string concatenation / template substitution of raw user input.
Related issues
- #6246 — Unicode/UTF-8 character corruption in hook system (related but distinct: corruption vs. escaping).
- #17550 — UserPromptSubmit hook with
hookSpecificOutputJSON (different field, same family). - #34713 — False "Hook Error" labels (related symptom display).
- #44943 — JSON parse errors in hook responses (response side; this report is request side).
- #1132 — jq escaping in Bash tool (different code path).
Additional notes
I am happy to test a fix in a pre-release build if helpful.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗