spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

fix(hookify): add size limits to transcript reading to prevent OOMBrowse files
When hookify rules use `field: transcript`, the entire transcript file
was read into memory. For long-running sessions or ralph loops,
transcripts can grow to gigabytes, causing OOM kills.

This fix adds:
- 10MB max size limit for transcript reading
- For larger files, only the tail (most recent content) is read
- Warning at 5MB to alert users of large transcripts
- Proper error handling consolidated into a reusable function

This addresses memory issues reported in monorepo where CC was
consuming 30-40GB before getting OOM killed during startup/resume.
Claude authored on Jan 5, 2026 parent 5c92b97 · commit 449aa9d3b023
1 changed file+62 -16compared with 5c92b97
Mplugins/hookify/core/rule_engine.py
@@ -1,6 +1,7 @@
11 #!/usr/bin/env python3
22 """Rule evaluation engine for hookify plugin."""
33
4+import os
45 import re
56 import sys
67 from functools import lru_cache
@@ -9,7 +10,14 @@
910 # Import from local module
1011 from hookify.core.config_loader import Rule, Condition
1112
13+# Maximum transcript size to load into memory (10MB default)
14+# For larger files, only the tail is read to prevent OOM
15+MAX_TRANSCRIPT_SIZE_BYTES = 10 * 1024 * 1024 # 10MB
1216
17+# Size threshold for warning about large transcripts
18+TRANSCRIPT_WARNING_SIZE_BYTES = 5 * 1024 * 1024 # 5MB
19+
20+
1321 # Cache compiled regexes (max 128 patterns)
1422 @lru_cache(maxsize=128)
1523 def compile_regex(pattern: str) -> re.Pattern:
@@ -24,6 +32,58 @@
2432 return re.compile(pattern, re.IGNORECASE)
2533
2634
35+def read_transcript_safely(transcript_path: str) -> str:
36+ """Read transcript file with size limits to prevent OOM.
37+
38+ For large transcripts (>10MB), only reads the tail of the file
39+ to prevent memory exhaustion. This preserves the most recent
40+ conversation context which is typically what rules care about.
41+
42+ Args:
43+ transcript_path: Path to the transcript file
44+
45+ Returns:
46+ Transcript content as string, possibly truncated for large files
47+ """
48+ try:
49+ file_size = os.path.getsize(transcript_path)
50+
51+ # Warn about large transcripts
52+ if file_size > TRANSCRIPT_WARNING_SIZE_BYTES:
53+ size_mb = file_size / (1024 * 1024)
54+ print(f"Warning: Large transcript ({size_mb:.1f}MB): {transcript_path}", file=sys.stderr)
55+
56+ # For files within limit, read normally
57+ if file_size <= MAX_TRANSCRIPT_SIZE_BYTES:
58+ with open(transcript_path, 'r') as f:
59+ return f.read()
60+
61+ # For large files, read only the tail to prevent OOM
62+ size_mb = file_size / (1024 * 1024)
63+ limit_mb = MAX_TRANSCRIPT_SIZE_BYTES / (1024 * 1024)
64+ print(f"Warning: Transcript too large ({size_mb:.1f}MB), reading last {limit_mb:.0f}MB only", file=sys.stderr)
65+
66+ with open(transcript_path, 'r') as f:
67+ # Seek to position near end, leaving room for MAX_TRANSCRIPT_SIZE_BYTES
68+ f.seek(file_size - MAX_TRANSCRIPT_SIZE_BYTES)
69+ # Skip partial line at seek position
70+ f.readline()
71+ return f.read()
72+
73+ except FileNotFoundError:
74+ print(f"Warning: Transcript file not found: {transcript_path}", file=sys.stderr)
75+ return ''
76+ except PermissionError:
77+ print(f"Warning: Permission denied reading transcript: {transcript_path}", file=sys.stderr)
78+ return ''
79+ except (IOError, OSError) as e:
80+ print(f"Warning: Error reading transcript {transcript_path}: {e}", file=sys.stderr)
81+ return ''
82+ except UnicodeDecodeError as e:
83+ print(f"Warning: Encoding error in transcript {transcript_path}: {e}", file=sys.stderr)
84+ return ''
85+
86+
2787 class RuleEngine:
2888 """Evaluates rules against hook input data."""
2989
@@ -205,24 +265,10 @@
205265 if field == 'reason':
206266 return input_data.get('reason', '')
207267 elif field == 'transcript':
208- # Read transcript file if path provided
268+ # Read transcript file with size limits to prevent OOM
209269 transcript_path = input_data.get('transcript_path')
210270 if transcript_path:
211- try:
212- with open(transcript_path, 'r') as f:
213- return f.read()
214- except FileNotFoundError:
215- print(f"Warning: Transcript file not found: {transcript_path}", file=sys.stderr)
216- return ''
217- except PermissionError:
218- print(f"Warning: Permission denied reading transcript: {transcript_path}", file=sys.stderr)
219- return ''
220- except (IOError, OSError) as e:
221- print(f"Warning: Error reading transcript {transcript_path}: {e}", file=sys.stderr)
222- return ''
223- except UnicodeDecodeError as e:
224- print(f"Warning: Encoding error in transcript {transcript_path}: {e}", file=sys.stderr)
225- return ''
271+ return read_transcript_safely(transcript_path)
226272 elif field == 'user_prompt':
227273 # For UserPromptSubmit events
228274 return input_data.get('user_prompt', '')