Fatal "JavaScript heap out of memory" crash caused by inefficient hook transcript parsing`
Title:Bug Report: Fatal "JavaScript heap out of memory" crash caused by inefficient hook transcript parsing
Body:
Describe the bug
Claude Code experiences a fatal "JavaScript heap out of memory" crash during multi-turn conversations when a hook is configured to read the session transcript. The root cause is that the hook script uses an inefficient jq -s command, which attempts to "slurp" the entire growing .jsonl transcript file into memory on every tool call. This eventually exhausts the Node.js heap limit and crashes the application.
The initial symptoms are often misleading, appearing as protocol violations or other errors, but the underlying issue is memory exhaustion.
Steps to Reproduce
- Create a hook script, for example, at
.claude/hooks/protocol_guardian.sh, with the following content (or any script that usesjq -son the transcript):
```bash
#!/bin/bash
set -e
input=$(cat)
transcript_path=$(echo "$input" | jq -r '.transcript_path')
# This is the problematic line that causes the crash
transcript=$(jq -s '.' "$transcript_path")
# ... rest of the logic ...
exit 0
```
- Register the hook in a
.claude/settings.jsonfile:
``json``
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/protocol_guardian.sh"
}
]
}
]
}
}
- Start a new Claude Code session in a project with these files.
- Provide a complex, multi-step prompt that will invoke the
maestro-orchestratorand generate a long conversation with many tool uses. For example:build a simple html calculator based on matrix theme. - Allow the agent to run. After several tool uses, the transcript file will grow large enough to trigger the crash.
Expected behavior
The hook should process the transcript in a memory-efficient way (e.g., via streaming) and should not crash the Claude Code application, regardless of the transcript's size.
Actual behavior
The Claude Code process crashes with a fatal error. The terminal output shows:
<--- Last few GCs --->
...
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----
1: 0x1003a553c node::Abort() [/usr/local/bin/node]
2: 0x1003a573c node::ModifyCodeGenerationFromStrings(...) [/usr/local/bin/node]
3: 0x10052a6c4 v8::internal::V8::FatalProcessOutOfMemory(...) [/usr/local/bin/node]
...
24: 0x100808230 v8::internal::JsonStringify(...) [/usr/local/bin/node]
...
[1] 84364 abort claude
The key issue is the JsonStringifier calls in the stack trace, confirming the crash happens while trying to process a large JSON object in memory.
Environment:
- Claude Code Version: 1.0.61
- OS: macOS (but likely affects all platforms)
- Node.js: The crash occurs in the underlying V8 engine of the Node.js runtime.
Root Cause Analysis
The jq -s '.' "$transcript_path" command reads the entire JSONL file into a single string in memory to parse it as an array. The JSONL transcript file can grow to be many megabytes long during an agentic session. The Node.js process that spawns the hook attempts to prepare this large string for stdin, which exhausts the heap.
Suggested Fix
The hook script should be rewritten to process the transcript file as a stream, which is the intended use for the JSONL format. This avoids loading the entire file into memory.
A corrected, memory-efficient approach looks like this:
#!/bin/bash
set -e
input=$(cat)
transcript_path=$(echo "$input" | jq -r '.transcript_path')
# Memory-efficient way to get the last relevant entry
last_task_call=$(cat "$transcript_path" | jq 'select(.role? == "assistant" and (.message.content? | (type == "array" and .[0].name? == "Task")))' | jq -s 'last')
# ... rest of the logic using stream-based processing ...
exit 0
This bug is critical for users implementing hooks that need to inspect the conversation history, as it can lead to frustrating and difficult-to-diagnose crashes. Adopting a stream-processing pattern in documentation and example hooks would be highly beneficial.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗