Compare commits

..

3 Commits

Author SHA1 Message Date
Claude
449aa9d3b0 fix(hookify): add size limits to transcript reading to prevent OOM
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.
2026-01-05 20:20:01 +00:00
Anthony Morris
5c92b97cc4 fix(ralph-wiggum): move multi-line bash from command to setup script (#16320)
Fixes #12170

The ralph-wiggum slash commands had multi-line bash scripts in their
```! blocks. Claude Code's security check blocks commands with
newlines to prevent command injection, causing the error:

  "Command contains newlines that could separate multiple commands"

Changes:

ralph-loop.md:
- Remove multi-line bash from code block
- Keep single-line call to setup script
- Keep scoped allowed-tools for security

cancel-ralph.md:
- Replace multi-line bash with step-by-step instructions
- Tighten allowed-tools to specific file paths

setup-ralph-loop.sh:
- Add completion promise display logic (moved from ralph-loop.md)
- Uses COMPLETION_PROMISE variable directly instead of reading from file
2026-01-04 23:27:48 -08:00
GitHub Actions
d213a74fc8 chore: Update CHANGELOG.md 2025-12-19 22:13:14 +00:00
6 changed files with 113 additions and 84 deletions

View File

@@ -1,5 +1,21 @@
# Changelog
## 2.0.74
- Added LSP (Language Server Protocol) tool for code intelligence features like go-to-definition, find references, and hover documentation
- Added `/terminal-setup` support for Kitty, Alacritty, Zed, and Warp terminals
- Added ctrl+t shortcut in `/theme` to toggle syntax highlighting on/off
- Added syntax highlighting info to theme picker
- Added guidance for macOS users when Alt shortcuts fail due to terminal configuration
- Fixed skill `allowed-tools` not being applied to tools invoked by the skill
- Fixed Opus 4.5 tip incorrectly showing when user was already using Opus
- Fixed a potential crash when syntax highlighting isn't initialized correctly
- Fixed visual bug in `/plugins discover` where list selection indicator showed while search box was focused
- Fixed macOS keyboard shortcuts to display 'opt' instead of 'alt'
- Improved `/context` command visualization with grouped skills and agents by source, slash commands, and sorted token count
- [Windows] Fixed issue with improper rendering
- [VSCode] Added gift tag pictogram for year-end promotion message
## 2.0.73
- Added clickable `[Image #N]` links that open attached images in the default viewer

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Rule evaluation engine for hookify plugin."""
import os
import re
import sys
from functools import lru_cache
@@ -9,6 +10,13 @@ from typing import List, Dict, Any, Optional
# Import from local module
from hookify.core.config_loader import Rule, Condition
# Maximum transcript size to load into memory (10MB default)
# For larger files, only the tail is read to prevent OOM
MAX_TRANSCRIPT_SIZE_BYTES = 10 * 1024 * 1024 # 10MB
# Size threshold for warning about large transcripts
TRANSCRIPT_WARNING_SIZE_BYTES = 5 * 1024 * 1024 # 5MB
# Cache compiled regexes (max 128 patterns)
@lru_cache(maxsize=128)
@@ -24,6 +32,58 @@ def compile_regex(pattern: str) -> re.Pattern:
return re.compile(pattern, re.IGNORECASE)
def read_transcript_safely(transcript_path: str) -> str:
"""Read transcript file with size limits to prevent OOM.
For large transcripts (>10MB), only reads the tail of the file
to prevent memory exhaustion. This preserves the most recent
conversation context which is typically what rules care about.
Args:
transcript_path: Path to the transcript file
Returns:
Transcript content as string, possibly truncated for large files
"""
try:
file_size = os.path.getsize(transcript_path)
# Warn about large transcripts
if file_size > TRANSCRIPT_WARNING_SIZE_BYTES:
size_mb = file_size / (1024 * 1024)
print(f"Warning: Large transcript ({size_mb:.1f}MB): {transcript_path}", file=sys.stderr)
# For files within limit, read normally
if file_size <= MAX_TRANSCRIPT_SIZE_BYTES:
with open(transcript_path, 'r') as f:
return f.read()
# For large files, read only the tail to prevent OOM
size_mb = file_size / (1024 * 1024)
limit_mb = MAX_TRANSCRIPT_SIZE_BYTES / (1024 * 1024)
print(f"Warning: Transcript too large ({size_mb:.1f}MB), reading last {limit_mb:.0f}MB only", file=sys.stderr)
with open(transcript_path, 'r') as f:
# Seek to position near end, leaving room for MAX_TRANSCRIPT_SIZE_BYTES
f.seek(file_size - MAX_TRANSCRIPT_SIZE_BYTES)
# Skip partial line at seek position
f.readline()
return f.read()
except FileNotFoundError:
print(f"Warning: Transcript file not found: {transcript_path}", file=sys.stderr)
return ''
except PermissionError:
print(f"Warning: Permission denied reading transcript: {transcript_path}", file=sys.stderr)
return ''
except (IOError, OSError) as e:
print(f"Warning: Error reading transcript {transcript_path}: {e}", file=sys.stderr)
return ''
except UnicodeDecodeError as e:
print(f"Warning: Encoding error in transcript {transcript_path}: {e}", file=sys.stderr)
return ''
class RuleEngine:
"""Evaluates rules against hook input data."""
@@ -205,24 +265,10 @@ class RuleEngine:
if field == 'reason':
return input_data.get('reason', '')
elif field == 'transcript':
# Read transcript file if path provided
# Read transcript file with size limits to prevent OOM
transcript_path = input_data.get('transcript_path')
if transcript_path:
try:
with open(transcript_path, 'r') as f:
return f.read()
except FileNotFoundError:
print(f"Warning: Transcript file not found: {transcript_path}", file=sys.stderr)
return ''
except PermissionError:
print(f"Warning: Permission denied reading transcript: {transcript_path}", file=sys.stderr)
return ''
except (IOError, OSError) as e:
print(f"Warning: Error reading transcript {transcript_path}: {e}", file=sys.stderr)
return ''
except UnicodeDecodeError as e:
print(f"Warning: Encoding error in transcript {transcript_path}: {e}", file=sys.stderr)
return ''
return read_transcript_safely(transcript_path)
elif field == 'user_prompt':
# For UserPromptSubmit events
return input_data.get('user_prompt', '')

View File

@@ -1,26 +1,18 @@
---
description: "Cancel active Ralph Wiggum loop"
allowed-tools: ["Bash"]
allowed-tools: ["Bash(test -f .claude/ralph-loop.local.md:*)", "Bash(rm .claude/ralph-loop.local.md)", "Read(.claude/ralph-loop.local.md)"]
hide-from-slash-command-tool: "true"
---
# Cancel Ralph
```!
if [[ -f .claude/ralph-loop.local.md ]]; then
ITERATION=$(grep '^iteration:' .claude/ralph-loop.local.md | sed 's/iteration: *//')
echo "FOUND_LOOP=true"
echo "ITERATION=$ITERATION"
else
echo "FOUND_LOOP=false"
fi
```
To cancel the Ralph loop:
Check the output above:
1. Check if `.claude/ralph-loop.local.md` exists using Bash: `test -f .claude/ralph-loop.local.md && echo "EXISTS" || echo "NOT_FOUND"`
1. **If FOUND_LOOP=false**:
- Say "No active Ralph loop found."
2. **If NOT_FOUND**: Say "No active Ralph loop found."
2. **If FOUND_LOOP=true**:
- Use Bash: `rm .claude/ralph-loop.local.md`
- Report: "Cancelled Ralph loop (was at iteration N)" where N is the ITERATION value from above.
3. **If EXISTS**:
- Read `.claude/ralph-loop.local.md` to get the current iteration number from the `iteration:` field
- Remove the file using Bash: `rm .claude/ralph-loop.local.md`
- Report: "Cancelled Ralph loop (was at iteration N)" where N is the iteration value

View File

@@ -11,36 +11,6 @@ Execute the setup script to initialize the Ralph loop:
```!
"${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh" $ARGUMENTS
# Extract and display completion promise if set
if [ -f .claude/ralph-loop.local.md ]; then
PROMISE=$(grep '^completion_promise:' .claude/ralph-loop.local.md | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
if [ -n "$PROMISE" ] && [ "$PROMISE" != "null" ]; then
echo ""
echo "═══════════════════════════════════════════════════════════"
echo "CRITICAL - Ralph Loop Completion Promise"
echo "═══════════════════════════════════════════════════════════"
echo ""
echo "To complete this loop, output this EXACT text:"
echo " <promise>$PROMISE</promise>"
echo ""
echo "STRICT REQUIREMENTS (DO NOT VIOLATE):"
echo " ✓ Use <promise> XML tags EXACTLY as shown above"
echo " ✓ The statement MUST be completely and unequivocally TRUE"
echo " ✓ Do NOT output false statements to exit the loop"
echo " ✓ Do NOT lie even if you think you should exit"
echo ""
echo "IMPORTANT - Do not circumvent the loop:"
echo " Even if you believe you're stuck, the task is impossible,"
echo " or you've been running too long - you MUST NOT output a"
echo " false promise statement. The loop is designed to continue"
echo " until the promise is GENUINELY TRUE. Trust the process."
echo ""
echo " If the loop should stop, the promise statement will become"
echo " true naturally. Do not force it by lying."
echo "═══════════════════════════════════════════════════════════"
fi
fi
```
Please work on the task. When you try to exit, the Ralph loop will feed the SAME PROMPT back to you for the next iteration. You'll see your previous work in files and git history, allowing you to iterate and improve.

View File

@@ -174,3 +174,30 @@ if [[ -n "$PROMPT" ]]; then
echo ""
echo "$PROMPT"
fi
# Display completion promise requirements if set
if [[ "$COMPLETION_PROMISE" != "null" ]]; then
echo ""
echo "═══════════════════════════════════════════════════════════"
echo "CRITICAL - Ralph Loop Completion Promise"
echo "═══════════════════════════════════════════════════════════"
echo ""
echo "To complete this loop, output this EXACT text:"
echo " <promise>$COMPLETION_PROMISE</promise>"
echo ""
echo "STRICT REQUIREMENTS (DO NOT VIOLATE):"
echo " ✓ Use <promise> XML tags EXACTLY as shown above"
echo " ✓ The statement MUST be completely and unequivocally TRUE"
echo " ✓ Do NOT output false statements to exit the loop"
echo " ✓ Do NOT lie even if you think you should exit"
echo ""
echo "IMPORTANT - Do not circumvent the loop:"
echo " Even if you believe you're stuck, the task is impossible,"
echo " or you've been running too long - you MUST NOT output a"
echo " false promise statement. The loop is designed to continue"
echo " until the promise is GENUINELY TRUE. Trust the process."
echo ""
echo " If the loop should stop, the promise statement will become"
echo " true naturally. Do not force it by lying."
echo "═══════════════════════════════════════════════════════════"
fi

View File

@@ -1,22 +0,0 @@
// Biome GritQL Plugin: Prevent <Box> from being nested inside <Text>
// This catches the Ink error: "<Box> can't be nested inside <Text> component"
language js;
// Match Text elements that contain Box children (direct nesting)
`<Text$_>$children</Text>` where {
$children <: contains `<Box$_>$_</Box>`,
register_diagnostic(
span = $children,
message = "<Box> can't be nested inside <Text> component. Use <Box> as a sibling or wrap <Text> inside <Box> instead."
)
}
// Also match self-closing Box inside Text
`<Text$_>$children</Text>` where {
$children <: contains `<Box$_ />`,
register_diagnostic(
span = $children,
message = "<Box> can't be nested inside <Text> component. Use <Box> as a sibling or wrap <Text> inside <Box> instead."
)
}