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

Merge pull request #11752 from anthropics/daisy/ralph-wiggum/public-marketplaceBrowse files
Add ralph-wiggum and hookify plugins to public marketplace
Daisy S. Hollman authored on Nov 17, 2025 · GitHub committed on Nov 17, 2025 parents 5dd91a9 + 59372c0 · commit 31ba6e541b97
33 changed files+3307 -0compared with 5dd91a9
M.claude-plugin/marketplace.json
Aplugins/hookify/.claude-plugin/plugin.json
Aplugins/hookify/.gitignore
Aplugins/hookify/README.md
Aplugins/hookify/agents/conversation-analyzer.md
Aplugins/hookify/commands/configure.md
Aplugins/hookify/commands/help.md
Aplugins/hookify/commands/hookify.md
Aplugins/hookify/commands/list.md
Aplugins/hookify/core/__init__.py
Aplugins/hookify/core/config_loader.py
Aplugins/hookify/core/rule_engine.py
Aplugins/hookify/examples/console-log-warning.local.md
Aplugins/hookify/examples/dangerous-rm.local.md
Aplugins/hookify/examples/require-tests-stop.local.md
Aplugins/hookify/examples/sensitive-files-warning.local.md
Aplugins/hookify/hooks/__init__.py
Aplugins/hookify/hooks/hooks.json
Aplugins/hookify/hooks/posttooluse.py
Aplugins/hookify/hooks/pretooluse.py
Aplugins/hookify/hooks/stop.py
Aplugins/hookify/hooks/userpromptsubmit.py
Aplugins/hookify/matchers/__init__.py
Aplugins/hookify/skills/writing-rules/SKILL.md
Aplugins/hookify/utils/__init__.py
Aplugins/ralph-wiggum/.claude-plugin/plugin.json
Aplugins/ralph-wiggum/README.md
Aplugins/ralph-wiggum/commands/cancel-ralph.md
Aplugins/ralph-wiggum/commands/help.md
Aplugins/ralph-wiggum/commands/ralph-loop.md
Aplugins/ralph-wiggum/hooks/hooks.json
Aplugins/ralph-wiggum/hooks/stop-hook.sh
Aplugins/ralph-wiggum/scripts/setup-ralph-loop.sh
@@ -101,6 +101,28 @@
101101 },
102102 "source": "./plugins/frontend-design",
103103 "category": "development"
104+ },
105+ {
106+ "name": "ralph-wiggum",
107+ "description": "Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly, seeing its previous work, until completion.",
108+ "version": "1.0.0",
109+ "author": {
110+ "name": "Daisy Hollman",
111+ "email": "daisy@anthropic.com"
112+ },
113+ "source": "./plugins/ralph-wiggum",
114+ "category": "development"
115+ },
116+ {
117+ "name": "hookify",
118+ "description": "Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or from explicit instructions. Define rules via simple markdown files.",
119+ "version": "0.1.0",
120+ "author": {
121+ "name": "Daisy Hollman",
122+ "email": "daisy@anthropic.com"
123+ },
124+ "source": "./plugins/hookify",
125+ "category": "productivity"
104126 }
105127 ]
106128 }
@@ -1,0 +1,9 @@
1+{
2+ "name": "hookify",
3+ "version": "0.1.0",
4+ "description": "Easily create hooks to prevent unwanted behaviors by analyzing conversation patterns",
5+ "author": {
6+ "name": "Daisy Hollman",
7+ "email": "daisy@anthropic.com"
8+ }
9+}
@@ -1,0 +1,30 @@
1+# Python
2+__pycache__/
3+*.py[cod]
4+*$py.class
5+*.so
6+.Python
7+
8+# Virtual environments
9+venv/
10+env/
11+ENV/
12+
13+# IDE
14+.vscode/
15+.idea/
16+*.swp
17+*.swo
18+
19+# OS
20+.DS_Store
21+Thumbs.db
22+
23+# Testing
24+.pytest_cache/
25+.coverage
26+htmlcov/
27+
28+# Local configuration (should not be committed)
29+.claude/*.local.md
30+.claude/*.local.json
@@ -1,0 +1,340 @@
1+# Hookify Plugin
2+
3+Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or from explicit instructions.
4+
5+## Overview
6+
7+The hookify plugin makes it simple to create hooks without editing complex `hooks.json` files. Instead, you create lightweight markdown configuration files that define patterns to watch for and messages to show when those patterns match.
8+
9+**Key features:**
10+- 🎯 Analyze conversations to find unwanted behaviors automatically
11+- 📝 Simple markdown configuration files with YAML frontmatter
12+- 🔍 Regex pattern matching for powerful rules
13+- 🚀 No coding required - just describe the behavior
14+- 🔄 Easy enable/disable without restarting
15+
16+## Quick Start
17+
18+### 1. Create Your First Rule
19+
20+```bash
21+/hookify Warn me when I use rm -rf commands
22+```
23+
24+This analyzes your request and creates `.claude/hookify.warn-rm.local.md`.
25+
26+### 2. Test It Immediately
27+
28+**No restart needed!** Rules take effect on the very next tool use.
29+
30+Ask Claude to run a command that should trigger the rule:
31+```
32+Run rm -rf /tmp/test
33+```
34+
35+You should see the warning message immediately!
36+
37+## Usage
38+
39+### Main Command: /hookify
40+
41+**With arguments:**
42+```
43+/hookify Don't use console.log in TypeScript files
44+```
45+Creates a rule from your explicit instructions.
46+
47+**Without arguments:**
48+```
49+/hookify
50+```
51+Analyzes recent conversation to find behaviors you've corrected or been frustrated by.
52+
53+### Helper Commands
54+
55+**List all rules:**
56+```
57+/hookify:list
58+```
59+
60+**Configure rules interactively:**
61+```
62+/hookify:configure
63+```
64+Enable/disable existing rules through an interactive interface.
65+
66+**Get help:**
67+```
68+/hookify:help
69+```
70+
71+## Rule Configuration Format
72+
73+### Simple Rule (Single Pattern)
74+
75+`.claude/hookify.dangerous-rm.local.md`:
76+```markdown
77+---
78+name: block-dangerous-rm
79+enabled: true
80+event: bash
81+pattern: rm\s+-rf
82+action: block
83+---
84+
85+⚠️ **Dangerous rm command detected!**
86+
87+This command could delete important files. Please:
88+- Verify the path is correct
89+- Consider using a safer approach
90+- Make sure you have backups
91+```
92+
93+**Action field:**
94+- `warn`: Shows warning but allows operation (default)
95+- `block`: Prevents operation from executing (PreToolUse) or stops session (Stop events)
96+
97+### Advanced Rule (Multiple Conditions)
98+
99+`.claude/hookify.sensitive-files.local.md`:
100+```markdown
101+---
102+name: warn-sensitive-files
103+enabled: true
104+event: file
105+action: warn
106+conditions:
107+ - field: file_path
108+ operator: regex_match
109+ pattern: \.env$|credentials|secrets
110+ - field: new_text
111+ operator: contains
112+ pattern: KEY
113+---
114+
115+🔐 **Sensitive file edit detected!**
116+
117+Ensure credentials are not hardcoded and file is in .gitignore.
118+```
119+
120+**All conditions must match** for the rule to trigger.
121+
122+## Event Types
123+
124+- **`bash`**: Triggers on Bash tool commands
125+- **`file`**: Triggers on Edit, Write, MultiEdit tools
126+- **`stop`**: Triggers when Claude wants to stop (for completion checks)
127+- **`prompt`**: Triggers on user prompt submission
128+- **`all`**: Triggers on all events
129+
130+## Pattern Syntax
131+
132+Use Python regex syntax:
133+
134+| Pattern | Matches | Example |
135+|---------|---------|---------|
136+| `rm\s+-rf` | rm -rf | rm -rf /tmp |
137+| `console\.log\(` | console.log( | console.log("test") |
138+| `(eval\|exec)\(` | eval( or exec( | eval("code") |
139+| `\.env$` | files ending in .env | .env, .env.local |
140+| `chmod\s+777` | chmod 777 | chmod 777 file.txt |
141+
142+**Tips:**
143+- Use `\s` for whitespace
144+- Escape special chars: `\.` for literal dot
145+- Use `|` for OR: `(foo|bar)`
146+- Use `.*` to match anything
147+- Set `action: block` for dangerous operations
148+- Set `action: warn` (or omit) for informational warnings
149+
150+## Examples
151+
152+### Example 1: Block Dangerous Commands
153+
154+```markdown
155+---
156+name: block-destructive-ops
157+enabled: true
158+event: bash
159+pattern: rm\s+-rf|dd\s+if=|mkfs|format
160+action: block
161+---
162+
163+🛑 **Destructive operation detected!**
164+
165+This command can cause data loss. Operation blocked for safety.
166+Please verify the exact path and use a safer approach.
167+```
168+
169+**This rule blocks the operation** - Claude will not be allowed to execute these commands.
170+
171+### Example 2: Warn About Debug Code
172+
173+```markdown
174+---
175+name: warn-debug-code
176+enabled: true
177+event: file
178+pattern: console\.log\(|debugger;|print\(
179+action: warn
180+---
181+
182+🐛 **Debug code detected**
183+
184+Remember to remove debugging statements before committing.
185+```
186+
187+**This rule warns but allows** - Claude sees the message but can still proceed.
188+
189+### Example 3: Require Tests Before Stopping
190+
191+```markdown
192+---
193+name: require-tests-run
194+enabled: false
195+event: stop
196+action: block
197+conditions:
198+ - field: transcript
199+ operator: not_contains
200+ pattern: npm test|pytest|cargo test
201+---
202+
203+**Tests not detected in transcript!**
204+
205+Before stopping, please run tests to verify your changes work correctly.
206+```
207+
208+**This blocks Claude from stopping** if no test commands appear in the session transcript. Enable only when you want strict enforcement.
209+
210+## Advanced Usage
211+
212+### Multiple Conditions
213+
214+Check multiple fields simultaneously:
215+
216+```markdown
217+---
218+name: api-key-in-typescript
219+enabled: true
220+event: file
221+conditions:
222+ - field: file_path
223+ operator: regex_match
224+ pattern: \.tsx?$
225+ - field: new_text
226+ operator: regex_match
227+ pattern: (API_KEY|SECRET|TOKEN)\s*=\s*["']
228+---
229+
230+🔐 **Hardcoded credential in TypeScript!**
231+
232+Use environment variables instead of hardcoded values.
233+```
234+
235+### Operators Reference
236+
237+- `regex_match`: Pattern must match (most common)
238+- `contains`: String must contain pattern
239+- `equals`: Exact string match
240+- `not_contains`: String must NOT contain pattern
241+- `starts_with`: String starts with pattern
242+- `ends_with`: String ends with pattern
243+
244+### Field Reference
245+
246+**For bash events:**
247+- `command`: The bash command string
248+
249+**For file events:**
250+- `file_path`: Path to file being edited
251+- `new_text`: New content being added (Edit, Write)
252+- `old_text`: Old content being replaced (Edit only)
253+- `content`: File content (Write only)
254+
255+**For prompt events:**
256+- `user_prompt`: The user's submitted prompt text
257+
258+**For stop events:**
259+- Use general matching on session state
260+
261+## Management
262+
263+### Enable/Disable Rules
264+
265+**Temporarily disable:**
266+Edit the `.local.md` file and set `enabled: false`
267+
268+**Re-enable:**
269+Set `enabled: true`
270+
271+**Or use interactive tool:**
272+```
273+/hookify:configure
274+```
275+
276+### Delete Rules
277+
278+Simply delete the `.local.md` file:
279+```bash
280+rm .claude/hookify.my-rule.local.md
281+```
282+
283+### View All Rules
284+
285+```
286+/hookify:list
287+```
288+
289+## Installation
290+
291+This plugin is part of the Claude Code Marketplace. It should be auto-discovered when the marketplace is installed.
292+
293+**Manual testing:**
294+```bash
295+cc --plugin-dir /path/to/hookify
296+```
297+
298+## Requirements
299+
300+- Python 3.7+
301+- No external dependencies (uses stdlib only)
302+
303+## Troubleshooting
304+
305+**Rule not triggering:**
306+1. Check rule file exists in `.claude/` directory (in project root, not plugin directory)
307+2. Verify `enabled: true` in frontmatter
308+3. Test regex pattern separately
309+4. Rules should work immediately - no restart needed
310+5. Try `/hookify:list` to see if rule is loaded
311+
312+**Import errors:**
313+- Ensure Python 3 is available: `python3 --version`
314+- Check hookify plugin is installed
315+
316+**Pattern not matching:**
317+- Test regex: `python3 -c "import re; print(re.search(r'pattern', 'text'))"`
318+- Use unquoted patterns in YAML to avoid escaping issues
319+- Start simple, then add complexity
320+
321+**Hook seems slow:**
322+- Keep patterns simple (avoid complex regex)
323+- Use specific event types (bash, file) instead of "all"
324+- Limit number of active rules
325+
326+## Contributing
327+
328+Found a useful rule pattern? Consider sharing example files via PR!
329+
330+## Future Enhancements
331+
332+- Severity levels (error/warning/info distinctions)
333+- Rule templates library
334+- Interactive pattern builder
335+- Hook testing utilities
336+- JSON format support (in addition to markdown)
337+
338+## License
339+
340+MIT License
@@ -1,0 +1,176 @@
1+---
2+name: conversation-analyzer
3+description: Use this agent when analyzing conversation transcripts to find behaviors worth preventing with hooks. Examples: <example>Context: User is running /hookify command without arguments\nuser: "/hookify"\nassistant: "I'll analyze the conversation to find behaviors you want to prevent"\n<commentary>The /hookify command without arguments triggers conversation analysis to find unwanted behaviors.</commentary></example><example>Context: User wants to create hooks from recent frustrations\nuser: "Can you look back at this conversation and help me create hooks for the mistakes you made?"\nassistant: "I'll use the conversation-analyzer agent to identify the issues and suggest hooks."\n<commentary>User explicitly asks to analyze conversation for mistakes that should be prevented.</commentary></example>
4+model: inherit
5+color: yellow
6+tools: ["Read", "Grep"]
7+---
8+
9+You are a conversation analysis specialist that identifies problematic behaviors in Claude Code sessions that could be prevented with hooks.
10+
11+**Your Core Responsibilities:**
12+1. Read and analyze user messages to find frustration signals
13+2. Identify specific tool usage patterns that caused issues
14+3. Extract actionable patterns that can be matched with regex
15+4. Categorize issues by severity and type
16+5. Provide structured findings for hook rule generation
17+
18+**Analysis Process:**
19+
20+### 1. Search for User Messages Indicating Issues
21+
22+Read through user messages in reverse chronological order (most recent first). Look for:
23+
24+**Explicit correction requests:**
25+- "Don't use X"
26+- "Stop doing Y"
27+- "Please don't Z"
28+- "Avoid..."
29+- "Never..."
30+
31+**Frustrated reactions:**
32+- "Why did you do X?"
33+- "I didn't ask for that"
34+- "That's not what I meant"
35+- "That was wrong"
36+
37+**Corrections and reversions:**
38+- User reverting changes Claude made
39+- User fixing issues Claude created
40+- User providing step-by-step corrections
41+
42+**Repeated issues:**
43+- Same type of mistake multiple times
44+- User having to remind multiple times
45+- Pattern of similar problems
46+
47+### 2. Identify Tool Usage Patterns
48+
49+For each issue, determine:
50+- **Which tool**: Bash, Edit, Write, MultiEdit
51+- **What action**: Specific command or code pattern
52+- **When it happened**: During what task/phase
53+- **Why problematic**: User's stated reason or implicit concern
54+
55+**Extract concrete examples:**
56+- For Bash: Actual command that was problematic
57+- For Edit/Write: Code pattern that was added
58+- For Stop: What was missing before stopping
59+
60+### 3. Create Regex Patterns
61+
62+Convert behaviors into matchable patterns:
63+
64+**Bash command patterns:**
65+- `rm\s+-rf` for dangerous deletes
66+- `sudo\s+` for privilege escalation
67+- `chmod\s+777` for permission issues
68+
69+**Code patterns (Edit/Write):**
70+- `console\.log\(` for debug logging
71+- `eval\(|new Function\(` for dangerous eval
72+- `innerHTML\s*=` for XSS risks
73+
74+**File path patterns:**
75+- `\.env$` for environment files
76+- `/node_modules/` for dependency files
77+- `dist/|build/` for generated files
78+
79+### 4. Categorize Severity
80+
81+**High severity (should block in future):**
82+- Dangerous commands (rm -rf, chmod 777)
83+- Security issues (hardcoded secrets, eval)
84+- Data loss risks
85+
86+**Medium severity (warn):**
87+- Style violations (console.log in production)
88+- Wrong file types (editing generated files)
89+- Missing best practices
90+
91+**Low severity (optional):**
92+- Preferences (coding style)
93+- Non-critical patterns
94+
95+### 5. Output Format
96+
97+Return your findings as structured text in this format:
98+
99+```
100+## Hookify Analysis Results
101+
102+### Issue 1: Dangerous rm Commands
103+**Severity**: High
104+**Tool**: Bash
105+**Pattern**: `rm\s+-rf`
106+**Occurrences**: 3 times
107+**Context**: Used rm -rf on /tmp directories without verification
108+**User Reaction**: "Please be more careful with rm commands"
109+
110+**Suggested Rule:**
111+- Name: warn-dangerous-rm
112+- Event: bash
113+- Pattern: rm\s+-rf
114+- Message: "Dangerous rm command detected. Verify path before proceeding."
115+
116+---
117+
118+### Issue 2: Console.log in TypeScript
119+**Severity**: Medium
120+**Tool**: Edit/Write
121+**Pattern**: `console\.log\(`
122+**Occurrences**: 2 times
123+**Context**: Added console.log statements to production TypeScript files
124+**User Reaction**: "Don't use console.log in production code"
125+
126+**Suggested Rule:**
127+- Name: warn-console-log
128+- Event: file
129+- Pattern: console\.log\(
130+- Message: "Console.log detected. Use proper logging library instead."
131+
132+---
133+
134+[Continue for each issue found...]
135+
136+## Summary
137+
138+Found {N} behaviors worth preventing:
139+- {N} high severity
140+- {N} medium severity
141+- {N} low severity
142+
143+Recommend creating rules for high and medium severity issues.
144+```
145+
146+**Quality Standards:**
147+- Be specific about patterns (don't be overly broad)
148+- Include actual examples from conversation
149+- Explain why each issue matters
150+- Provide ready-to-use regex patterns
151+- Don't false-positive on discussions about what NOT to do
152+
153+**Edge Cases:**
154+
155+**User discussing hypotheticals:**
156+- "What would happen if I used rm -rf?"
157+- Don't treat as problematic behavior
158+
159+**Teaching moments:**
160+- "Here's what you shouldn't do: ..."
161+- Context indicates explanation, not actual problem
162+
163+**One-time accidents:**
164+- Single occurrence, already fixed
165+- Mention but mark as low priority
166+
167+**Subjective preferences:**
168+- "I prefer X over Y"
169+- Mark as low severity, let user decide
170+
171+**Return Results:**
172+Provide your analysis in the structured format above. The /hookify command will use this to:
173+1. Present findings to user
174+2. Ask which rules to create
175+3. Generate .local.md configuration files
176+4. Save rules to .claude directory
@@ -1,0 +1,128 @@
1+---
2+description: Enable or disable hookify rules interactively
3+allowed-tools: ["Glob", "Read", "Edit", "AskUserQuestion", "Skill"]
4+---
5+
6+# Configure Hookify Rules
7+
8+**Load hookify:writing-rules skill first** to understand rule format.
9+
10+Enable or disable existing hookify rules using an interactive interface.
11+
12+## Steps
13+
14+### 1. Find Existing Rules
15+
16+Use Glob tool to find all hookify rule files:
17+```
18+pattern: ".claude/hookify.*.local.md"
19+```
20+
21+If no rules found, inform user:
22+```
23+No hookify rules configured yet. Use `/hookify` to create your first rule.
24+```
25+
26+### 2. Read Current State
27+
28+For each rule file:
29+- Read the file
30+- Extract `name` and `enabled` fields from frontmatter
31+- Build list of rules with current state
32+
33+### 3. Ask User Which Rules to Toggle
34+
35+Use AskUserQuestion to let user select rules:
36+
37+```json
38+{
39+ "questions": [
40+ {
41+ "question": "Which rules would you like to enable or disable?",
42+ "header": "Configure",
43+ "multiSelect": true,
44+ "options": [
45+ {
46+ "label": "warn-dangerous-rm (currently enabled)",
47+ "description": "Warns about rm -rf commands"
48+ },
49+ {
50+ "label": "warn-console-log (currently disabled)",
51+ "description": "Warns about console.log in code"
52+ },
53+ {
54+ "label": "require-tests (currently enabled)",
55+ "description": "Requires tests before stopping"
56+ }
57+ ]
58+ }
59+ ]
60+}
61+```
62+
63+**Option format:**
64+- Label: `{rule-name} (currently {enabled|disabled})`
65+- Description: Brief description from rule's message or pattern
66+
67+### 4. Parse User Selection
68+
69+For each selected rule:
70+- Determine current state from label (enabled/disabled)
71+- Toggle state: enabled → disabled, disabled → enabled
72+
73+### 5. Update Rule Files
74+
75+For each rule to toggle:
76+- Use Read tool to read current content
77+- Use Edit tool to change `enabled: true` to `enabled: false` (or vice versa)
78+- Handle both with and without quotes
79+
80+**Edit pattern for enabling:**
81+```
82+old_string: "enabled: false"
83+new_string: "enabled: true"
84+```
85+
86+**Edit pattern for disabling:**
87+```
88+old_string: "enabled: true"
89+new_string: "enabled: false"
90+```
91+
92+### 6. Confirm Changes
93+
94+Show user what was changed:
95+
96+```
97+## Hookify Rules Updated
98+
99+**Enabled:**
100+- warn-console-log
101+
102+**Disabled:**
103+- warn-dangerous-rm
104+
105+**Unchanged:**
106+- require-tests
107+
108+Changes apply immediately - no restart needed
109+```
110+
111+## Important Notes
112+
113+- Changes take effect immediately on next tool use
114+- You can also manually edit .claude/hookify.*.local.md files
115+- To permanently remove a rule, delete its .local.md file
116+- Use `/hookify:list` to see all configured rules
117+
118+## Edge Cases
119+
120+**No rules to configure:**
121+- Show message about using `/hookify` to create rules first
122+
123+**User selects no rules:**
124+- Inform that no changes were made
125+
126+**File read/write errors:**
127+- Inform user of specific error
128+- Suggest manual editing as fallback
@@ -1,0 +1,175 @@
1+---
2+description: Get help with the hookify plugin
3+allowed-tools: ["Read"]
4+---
5+
6+# Hookify Plugin Help
7+
8+Explain how the hookify plugin works and how to use it.
9+
10+## Overview
11+
12+The hookify plugin makes it easy to create custom hooks that prevent unwanted behaviors. Instead of editing `hooks.json` files, users create simple markdown configuration files that define patterns to watch for.
13+
14+## How It Works
15+
16+### 1. Hook System
17+
18+Hookify installs generic hooks that run on these events:
19+- **PreToolUse**: Before any tool executes (Bash, Edit, Write, etc.)
20+- **PostToolUse**: After a tool executes
21+- **Stop**: When Claude wants to stop working
22+- **UserPromptSubmit**: When user submits a prompt
23+
24+These hooks read configuration files from `.claude/hookify.*.local.md` and check if any rules match the current operation.
25+
26+### 2. Configuration Files
27+
28+Users create rules in `.claude/hookify.{rule-name}.local.md` files:
29+
30+```markdown
31+---
32+name: warn-dangerous-rm
33+enabled: true
34+event: bash
35+pattern: rm\s+-rf
36+---
37+
38+⚠️ **Dangerous rm command detected!**
39+
40+This command could delete important files. Please verify the path.
41+```
42+
43+**Key fields:**
44+- `name`: Unique identifier for the rule
45+- `enabled`: true/false to activate/deactivate
46+- `event`: bash, file, stop, prompt, or all
47+- `pattern`: Regex pattern to match
48+
49+The message body is what Claude sees when the rule triggers.
50+
51+### 3. Creating Rules
52+
53+**Option A: Use /hookify command**
54+```
55+/hookify Don't use console.log in production files
56+```
57+
58+This analyzes your request and creates the appropriate rule file.
59+
60+**Option B: Create manually**
61+Create `.claude/hookify.my-rule.local.md` with the format above.
62+
63+**Option C: Analyze conversation**
64+```
65+/hookify
66+```
67+
68+Without arguments, hookify analyzes recent conversation to find behaviors you want to prevent.
69+
70+## Available Commands
71+
72+- **`/hookify`** - Create hooks from conversation analysis or explicit instructions
73+- **`/hookify:help`** - Show this help (what you're reading now)
74+- **`/hookify:list`** - List all configured hooks
75+- **`/hookify:configure`** - Enable/disable existing hooks interactively
76+
77+## Example Use Cases
78+
79+**Prevent dangerous commands:**
80+```markdown
81+---
82+name: block-chmod-777
83+enabled: true
84+event: bash
85+pattern: chmod\s+777
86+---
87+
88+Don't use chmod 777 - it's a security risk. Use specific permissions instead.
89+```
90+
91+**Warn about debugging code:**
92+```markdown
93+---
94+name: warn-console-log
95+enabled: true
96+event: file
97+pattern: console\.log\(
98+---
99+
100+Console.log detected. Remember to remove debug logging before committing.
101+```
102+
103+**Require tests before stopping:**
104+```markdown
105+---
106+name: require-tests
107+enabled: true
108+event: stop
109+pattern: .*
110+---
111+
112+Did you run tests before finishing? Make sure `npm test` or equivalent was executed.
113+```
114+
115+## Pattern Syntax
116+
117+Use Python regex syntax:
118+- `\s` - whitespace
119+- `\.` - literal dot
120+- `|` - OR
121+- `+` - one or more
122+- `*` - zero or more
123+- `\d` - digit
124+- `[abc]` - character class
125+
126+**Examples:**
127+- `rm\s+-rf` - matches "rm -rf"
128+- `console\.log\(` - matches "console.log("
129+- `(eval|exec)\(` - matches "eval(" or "exec("
130+- `\.env$` - matches files ending in .env
131+
132+## Important Notes
133+
134+**No Restart Needed**: Hookify rules (`.local.md` files) take effect immediately on the next tool use. The hookify hooks are already loaded and read your rules dynamically.
135+
136+**Block or Warn**: Rules can either `block` operations (prevent execution) or `warn` (show message but allow). Set `action: block` or `action: warn` in the rule's frontmatter.
137+
138+**Rule Files**: Keep rules in `.claude/hookify.*.local.md` - they should be git-ignored (add to .gitignore if needed).
139+
140+**Disable Rules**: Set `enabled: false` in frontmatter or delete the file.
141+
142+## Troubleshooting
143+
144+**Hook not triggering:**
145+- Check rule file is in `.claude/` directory
146+- Verify `enabled: true` in frontmatter
147+- Confirm pattern is valid regex
148+- Test pattern: `python3 -c "import re; print(re.search('your_pattern', 'test_text'))"`
149+- Rules take effect immediately - no restart needed
150+
151+**Import errors:**
152+- Check Python 3 is available: `python3 --version`
153+- Verify hookify plugin is installed correctly
154+
155+**Pattern not matching:**
156+- Test regex separately
157+- Check for escaping issues (use unquoted patterns in YAML)
158+- Try simpler pattern first, then refine
159+
160+## Getting Started
161+
162+1. Create your first rule:
163+ ```
164+ /hookify Warn me when I try to use rm -rf
165+ ```
166+
167+2. Try to trigger it:
168+ - Ask Claude to run `rm -rf /tmp/test`
169+ - You should see the warning
170+
171+4. Refine the rule by editing `.claude/hookify.warn-rm.local.md`
172+
173+5. Create more rules as you encounter unwanted behaviors
174+
175+For more examples, check the `${CLAUDE_PLUGIN_ROOT}/examples/` directory.
@@ -1,0 +1,231 @@
1+---
2+description: Create hooks to prevent unwanted behaviors from conversation analysis or explicit instructions
3+argument-hint: Optional specific behavior to address
4+allowed-tools: ["Read", "Write", "AskUserQuestion", "Task", "Grep", "TodoWrite", "Skill"]
5+---
6+
7+# Hookify - Create Hooks from Unwanted Behaviors
8+
9+**FIRST: Load the hookify:writing-rules skill** using the Skill tool to understand rule file format and syntax.
10+
11+Create hook rules to prevent problematic behaviors by analyzing the conversation or from explicit user instructions.
12+
13+## Your Task
14+
15+You will help the user create hookify rules to prevent unwanted behaviors. Follow these steps:
16+
17+### Step 1: Gather Behavior Information
18+
19+**If $ARGUMENTS is provided:**
20+- User has given specific instructions: `$ARGUMENTS`
21+- Still analyze recent conversation (last 10-15 user messages) for additional context
22+- Look for examples of the behavior happening
23+
24+**If $ARGUMENTS is empty:**
25+- Launch the conversation-analyzer agent to find problematic behaviors
26+- Agent will scan user prompts for frustration signals
27+- Agent will return structured findings
28+
29+**To analyze conversation:**
30+Use the Task tool to launch conversation-analyzer agent:
31+```
32+{
33+ "subagent_type": "general-purpose",
34+ "description": "Analyze conversation for unwanted behaviors",
35+ "prompt": "You are analyzing a Claude Code conversation to find behaviors the user wants to prevent.
36+
37+Read user messages in the current conversation and identify:
38+1. Explicit requests to avoid something (\"don't do X\", \"stop doing Y\")
39+2. Corrections or reversions (user fixing Claude's actions)
40+3. Frustrated reactions (\"why did you do X?\", \"I didn't ask for that\")
41+4. Repeated issues (same problem multiple times)
42+
43+For each issue found, extract:
44+- What tool was used (Bash, Edit, Write, etc.)
45+- Specific pattern or command
46+- Why it was problematic
47+- User's stated reason
48+
49+Return findings as a structured list with:
50+- category: Type of issue
51+- tool: Which tool was involved
52+- pattern: Regex or literal pattern to match
53+- context: What happened
54+- severity: high/medium/low
55+
56+Focus on the most recent issues (last 20-30 messages). Don't go back further unless explicitly asked."
57+}
58+```
59+
60+### Step 2: Present Findings to User
61+
62+After gathering behaviors (from arguments or agent), present to user using AskUserQuestion:
63+
64+**Question 1: Which behaviors to hookify?**
65+- Header: "Create Rules"
66+- multiSelect: true
67+- Options: List each detected behavior (max 4)
68+ - Label: Short description (e.g., "Block rm -rf")
69+ - Description: Why it's problematic
70+
71+**Question 2: For each selected behavior, ask about action:**
72+- "Should this block the operation or just warn?"
73+- Options:
74+ - "Just warn" (action: warn - shows message but allows)
75+ - "Block operation" (action: block - prevents execution)
76+
77+**Question 3: Ask for example patterns:**
78+- "What patterns should trigger this rule?"
79+- Show detected patterns
80+- Allow user to refine or add more
81+
82+### Step 3: Generate Rule Files
83+
84+For each confirmed behavior, create a `.claude/hookify.{rule-name}.local.md` file:
85+
86+**Rule naming convention:**
87+- Use kebab-case
88+- Be descriptive: `block-dangerous-rm`, `warn-console-log`, `require-tests-before-stop`
89+- Start with action verb: block, warn, prevent, require
90+
91+**File format:**
92+```markdown
93+---
94+name: {rule-name}
95+enabled: true
96+event: {bash|file|stop|prompt|all}
97+pattern: {regex pattern}
98+action: {warn|block}
99+---
100+
101+{Message to show Claude when rule triggers}
102+```
103+
104+**Action values:**
105+- `warn`: Show message but allow operation (default)
106+- `block`: Prevent operation or stop session
107+
108+**For more complex rules (multiple conditions):**
109+```markdown
110+---
111+name: {rule-name}
112+enabled: true
113+event: file
114+conditions:
115+ - field: file_path
116+ operator: regex_match
117+ pattern: \.env$
118+ - field: new_text
119+ operator: contains
120+ pattern: API_KEY
121+---
122+
123+{Warning message}
124+```
125+
126+### Step 4: Create Files and Confirm
127+
128+**IMPORTANT**: Rule files must be created in the current working directory's `.claude/` folder, NOT the plugin directory.
129+
130+Use the current working directory (where Claude Code was started) as the base path.
131+
132+1. Check if `.claude/` directory exists in current working directory
133+ - If not, create it first with: `mkdir -p .claude`
134+
135+2. Use Write tool to create each `.claude/hookify.{name}.local.md` file
136+ - Use relative path from current working directory: `.claude/hookify.{name}.local.md`
137+ - The path should resolve to the project's .claude directory, not the plugin's
138+
139+3. Show user what was created:
140+ ```
141+ Created 3 hookify rules:
142+ - .claude/hookify.dangerous-rm.local.md
143+ - .claude/hookify.console-log.local.md
144+ - .claude/hookify.sensitive-files.local.md
145+
146+ These rules will trigger on:
147+ - dangerous-rm: Bash commands matching "rm -rf"
148+ - console-log: Edits adding console.log statements
149+ - sensitive-files: Edits to .env or credentials files
150+ ```
151+
152+4. Verify files were created in the correct location by listing them
153+
154+5. Inform user: **"Rules are active immediately - no restart needed!"**
155+
156+ The hookify hooks are already loaded and will read your new rules on the next tool use.
157+
158+## Event Types Reference
159+
160+- **bash**: Matches Bash tool commands
161+- **file**: Matches Edit, Write, MultiEdit tools
162+- **stop**: Matches when agent wants to stop (use for completion checks)
163+- **prompt**: Matches when user submits prompts
164+- **all**: Matches all events
165+
166+## Pattern Writing Tips
167+
168+**Bash patterns:**
169+- Match dangerous commands: `rm\s+-rf|chmod\s+777|dd\s+if=`
170+- Match specific tools: `npm\s+install\s+|pip\s+install`
171+
172+**File patterns:**
173+- Match code patterns: `console\.log\(|eval\(|innerHTML\s*=`
174+- Match file paths: `\.env$|\.git/|node_modules/`
175+
176+**Stop patterns:**
177+- Check for missing steps: (check transcript or completion criteria)
178+
179+## Example Workflow
180+
181+**User says**: "/hookify Don't use rm -rf without asking me first"
182+
183+**Your response**:
184+1. Analyze: User wants to prevent rm -rf commands
185+2. Ask: "Should I block this command or just warn you?"
186+3. User selects: "Just warn"
187+4. Create `.claude/hookify.dangerous-rm.local.md`:
188+ ```markdown
189+ ---
190+ name: warn-dangerous-rm
191+ enabled: true
192+ event: bash
193+ pattern: rm\s+-rf
194+ ---
195+
196+ ⚠️ **Dangerous rm command detected**
197+
198+ You requested to be warned before using rm -rf.
199+ Please verify the path is correct.
200+ ```
201+5. Confirm: "Created hookify rule. It's active immediately - try triggering it!"
202+
203+## Important Notes
204+
205+- **No restart needed**: Rules take effect immediately on the next tool use
206+- **File location**: Create files in project's `.claude/` directory (current working directory), NOT the plugin's .claude/
207+- **Regex syntax**: Use Python regex syntax (raw strings, no need to escape in YAML)
208+- **Action types**: Rules can `warn` (default) or `block` operations
209+- **Testing**: Test rules immediately after creating them
210+
211+## Troubleshooting
212+
213+**If rule file creation fails:**
214+1. Check current working directory with pwd
215+2. Ensure `.claude/` directory exists (create with mkdir if needed)
216+3. Use absolute path if needed: `{cwd}/.claude/hookify.{name}.local.md`
217+4. Verify file was created with Glob or ls
218+
219+**If rule doesn't trigger after creation:**
220+1. Verify file is in project `.claude/` not plugin `.claude/`
221+2. Check file with Read tool to ensure pattern is correct
222+3. Test pattern with: `python3 -c "import re; print(re.search(r'pattern', 'test text'))"`
223+4. Verify `enabled: true` in frontmatter
224+5. Remember: Rules work immediately, no restart needed
225+
226+**If blocking seems too strict:**
227+1. Change `action: block` to `action: warn` in the rule file
228+2. Or adjust the pattern to be more specific
229+3. Changes take effect on next tool use
230+
231+Use TodoWrite to track your progress through the steps.
@@ -1,0 +1,82 @@
1+---
2+description: List all configured hookify rules
3+allowed-tools: ["Glob", "Read", "Skill"]
4+---
5+
6+# List Hookify Rules
7+
8+**Load hookify:writing-rules skill first** to understand rule format.
9+
10+Show all configured hookify rules in the project.
11+
12+## Steps
13+
14+1. Use Glob tool to find all hookify rule files:
15+ ```
16+ pattern: ".claude/hookify.*.local.md"
17+ ```
18+
19+2. For each file found:
20+ - Use Read tool to read the file
21+ - Extract frontmatter fields: name, enabled, event, pattern
22+ - Extract message preview (first 100 chars)
23+
24+3. Present results in a table:
25+
26+```
27+## Configured Hookify Rules
28+
29+| Name | Enabled | Event | Pattern | File |
30+|------|---------|-------|---------|------|
31+| warn-dangerous-rm | ✅ Yes | bash | rm\s+-rf | hookify.dangerous-rm.local.md |
32+| warn-console-log | ✅ Yes | file | console\.log\( | hookify.console-log.local.md |
33+| check-tests | ❌ No | stop | .* | hookify.require-tests.local.md |
34+
35+**Total**: 3 rules (2 enabled, 1 disabled)
36+```
37+
38+4. For each rule, show a brief preview:
39+```
40+### warn-dangerous-rm
41+**Event**: bash
42+**Pattern**: `rm\s+-rf`
43+**Message**: "⚠️ **Dangerous rm command detected!** This command could delete..."
44+
45+**Status**: ✅ Active
46+**File**: .claude/hookify.dangerous-rm.local.md
47+```
48+
49+5. Add helpful footer:
50+```
51+---
52+
53+To modify a rule: Edit the .local.md file directly
54+To disable a rule: Set `enabled: false` in frontmatter
55+To enable a rule: Set `enabled: true` in frontmatter
56+To delete a rule: Remove the .local.md file
57+To create a rule: Use `/hookify` command
58+
59+**Remember**: Changes take effect immediately - no restart needed
60+```
61+
62+## If No Rules Found
63+
64+If no hookify rules exist:
65+
66+```
67+## No Hookify Rules Configured
68+
69+You haven't created any hookify rules yet.
70+
71+To get started:
72+1. Use `/hookify` to analyze conversation and create rules
73+2. Or manually create `.claude/hookify.my-rule.local.md` files
74+3. See `/hookify:help` for documentation
75+
76+Example:
77+```
78+/hookify Warn me when I use console.log
79+```
80+
81+Check `${CLAUDE_PLUGIN_ROOT}/examples/` for example rule files.
82+```
No content changes (mode only).
@@ -1,0 +1,297 @@
1+#!/usr/bin/env python3
2+"""Configuration loader for hookify plugin.
3+
4+Loads and parses .claude/hookify.*.local.md files.
5+"""
6+
7+import os
8+import sys
9+import glob
10+import re
11+from typing import List, Optional, Dict, Any
12+from dataclasses import dataclass, field
13+
14+
15+@dataclass
16+class Condition:
17+ """A single condition for matching."""
18+ field: str # "command", "new_text", "old_text", "file_path", etc.
19+ operator: str # "regex_match", "contains", "equals", etc.
20+ pattern: str # Pattern to match
21+
22+ @classmethod
23+ def from_dict(cls, data: Dict[str, Any]) -> 'Condition':
24+ """Create Condition from dict."""
25+ return cls(
26+ field=data.get('field', ''),
27+ operator=data.get('operator', 'regex_match'),
28+ pattern=data.get('pattern', '')
29+ )
30+
31+
32+@dataclass
33+class Rule:
34+ """A hookify rule."""
35+ name: str
36+ enabled: bool
37+ event: str # "bash", "file", "stop", "all", etc.
38+ pattern: Optional[str] = None # Simple pattern (legacy)
39+ conditions: List[Condition] = field(default_factory=list)
40+ action: str = "warn" # "warn" or "block" (future)
41+ tool_matcher: Optional[str] = None # Override tool matching
42+ message: str = "" # Message body from markdown
43+
44+ @classmethod
45+ def from_dict(cls, frontmatter: Dict[str, Any], message: str) -> 'Rule':
46+ """Create Rule from frontmatter dict and message body."""
47+ # Handle both simple pattern and complex conditions
48+ conditions = []
49+
50+ # New style: explicit conditions list
51+ if 'conditions' in frontmatter:
52+ cond_list = frontmatter['conditions']
53+ if isinstance(cond_list, list):
54+ conditions = [Condition.from_dict(c) for c in cond_list]
55+
56+ # Legacy style: simple pattern field
57+ simple_pattern = frontmatter.get('pattern')
58+ if simple_pattern and not conditions:
59+ # Convert simple pattern to condition
60+ # Infer field from event
61+ event = frontmatter.get('event', 'all')
62+ if event == 'bash':
63+ field = 'command'
64+ elif event == 'file':
65+ field = 'new_text'
66+ else:
67+ field = 'content'
68+
69+ conditions = [Condition(
70+ field=field,
71+ operator='regex_match',
72+ pattern=simple_pattern
73+ )]
74+
75+ return cls(
76+ name=frontmatter.get('name', 'unnamed'),
77+ enabled=frontmatter.get('enabled', True),
78+ event=frontmatter.get('event', 'all'),
79+ pattern=simple_pattern,
80+ conditions=conditions,
81+ action=frontmatter.get('action', 'warn'),
82+ tool_matcher=frontmatter.get('tool_matcher'),
83+ message=message.strip()
84+ )
85+
86+
87+def extract_frontmatter(content: str) -> tuple[Dict[str, Any], str]:
88+ """Extract YAML frontmatter and message body from markdown.
89+
90+ Returns (frontmatter_dict, message_body).
91+
92+ Supports multi-line dictionary items in lists by preserving indentation.
93+ """
94+ if not content.startswith('---'):
95+ return {}, content
96+
97+ # Split on --- markers
98+ parts = content.split('---', 2)
99+ if len(parts) < 3:
100+ return {}, content
101+
102+ frontmatter_text = parts[1]
103+ message = parts[2].strip()
104+
105+ # Simple YAML parser that handles indented list items
106+ frontmatter = {}
107+ lines = frontmatter_text.split('\n')
108+
109+ current_key = None
110+ current_list = []
111+ current_dict = {}
112+ in_list = False
113+ in_dict_item = False
114+
115+ for line in lines:
116+ # Skip empty lines and comments
117+ stripped = line.strip()
118+ if not stripped or stripped.startswith('#'):
119+ continue
120+
121+ # Check indentation level
122+ indent = len(line) - len(line.lstrip())
123+
124+ # Top-level key (no indentation or minimal)
125+ if indent == 0 and ':' in line and not line.strip().startswith('-'):
126+ # Save previous list/dict if any
127+ if in_list and current_key:
128+ if in_dict_item and current_dict:
129+ current_list.append(current_dict)
130+ current_dict = {}
131+ frontmatter[current_key] = current_list
132+ in_list = False
133+ in_dict_item = False
134+ current_list = []
135+
136+ key, value = line.split(':', 1)
137+ key = key.strip()
138+ value = value.strip()
139+
140+ if not value:
141+ # Empty value - list or nested structure follows
142+ current_key = key
143+ in_list = True
144+ current_list = []
145+ else:
146+ # Simple key-value pair
147+ value = value.strip('"').strip("'")
148+ if value.lower() == 'true':
149+ value = True
150+ elif value.lower() == 'false':
151+ value = False
152+ frontmatter[key] = value
153+
154+ # List item (starts with -)
155+ elif stripped.startswith('-') and in_list:
156+ # Save previous dict item if any
157+ if in_dict_item and current_dict:
158+ current_list.append(current_dict)
159+ current_dict = {}
160+
161+ item_text = stripped[1:].strip()
162+
163+ # Check if this is an inline dict (key: value on same line)
164+ if ':' in item_text and ',' in item_text:
165+ # Inline comma-separated dict: "- field: command, operator: regex_match"
166+ item_dict = {}
167+ for part in item_text.split(','):
168+ if ':' in part:
169+ k, v = part.split(':', 1)
170+ item_dict[k.strip()] = v.strip().strip('"').strip("'")
171+ current_list.append(item_dict)
172+ in_dict_item = False
173+ elif ':' in item_text:
174+ # Start of multi-line dict item: "- field: command"
175+ in_dict_item = True
176+ k, v = item_text.split(':', 1)
177+ current_dict = {k.strip(): v.strip().strip('"').strip("'")}
178+ else:
179+ # Simple list item
180+ current_list.append(item_text.strip('"').strip("'"))
181+ in_dict_item = False
182+
183+ # Continuation of dict item (indented under list item)
184+ elif indent > 2 and in_dict_item and ':' in line:
185+ # This is a field of the current dict item
186+ k, v = stripped.split(':', 1)
187+ current_dict[k.strip()] = v.strip().strip('"').strip("'")
188+
189+ # Save final list/dict if any
190+ if in_list and current_key:
191+ if in_dict_item and current_dict:
192+ current_list.append(current_dict)
193+ frontmatter[current_key] = current_list
194+
195+ return frontmatter, message
196+
197+
198+def load_rules(event: Optional[str] = None) -> List[Rule]:
199+ """Load all hookify rules from .claude directory.
200+
201+ Args:
202+ event: Optional event filter ("bash", "file", "stop", etc.)
203+
204+ Returns:
205+ List of enabled Rule objects matching the event.
206+ """
207+ rules = []
208+
209+ # Find all hookify.*.local.md files
210+ pattern = os.path.join('.claude', 'hookify.*.local.md')
211+ files = glob.glob(pattern)
212+
213+ for file_path in files:
214+ try:
215+ rule = load_rule_file(file_path)
216+ if not rule:
217+ continue
218+
219+ # Filter by event if specified
220+ if event:
221+ if rule.event != 'all' and rule.event != event:
222+ continue
223+
224+ # Only include enabled rules
225+ if rule.enabled:
226+ rules.append(rule)
227+
228+ except (IOError, OSError, PermissionError) as e:
229+ # File I/O errors - log and continue
230+ print(f"Warning: Failed to read {file_path}: {e}", file=sys.stderr)
231+ continue
232+ except (ValueError, KeyError, AttributeError, TypeError) as e:
233+ # Parsing errors - log and continue
234+ print(f"Warning: Failed to parse {file_path}: {e}", file=sys.stderr)
235+ continue
236+ except Exception as e:
237+ # Unexpected errors - log with type details
238+ print(f"Warning: Unexpected error loading {file_path} ({type(e).__name__}): {e}", file=sys.stderr)
239+ continue
240+
241+ return rules
242+
243+
244+def load_rule_file(file_path: str) -> Optional[Rule]:
245+ """Load a single rule file.
246+
247+ Returns:
248+ Rule object or None if file is invalid.
249+ """
250+ try:
251+ with open(file_path, 'r') as f:
252+ content = f.read()
253+
254+ frontmatter, message = extract_frontmatter(content)
255+
256+ if not frontmatter:
257+ print(f"Warning: {file_path} missing YAML frontmatter (must start with ---)", file=sys.stderr)
258+ return None
259+
260+ rule = Rule.from_dict(frontmatter, message)
261+ return rule
262+
263+ except (IOError, OSError, PermissionError) as e:
264+ print(f"Error: Cannot read {file_path}: {e}", file=sys.stderr)
265+ return None
266+ except (ValueError, KeyError, AttributeError, TypeError) as e:
267+ print(f"Error: Malformed rule file {file_path}: {e}", file=sys.stderr)
268+ return None
269+ except UnicodeDecodeError as e:
270+ print(f"Error: Invalid encoding in {file_path}: {e}", file=sys.stderr)
271+ return None
272+ except Exception as e:
273+ print(f"Error: Unexpected error parsing {file_path} ({type(e).__name__}): {e}", file=sys.stderr)
274+ return None
275+
276+
277+# For testing
278+if __name__ == '__main__':
279+ import sys
280+
281+ # Test frontmatter parsing
282+ test_content = """---
283+name: test-rule
284+enabled: true
285+event: bash
286+pattern: "rm -rf"
287+---
288+
289+⚠️ Dangerous command detected!
290+"""
291+
292+ fm, msg = extract_frontmatter(test_content)
293+ print("Frontmatter:", fm)
294+ print("Message:", msg)
295+
296+ rule = Rule.from_dict(fm, msg)
297+ print("Rule:", rule)
@@ -1,0 +1,313 @@
1+#!/usr/bin/env python3
2+"""Rule evaluation engine for hookify plugin."""
3+
4+import re
5+import sys
6+from functools import lru_cache
7+from typing import List, Dict, Any, Optional
8+
9+# Import from local module
10+from hookify.core.config_loader import Rule, Condition
11+
12+
13+# Cache compiled regexes (max 128 patterns)
14+@lru_cache(maxsize=128)
15+def compile_regex(pattern: str) -> re.Pattern:
16+ """Compile regex pattern with caching.
17+
18+ Args:
19+ pattern: Regex pattern string
20+
21+ Returns:
22+ Compiled regex pattern
23+ """
24+ return re.compile(pattern, re.IGNORECASE)
25+
26+
27+class RuleEngine:
28+ """Evaluates rules against hook input data."""
29+
30+ def __init__(self):
31+ """Initialize rule engine."""
32+ # No need for instance cache anymore - using global lru_cache
33+ pass
34+
35+ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[str, Any]:
36+ """Evaluate all rules and return combined results.
37+
38+ Checks all rules and accumulates matches. Blocking rules take priority
39+ over warning rules. All matching rule messages are combined.
40+
41+ Args:
42+ rules: List of Rule objects to evaluate
43+ input_data: Hook input JSON (tool_name, tool_input, etc.)
44+
45+ Returns:
46+ Response dict with systemMessage, hookSpecificOutput, etc.
47+ Empty dict {} if no rules match.
48+ """
49+ hook_event = input_data.get('hook_event_name', '')
50+ blocking_rules = []
51+ warning_rules = []
52+
53+ for rule in rules:
54+ if self._rule_matches(rule, input_data):
55+ if rule.action == 'block':
56+ blocking_rules.append(rule)
57+ else:
58+ warning_rules.append(rule)
59+
60+ # If any blocking rules matched, block the operation
61+ if blocking_rules:
62+ messages = [f"**[{r.name}]**\n{r.message}" for r in blocking_rules]
63+ combined_message = "\n\n".join(messages)
64+
65+ # Use appropriate blocking format based on event type
66+ if hook_event == 'Stop':
67+ return {
68+ "decision": "block",
69+ "reason": combined_message,
70+ "systemMessage": combined_message
71+ }
72+ elif hook_event in ['PreToolUse', 'PostToolUse']:
73+ return {
74+ "hookSpecificOutput": {
75+ "hookEventName": hook_event,
76+ "permissionDecision": "deny"
77+ },
78+ "systemMessage": combined_message
79+ }
80+ else:
81+ # For other events, just show message
82+ return {
83+ "systemMessage": combined_message
84+ }
85+
86+ # If only warnings, show them but allow operation
87+ if warning_rules:
88+ messages = [f"**[{r.name}]**\n{r.message}" for r in warning_rules]
89+ return {
90+ "systemMessage": "\n\n".join(messages)
91+ }
92+
93+ # No matches - allow operation
94+ return {}
95+
96+ def _rule_matches(self, rule: Rule, input_data: Dict[str, Any]) -> bool:
97+ """Check if rule matches input data.
98+
99+ Args:
100+ rule: Rule to evaluate
101+ input_data: Hook input data
102+
103+ Returns:
104+ True if rule matches, False otherwise
105+ """
106+ # Extract tool information
107+ tool_name = input_data.get('tool_name', '')
108+ tool_input = input_data.get('tool_input', {})
109+
110+ # Check tool matcher if specified
111+ if rule.tool_matcher:
112+ if not self._matches_tool(rule.tool_matcher, tool_name):
113+ return False
114+
115+ # If no conditions, don't match
116+ # (Rules must have at least one condition to be valid)
117+ if not rule.conditions:
118+ return False
119+
120+ # All conditions must match
121+ for condition in rule.conditions:
122+ if not self._check_condition(condition, tool_name, tool_input, input_data):
123+ return False
124+
125+ return True
126+
127+ def _matches_tool(self, matcher: str, tool_name: str) -> bool:
128+ """Check if tool_name matches the matcher pattern.
129+
130+ Args:
131+ matcher: Pattern like "Bash", "Edit|Write", "*"
132+ tool_name: Actual tool name
133+
134+ Returns:
135+ True if matches
136+ """
137+ if matcher == '*':
138+ return True
139+
140+ # Split on | for OR matching
141+ patterns = matcher.split('|')
142+ return tool_name in patterns
143+
144+ def _check_condition(self, condition: Condition, tool_name: str,
145+ tool_input: Dict[str, Any], input_data: Dict[str, Any] = None) -> bool:
146+ """Check if a single condition matches.
147+
148+ Args:
149+ condition: Condition to check
150+ tool_name: Tool being used
151+ tool_input: Tool input dict
152+ input_data: Full hook input data (for Stop events, etc.)
153+
154+ Returns:
155+ True if condition matches
156+ """
157+ # Extract the field value to check
158+ field_value = self._extract_field(condition.field, tool_name, tool_input, input_data)
159+ if field_value is None:
160+ return False
161+
162+ # Apply operator
163+ operator = condition.operator
164+ pattern = condition.pattern
165+
166+ if operator == 'regex_match':
167+ return self._regex_match(pattern, field_value)
168+ elif operator == 'contains':
169+ return pattern in field_value
170+ elif operator == 'equals':
171+ return pattern == field_value
172+ elif operator == 'not_contains':
173+ return pattern not in field_value
174+ elif operator == 'starts_with':
175+ return field_value.startswith(pattern)
176+ elif operator == 'ends_with':
177+ return field_value.endswith(pattern)
178+ else:
179+ # Unknown operator
180+ return False
181+
182+ def _extract_field(self, field: str, tool_name: str,
183+ tool_input: Dict[str, Any], input_data: Dict[str, Any] = None) -> Optional[str]:
184+ """Extract field value from tool input or hook input data.
185+
186+ Args:
187+ field: Field name like "command", "new_text", "file_path", "reason", "transcript"
188+ tool_name: Tool being used (may be empty for Stop events)
189+ tool_input: Tool input dict
190+ input_data: Full hook input (for accessing transcript_path, reason, etc.)
191+
192+ Returns:
193+ Field value as string, or None if not found
194+ """
195+ # Direct tool_input fields
196+ if field in tool_input:
197+ value = tool_input[field]
198+ if isinstance(value, str):
199+ return value
200+ return str(value)
201+
202+ # For Stop events and other non-tool events, check input_data
203+ if input_data:
204+ # Stop event specific fields
205+ if field == 'reason':
206+ return input_data.get('reason', '')
207+ elif field == 'transcript':
208+ # Read transcript file if path provided
209+ transcript_path = input_data.get('transcript_path')
210+ 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 ''
226+ elif field == 'user_prompt':
227+ # For UserPromptSubmit events
228+ return input_data.get('user_prompt', '')
229+
230+ # Handle special cases by tool type
231+ if tool_name == 'Bash':
232+ if field == 'command':
233+ return tool_input.get('command', '')
234+
235+ elif tool_name in ['Write', 'Edit']:
236+ if field == 'content':
237+ # Write uses 'content', Edit has 'new_string'
238+ return tool_input.get('content') or tool_input.get('new_string', '')
239+ elif field == 'new_text' or field == 'new_string':
240+ return tool_input.get('new_string', '')
241+ elif field == 'old_text' or field == 'old_string':
242+ return tool_input.get('old_string', '')
243+ elif field == 'file_path':
244+ return tool_input.get('file_path', '')
245+
246+ elif tool_name == 'MultiEdit':
247+ if field == 'file_path':
248+ return tool_input.get('file_path', '')
249+ elif field in ['new_text', 'content']:
250+ # Concatenate all edits
251+ edits = tool_input.get('edits', [])
252+ return ' '.join(e.get('new_string', '') for e in edits)
253+
254+ return None
255+
256+ def _regex_match(self, pattern: str, text: str) -> bool:
257+ """Check if pattern matches text using regex.
258+
259+ Args:
260+ pattern: Regex pattern
261+ text: Text to match against
262+
263+ Returns:
264+ True if pattern matches
265+ """
266+ try:
267+ # Use cached compiled regex (LRU cache with max 128 patterns)
268+ regex = compile_regex(pattern)
269+ return bool(regex.search(text))
270+
271+ except re.error as e:
272+ print(f"Invalid regex pattern '{pattern}': {e}", file=sys.stderr)
273+ return False
274+
275+
276+# For testing
277+if __name__ == '__main__':
278+ from hookify.core.config_loader import Condition, Rule
279+
280+ # Test rule evaluation
281+ rule = Rule(
282+ name="test-rm",
283+ enabled=True,
284+ event="bash",
285+ conditions=[
286+ Condition(field="command", operator="regex_match", pattern=r"rm\s+-rf")
287+ ],
288+ message="Dangerous rm command!"
289+ )
290+
291+ engine = RuleEngine()
292+
293+ # Test matching input
294+ test_input = {
295+ "tool_name": "Bash",
296+ "tool_input": {
297+ "command": "rm -rf /tmp/test"
298+ }
299+ }
300+
301+ result = engine.evaluate_rules([rule], test_input)
302+ print("Match result:", result)
303+
304+ # Test non-matching input
305+ test_input2 = {
306+ "tool_name": "Bash",
307+ "tool_input": {
308+ "command": "ls -la"
309+ }
310+ }
311+
312+ result2 = engine.evaluate_rules([rule], test_input2)
313+ print("Non-match result:", result2)
@@ -1,0 +1,14 @@
1+---
2+name: warn-console-log
3+enabled: true
4+event: file
5+pattern: console\.log\(
6+action: warn
7+---
8+
9+🔍 **Console.log detected**
10+
11+You're adding a console.log statement. Please consider:
12+- Is this for debugging or should it be proper logging?
13+- Will this ship to production?
14+- Should this use a logging library instead?
@@ -1,0 +1,14 @@
1+---
2+name: block-dangerous-rm
3+enabled: true
4+event: bash
5+pattern: rm\s+-rf
6+action: block
7+---
8+
9+⚠️ **Dangerous rm command detected!**
10+
11+This command could delete important files. Please:
12+- Verify the path is correct
13+- Consider using a safer approach
14+- Make sure you have backups
@@ -1,0 +1,22 @@
1+---
2+name: require-tests-run
3+enabled: false
4+event: stop
5+action: block
6+conditions:
7+ - field: transcript
8+ operator: not_contains
9+ pattern: npm test|pytest|cargo test
10+---
11+
12+**Tests not detected in transcript!**
13+
14+Before stopping, please run tests to verify your changes work correctly.
15+
16+Look for test commands like:
17+- `npm test`
18+- `pytest`
19+- `cargo test`
20+
21+**Note:** This rule blocks stopping if no test commands appear in the transcript.
22+Enable this rule only when you want strict test enforcement.
@@ -1,0 +1,18 @@
1+---
2+name: warn-sensitive-files
3+enabled: true
4+event: file
5+action: warn
6+conditions:
7+ - field: file_path
8+ operator: regex_match
9+ pattern: \.env$|\.env\.|credentials|secrets
10+---
11+
12+🔐 **Sensitive file detected**
13+
14+You're editing a file that may contain sensitive data:
15+- Ensure credentials are not hardcoded
16+- Use environment variables for secrets
17+- Verify this file is in .gitignore
18+- Consider using a secrets manager
No content changes (mode only).
@@ -1,0 +1,49 @@
1+{
2+ "description": "Hookify plugin - User-configurable hooks from .local.md files",
3+ "hooks": {
4+ "PreToolUse": [
5+ {
6+ "hooks": [
7+ {
8+ "type": "command",
9+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py",
10+ "timeout": 10
11+ }
12+ ]
13+ }
14+ ],
15+ "PostToolUse": [
16+ {
17+ "hooks": [
18+ {
19+ "type": "command",
20+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/posttooluse.py",
21+ "timeout": 10
22+ }
23+ ]
24+ }
25+ ],
26+ "Stop": [
27+ {
28+ "hooks": [
29+ {
30+ "type": "command",
31+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop.py",
32+ "timeout": 10
33+ }
34+ ]
35+ }
36+ ],
37+ "UserPromptSubmit": [
38+ {
39+ "hooks": [
40+ {
41+ "type": "command",
42+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/userpromptsubmit.py",
43+ "timeout": 10
44+ }
45+ ]
46+ }
47+ ]
48+ }
49+}
@@ -1,0 +1,66 @@
1+#!/usr/bin/env python3
2+"""PostToolUse hook executor for hookify plugin.
3+
4+This script is called by Claude Code after a tool executes.
5+It reads .claude/hookify.*.local.md files and evaluates rules.
6+"""
7+
8+import os
9+import sys
10+import json
11+
12+# CRITICAL: Add plugin root to Python path for imports
13+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
14+if PLUGIN_ROOT:
15+ parent_dir = os.path.dirname(PLUGIN_ROOT)
16+ if parent_dir not in sys.path:
17+ sys.path.insert(0, parent_dir)
18+ if PLUGIN_ROOT not in sys.path:
19+ sys.path.insert(0, PLUGIN_ROOT)
20+
21+try:
22+ from hookify.core.config_loader import load_rules
23+ from hookify.core.rule_engine import RuleEngine
24+except ImportError as e:
25+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
26+ print(json.dumps(error_msg), file=sys.stdout)
27+ sys.exit(0)
28+
29+
30+def main():
31+ """Main entry point for PostToolUse hook."""
32+ try:
33+ # Read input from stdin
34+ input_data = json.load(sys.stdin)
35+
36+ # Determine event type based on tool
37+ tool_name = input_data.get('tool_name', '')
38+ event = None
39+ if tool_name == 'Bash':
40+ event = 'bash'
41+ elif tool_name in ['Edit', 'Write', 'MultiEdit']:
42+ event = 'file'
43+
44+ # Load rules
45+ rules = load_rules(event=event)
46+
47+ # Evaluate rules
48+ engine = RuleEngine()
49+ result = engine.evaluate_rules(rules, input_data)
50+
51+ # Always output JSON (even if empty)
52+ print(json.dumps(result), file=sys.stdout)
53+
54+ except Exception as e:
55+ error_output = {
56+ "systemMessage": f"Hookify error: {str(e)}"
57+ }
58+ print(json.dumps(error_output), file=sys.stdout)
59+
60+ finally:
61+ # ALWAYS exit 0
62+ sys.exit(0)
63+
64+
65+if __name__ == '__main__':
66+ main()
@@ -1,0 +1,74 @@
1+#!/usr/bin/env python3
2+"""PreToolUse hook executor for hookify plugin.
3+
4+This script is called by Claude Code before any tool executes.
5+It reads .claude/hookify.*.local.md files and evaluates rules.
6+"""
7+
8+import os
9+import sys
10+import json
11+
12+# CRITICAL: Add plugin root to Python path for imports
13+# We need to add the parent of the plugin directory so Python can find "hookify" package
14+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
15+if PLUGIN_ROOT:
16+ # Add the parent directory of the plugin
17+ parent_dir = os.path.dirname(PLUGIN_ROOT)
18+ if parent_dir not in sys.path:
19+ sys.path.insert(0, parent_dir)
20+
21+ # Also add PLUGIN_ROOT itself in case we have other scripts
22+ if PLUGIN_ROOT not in sys.path:
23+ sys.path.insert(0, PLUGIN_ROOT)
24+
25+try:
26+ from hookify.core.config_loader import load_rules
27+ from hookify.core.rule_engine import RuleEngine
28+except ImportError as e:
29+ # If imports fail, allow operation and log error
30+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
31+ print(json.dumps(error_msg), file=sys.stdout)
32+ sys.exit(0)
33+
34+
35+def main():
36+ """Main entry point for PreToolUse hook."""
37+ try:
38+ # Read input from stdin
39+ input_data = json.load(sys.stdin)
40+
41+ # Determine event type for filtering
42+ # For PreToolUse, we use tool_name to determine "bash" vs "file" event
43+ tool_name = input_data.get('tool_name', '')
44+
45+ event = None
46+ if tool_name == 'Bash':
47+ event = 'bash'
48+ elif tool_name in ['Edit', 'Write', 'MultiEdit']:
49+ event = 'file'
50+
51+ # Load rules
52+ rules = load_rules(event=event)
53+
54+ # Evaluate rules
55+ engine = RuleEngine()
56+ result = engine.evaluate_rules(rules, input_data)
57+
58+ # Always output JSON (even if empty)
59+ print(json.dumps(result), file=sys.stdout)
60+
61+ except Exception as e:
62+ # On any error, allow the operation and log
63+ error_output = {
64+ "systemMessage": f"Hookify error: {str(e)}"
65+ }
66+ print(json.dumps(error_output), file=sys.stdout)
67+
68+ finally:
69+ # ALWAYS exit 0 - never block operations due to hook errors
70+ sys.exit(0)
71+
72+
73+if __name__ == '__main__':
74+ main()
@@ -1,0 +1,59 @@
1+#!/usr/bin/env python3
2+"""Stop hook executor for hookify plugin.
3+
4+This script is called by Claude Code when agent wants to stop.
5+It reads .claude/hookify.*.local.md files and evaluates stop rules.
6+"""
7+
8+import os
9+import sys
10+import json
11+
12+# CRITICAL: Add plugin root to Python path for imports
13+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
14+if PLUGIN_ROOT:
15+ parent_dir = os.path.dirname(PLUGIN_ROOT)
16+ if parent_dir not in sys.path:
17+ sys.path.insert(0, parent_dir)
18+ if PLUGIN_ROOT not in sys.path:
19+ sys.path.insert(0, PLUGIN_ROOT)
20+
21+try:
22+ from hookify.core.config_loader import load_rules
23+ from hookify.core.rule_engine import RuleEngine
24+except ImportError as e:
25+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
26+ print(json.dumps(error_msg), file=sys.stdout)
27+ sys.exit(0)
28+
29+
30+def main():
31+ """Main entry point for Stop hook."""
32+ try:
33+ # Read input from stdin
34+ input_data = json.load(sys.stdin)
35+
36+ # Load stop rules
37+ rules = load_rules(event='stop')
38+
39+ # Evaluate rules
40+ engine = RuleEngine()
41+ result = engine.evaluate_rules(rules, input_data)
42+
43+ # Always output JSON (even if empty)
44+ print(json.dumps(result), file=sys.stdout)
45+
46+ except Exception as e:
47+ # On any error, allow the operation
48+ error_output = {
49+ "systemMessage": f"Hookify error: {str(e)}"
50+ }
51+ print(json.dumps(error_output), file=sys.stdout)
52+
53+ finally:
54+ # ALWAYS exit 0
55+ sys.exit(0)
56+
57+
58+if __name__ == '__main__':
59+ main()
@@ -1,0 +1,58 @@
1+#!/usr/bin/env python3
2+"""UserPromptSubmit hook executor for hookify plugin.
3+
4+This script is called by Claude Code when user submits a prompt.
5+It reads .claude/hookify.*.local.md files and evaluates rules.
6+"""
7+
8+import os
9+import sys
10+import json
11+
12+# CRITICAL: Add plugin root to Python path for imports
13+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
14+if PLUGIN_ROOT:
15+ parent_dir = os.path.dirname(PLUGIN_ROOT)
16+ if parent_dir not in sys.path:
17+ sys.path.insert(0, parent_dir)
18+ if PLUGIN_ROOT not in sys.path:
19+ sys.path.insert(0, PLUGIN_ROOT)
20+
21+try:
22+ from hookify.core.config_loader import load_rules
23+ from hookify.core.rule_engine import RuleEngine
24+except ImportError as e:
25+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
26+ print(json.dumps(error_msg), file=sys.stdout)
27+ sys.exit(0)
28+
29+
30+def main():
31+ """Main entry point for UserPromptSubmit hook."""
32+ try:
33+ # Read input from stdin
34+ input_data = json.load(sys.stdin)
35+
36+ # Load user prompt rules
37+ rules = load_rules(event='prompt')
38+
39+ # Evaluate rules
40+ engine = RuleEngine()
41+ result = engine.evaluate_rules(rules, input_data)
42+
43+ # Always output JSON (even if empty)
44+ print(json.dumps(result), file=sys.stdout)
45+
46+ except Exception as e:
47+ error_output = {
48+ "systemMessage": f"Hookify error: {str(e)}"
49+ }
50+ print(json.dumps(error_output), file=sys.stdout)
51+
52+ finally:
53+ # ALWAYS exit 0
54+ sys.exit(0)
55+
56+
57+if __name__ == '__main__':
58+ main()
No content changes (mode only).
@@ -1,0 +1,374 @@
1+---
2+name: Writing Hookify Rules
3+description: This skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
4+version: 0.1.0
5+---
6+
7+# Writing Hookify Rules
8+
9+## Overview
10+
11+Hookify rules are markdown files with YAML frontmatter that define patterns to watch for and messages to show when those patterns match. Rules are stored in `.claude/hookify.{rule-name}.local.md` files.
12+
13+## Rule File Format
14+
15+### Basic Structure
16+
17+```markdown
18+---
19+name: rule-identifier
20+enabled: true
21+event: bash|file|stop|prompt|all
22+pattern: regex-pattern-here
23+---
24+
25+Message to show Claude when this rule triggers.
26+Can include markdown formatting, warnings, suggestions, etc.
27+```
28+
29+### Frontmatter Fields
30+
31+**name** (required): Unique identifier for the rule
32+- Use kebab-case: `warn-dangerous-rm`, `block-console-log`
33+- Be descriptive and action-oriented
34+- Start with verb: warn, prevent, block, require, check
35+
36+**enabled** (required): Boolean to activate/deactivate
37+- `true`: Rule is active
38+- `false`: Rule is disabled (won't trigger)
39+- Can toggle without deleting rule
40+
41+**event** (required): Which hook event to trigger on
42+- `bash`: Bash tool commands
43+- `file`: Edit, Write, MultiEdit tools
44+- `stop`: When agent wants to stop
45+- `prompt`: When user submits a prompt
46+- `all`: All events
47+
48+**action** (optional): What to do when rule matches
49+- `warn`: Show message but allow operation (default)
50+- `block`: Prevent operation (PreToolUse) or stop session (Stop events)
51+- If omitted, defaults to `warn`
52+
53+**pattern** (simple format): Regex pattern to match
54+- Used for simple single-condition rules
55+- Matches against command (bash) or new_text (file)
56+- Python regex syntax
57+
58+**Example:**
59+```yaml
60+event: bash
61+pattern: rm\s+-rf
62+```
63+
64+### Advanced Format (Multiple Conditions)
65+
66+For complex rules with multiple conditions:
67+
68+```markdown
69+---
70+name: warn-env-file-edits
71+enabled: true
72+event: file
73+conditions:
74+ - field: file_path
75+ operator: regex_match
76+ pattern: \.env$
77+ - field: new_text
78+ operator: contains
79+ pattern: API_KEY
80+---
81+
82+You're adding an API key to a .env file. Ensure this file is in .gitignore!
83+```
84+
85+**Condition fields:**
86+- `field`: Which field to check
87+ - For bash: `command`
88+ - For file: `file_path`, `new_text`, `old_text`, `content`
89+- `operator`: How to match
90+ - `regex_match`: Regex pattern matching
91+ - `contains`: Substring check
92+ - `equals`: Exact match
93+ - `not_contains`: Substring must NOT be present
94+ - `starts_with`: Prefix check
95+ - `ends_with`: Suffix check
96+- `pattern`: Pattern or string to match
97+
98+**All conditions must match for rule to trigger.**
99+
100+## Message Body
101+
102+The markdown content after frontmatter is shown to Claude when the rule triggers.
103+
104+**Good messages:**
105+- Explain what was detected
106+- Explain why it's problematic
107+- Suggest alternatives or best practices
108+- Use formatting for clarity (bold, lists, etc.)
109+
110+**Example:**
111+```markdown
112+⚠️ **Console.log detected!**
113+
114+You're adding console.log to production code.
115+
116+**Why this matters:**
117+- Debug logs shouldn't ship to production
118+- Console.log can expose sensitive data
119+- Impacts browser performance
120+
121+**Alternatives:**
122+- Use a proper logging library
123+- Remove before committing
124+- Use conditional debug builds
125+```
126+
127+## Event Type Guide
128+
129+### bash Events
130+
131+Match Bash command patterns:
132+
133+```markdown
134+---
135+event: bash
136+pattern: sudo\s+|rm\s+-rf|chmod\s+777
137+---
138+
139+Dangerous command detected!
140+```
141+
142+**Common patterns:**
143+- Dangerous commands: `rm\s+-rf`, `dd\s+if=`, `mkfs`
144+- Privilege escalation: `sudo\s+`, `su\s+`
145+- Permission issues: `chmod\s+777`, `chown\s+root`
146+
147+### file Events
148+
149+Match Edit/Write/MultiEdit operations:
150+
151+```markdown
152+---
153+event: file
154+pattern: console\.log\(|eval\(|innerHTML\s*=
155+---
156+
157+Potentially problematic code pattern detected!
158+```
159+
160+**Match on different fields:**
161+```markdown
162+---
163+event: file
164+conditions:
165+ - field: file_path
166+ operator: regex_match
167+ pattern: \.tsx?$
168+ - field: new_text
169+ operator: regex_match
170+ pattern: console\.log\(
171+---
172+
173+Console.log in TypeScript file!
174+```
175+
176+**Common patterns:**
177+- Debug code: `console\.log\(`, `debugger`, `print\(`
178+- Security risks: `eval\(`, `innerHTML\s*=`, `dangerouslySetInnerHTML`
179+- Sensitive files: `\.env$`, `credentials`, `\.pem$`
180+- Generated files: `node_modules/`, `dist/`, `build/`
181+
182+### stop Events
183+
184+Match when agent wants to stop (completion checks):
185+
186+```markdown
187+---
188+event: stop
189+pattern: .*
190+---
191+
192+Before stopping, verify:
193+- [ ] Tests were run
194+- [ ] Build succeeded
195+- [ ] Documentation updated
196+```
197+
198+**Use for:**
199+- Reminders about required steps
200+- Completion checklists
201+- Process enforcement
202+
203+### prompt Events
204+
205+Match user prompt content (advanced):
206+
207+```markdown
208+---
209+event: prompt
210+conditions:
211+ - field: user_prompt
212+ operator: contains
213+ pattern: deploy to production
214+---
215+
216+Production deployment checklist:
217+- [ ] Tests passing?
218+- [ ] Reviewed by team?
219+- [ ] Monitoring ready?
220+```
221+
222+## Pattern Writing Tips
223+
224+### Regex Basics
225+
226+**Literal characters:** Most characters match themselves
227+- `rm` matches "rm"
228+- `console.log` matches "console.log"
229+
230+**Special characters need escaping:**
231+- `.` (any char) → `\.` (literal dot)
232+- `(` `)` → `\(` `\)` (literal parens)
233+- `[` `]` → `\[` `\]` (literal brackets)
234+
235+**Common metacharacters:**
236+- `\s` - whitespace (space, tab, newline)
237+- `\d` - digit (0-9)
238+- `\w` - word character (a-z, A-Z, 0-9, _)
239+- `.` - any character
240+- `+` - one or more
241+- `*` - zero or more
242+- `?` - zero or one
243+- `|` - OR
244+
245+**Examples:**
246+```
247+rm\s+-rf Matches: rm -rf, rm -rf
248+console\.log\( Matches: console.log(
249+(eval|exec)\( Matches: eval( or exec(
250+chmod\s+777 Matches: chmod 777, chmod 777
251+API_KEY\s*= Matches: API_KEY=, API_KEY =
252+```
253+
254+### Testing Patterns
255+
256+Test regex patterns before using:
257+
258+```bash
259+python3 -c "import re; print(re.search(r'your_pattern', 'test text'))"
260+```
261+
262+Or use online regex testers (regex101.com with Python flavor).
263+
264+### Common Pitfalls
265+
266+**Too broad:**
267+```yaml
268+pattern: log # Matches "log", "login", "dialog", "catalog"
269+```
270+Better: `console\.log\(|logger\.`
271+
272+**Too specific:**
273+```yaml
274+pattern: rm -rf /tmp # Only matches exact path
275+```
276+Better: `rm\s+-rf`
277+
278+**Escaping issues:**
279+- YAML quoted strings: `"pattern"` requires double backslashes `\\s`
280+- YAML unquoted: `pattern: \s` works as-is
281+- **Recommendation**: Use unquoted patterns in YAML
282+
283+## File Organization
284+
285+**Location:** All rules in `.claude/` directory
286+**Naming:** `.claude/hookify.{descriptive-name}.local.md`
287+**Gitignore:** Add `.claude/*.local.md` to `.gitignore`
288+
289+**Good names:**
290+- `hookify.dangerous-rm.local.md`
291+- `hookify.console-log.local.md`
292+- `hookify.require-tests.local.md`
293+- `hookify.sensitive-files.local.md`
294+
295+**Bad names:**
296+- `hookify.rule1.local.md` (not descriptive)
297+- `hookify.md` (missing .local)
298+- `danger.local.md` (missing hookify prefix)
299+
300+## Workflow
301+
302+### Creating a Rule
303+
304+1. Identify unwanted behavior
305+2. Determine which tool is involved (Bash, Edit, etc.)
306+3. Choose event type (bash, file, stop, etc.)
307+4. Write regex pattern
308+5. Create `.claude/hookify.{name}.local.md` file in project root
309+6. Test immediately - rules are read dynamically on next tool use
310+
311+### Refining a Rule
312+
313+1. Edit the `.local.md` file
314+2. Adjust pattern or message
315+3. Test immediately - changes take effect on next tool use
316+
317+### Disabling a Rule
318+
319+**Temporary:** Set `enabled: false` in frontmatter
320+**Permanent:** Delete the `.local.md` file
321+
322+## Examples
323+
324+See `${CLAUDE_PLUGIN_ROOT}/examples/` for complete examples:
325+- `dangerous-rm.local.md` - Block dangerous rm commands
326+- `console-log-warning.local.md` - Warn about console.log
327+- `sensitive-files-warning.local.md` - Warn about editing .env files
328+
329+## Quick Reference
330+
331+**Minimum viable rule:**
332+```markdown
333+---
334+name: my-rule
335+enabled: true
336+event: bash
337+pattern: dangerous_command
338+---
339+
340+Warning message here
341+```
342+
343+**Rule with conditions:**
344+```markdown
345+---
346+name: my-rule
347+enabled: true
348+event: file
349+conditions:
350+ - field: file_path
351+ operator: regex_match
352+ pattern: \.ts$
353+ - field: new_text
354+ operator: contains
355+ pattern: any
356+---
357+
358+Warning message
359+```
360+
361+**Event types:**
362+- `bash` - Bash commands
363+- `file` - File edits
364+- `stop` - Completion checks
365+- `prompt` - User input
366+- `all` - All events
367+
368+**Field options:**
369+- Bash: `command`
370+- File: `file_path`, `new_text`, `old_text`, `content`
371+- Prompt: `user_prompt`
372+
373+**Operators:**
374+- `regex_match`, `contains`, `equals`, `not_contains`, `starts_with`, `ends_with`
No content changes (mode only).
@@ -1,0 +1,9 @@
1+{
2+ "name": "ralph-wiggum",
3+ "version": "1.0.0",
4+ "description": "Implementation of the Ralph Wiggum technique - continuous self-referential AI loops for interactive iterative development. Run Claude in a while-true loop with the same prompt until task completion.",
5+ "author": {
6+ "name": "Daisy Hollman",
7+ "email": "daisy@anthropic.com"
8+ }
9+}
@@ -1,0 +1,179 @@
1+# Ralph Wiggum Plugin
2+
3+Implementation of the Ralph Wiggum technique for iterative, self-referential AI development loops in Claude Code.
4+
5+## What is Ralph?
6+
7+Ralph is a development methodology based on continuous AI agent loops. As Geoffrey Huntley describes it: **"Ralph is a Bash loop"** - a simple `while true` that repeatedly feeds an AI agent a prompt file, allowing it to iteratively improve its work until completion.
8+
9+The technique is named after Ralph Wiggum from The Simpsons, embodying the philosophy of persistent iteration despite setbacks.
10+
11+### Core Concept
12+
13+This plugin implements Ralph using a **Stop hook** that intercepts Claude's exit attempts:
14+
15+```bash
16+# You run ONCE:
17+/ralph-loop "Your task description" --completion-promise "DONE"
18+
19+# Then Claude Code automatically:
20+# 1. Works on the task
21+# 2. Tries to exit
22+# 3. Stop hook blocks exit
23+# 4. Stop hook feeds the SAME prompt back
24+# 5. Repeat until completion
25+```
26+
27+The loop happens **inside your current session** - you don't need external bash loops. The Stop hook in `hooks/stop-hook.sh` creates the self-referential feedback loop by blocking normal session exit.
28+
29+This creates a **self-referential feedback loop** where:
30+- The prompt never changes between iterations
31+- Claude's previous work persists in files
32+- Each iteration sees modified files and git history
33+- Claude autonomously improves by reading its own past work in files
34+
35+## Quick Start
36+
37+```bash
38+/ralph-loop "Build a REST API for todos. Requirements: CRUD operations, input validation, tests. Output <promise>COMPLETE</promise> when done." --completion-promise "COMPLETE" --max-iterations 50
39+```
40+
41+Claude will:
42+- Implement the API iteratively
43+- Run tests and see failures
44+- Fix bugs based on test output
45+- Iterate until all requirements met
46+- Output the completion promise when done
47+
48+## Commands
49+
50+### /ralph-loop
51+
52+Start a Ralph loop in your current session.
53+
54+**Usage:**
55+```bash
56+/ralph-loop "<prompt>" --max-iterations <n> --completion-promise "<text>"
57+```
58+
59+**Options:**
60+- `--max-iterations <n>` - Stop after N iterations (default: unlimited)
61+- `--completion-promise <text>` - Phrase that signals completion
62+
63+### /cancel-ralph
64+
65+Cancel the active Ralph loop.
66+
67+**Usage:**
68+```bash
69+/cancel-ralph
70+```
71+
72+## Prompt Writing Best Practices
73+
74+### 1. Clear Completion Criteria
75+
76+❌ Bad: "Build a todo API and make it good."
77+
78+✅ Good:
79+```markdown
80+Build a REST API for todos.
81+
82+When complete:
83+- All CRUD endpoints working
84+- Input validation in place
85+- Tests passing (coverage > 80%)
86+- README with API docs
87+- Output: <promise>COMPLETE</promise>
88+```
89+
90+### 2. Incremental Goals
91+
92+❌ Bad: "Create a complete e-commerce platform."
93+
94+✅ Good:
95+```markdown
96+Phase 1: User authentication (JWT, tests)
97+Phase 2: Product catalog (list/search, tests)
98+Phase 3: Shopping cart (add/remove, tests)
99+
100+Output <promise>COMPLETE</promise> when all phases done.
101+```
102+
103+### 3. Self-Correction
104+
105+❌ Bad: "Write code for feature X."
106+
107+✅ Good:
108+```markdown
109+Implement feature X following TDD:
110+1. Write failing tests
111+2. Implement feature
112+3. Run tests
113+4. If any fail, debug and fix
114+5. Refactor if needed
115+6. Repeat until all green
116+7. Output: <promise>COMPLETE</promise>
117+```
118+
119+### 4. Escape Hatches
120+
121+Always use `--max-iterations` as a safety net to prevent infinite loops on impossible tasks:
122+
123+```bash
124+# Recommended: Always set a reasonable iteration limit
125+/ralph-loop "Try to implement feature X" --max-iterations 20
126+
127+# In your prompt, include what to do if stuck:
128+# "After 15 iterations, if not complete:
129+# - Document what's blocking progress
130+# - List what was attempted
131+# - Suggest alternative approaches"
132+```
133+
134+**Note**: The `--completion-promise` uses exact string matching, so you cannot use it for multiple completion conditions (like "SUCCESS" vs "BLOCKED"). Always rely on `--max-iterations` as your primary safety mechanism.
135+
136+## Philosophy
137+
138+Ralph embodies several key principles:
139+
140+### 1. Iteration > Perfection
141+Don't aim for perfect on first try. Let the loop refine the work.
142+
143+### 2. Failures Are Data
144+"Deterministically bad" means failures are predictable and informative. Use them to tune prompts.
145+
146+### 3. Operator Skill Matters
147+Success depends on writing good prompts, not just having a good model.
148+
149+### 4. Persistence Wins
150+Keep trying until success. The loop handles retry logic automatically.
151+
152+## When to Use Ralph
153+
154+**Good for:**
155+- Well-defined tasks with clear success criteria
156+- Tasks requiring iteration and refinement (e.g., getting tests to pass)
157+- Greenfield projects where you can walk away
158+- Tasks with automatic verification (tests, linters)
159+
160+**Not good for:**
161+- Tasks requiring human judgment or design decisions
162+- One-shot operations
163+- Tasks with unclear success criteria
164+- Production debugging (use targeted debugging instead)
165+
166+## Real-World Results
167+
168+- Successfully generated 6 repositories overnight in Y Combinator hackathon testing
169+- One $50k contract completed for $297 in API costs
170+- Created entire programming language ("cursed") over 3 months using this approach
171+
172+## Learn More
173+
174+- Original technique: https://ghuntley.com/ralph/
175+- Ralph Orchestrator: https://github.com/mikeyobrien/ralph-orchestrator
176+
177+## For Help
178+
179+Run `/help` in Claude Code for detailed command reference and examples.
@@ -1,0 +1,26 @@
1+---
2+description: "Cancel active Ralph Wiggum loop"
3+allowed-tools: ["Bash"]
4+hide-from-slash-command-tool: "true"
5+---
6+
7+# Cancel Ralph
8+
9+```!
10+if [[ -f .claude/ralph-loop.local.md ]]; then
11+ ITERATION=$(grep '^iteration:' .claude/ralph-loop.local.md | sed 's/iteration: *//')
12+ echo "FOUND_LOOP=true"
13+ echo "ITERATION=$ITERATION"
14+else
15+ echo "FOUND_LOOP=false"
16+fi
17+```
18+
19+Check the output above:
20+
21+1. **If FOUND_LOOP=false**:
22+ - Say "No active Ralph loop found."
23+
24+2. **If FOUND_LOOP=true**:
25+ - Use Bash: `rm .claude/ralph-loop.local.md`
26+ - Report: "Cancelled Ralph loop (was at iteration N)" where N is the ITERATION value from above.
@@ -1,0 +1,126 @@
1+---
2+description: "Explain Ralph Wiggum technique and available commands"
3+---
4+
5+# Ralph Wiggum Plugin Help
6+
7+Please explain the following to the user:
8+
9+## What is the Ralph Wiggum Technique?
10+
11+The Ralph Wiggum technique is an iterative development methodology based on continuous AI loops, pioneered by Geoffrey Huntley.
12+
13+**Core concept:**
14+```bash
15+while :; do
16+ cat PROMPT.md | claude-code --continue
17+done
18+```
19+
20+The same prompt is fed to Claude repeatedly. The "self-referential" aspect comes from Claude seeing its own previous work in the files and git history, not from feeding output back as input.
21+
22+**Each iteration:**
23+1. Claude receives the SAME prompt
24+2. Works on the task, modifying files
25+3. Tries to exit
26+4. Stop hook intercepts and feeds the same prompt again
27+5. Claude sees its previous work in the files
28+6. Iteratively improves until completion
29+
30+The technique is described as "deterministically bad in an undeterministic world" - failures are predictable, enabling systematic improvement through prompt tuning.
31+
32+## Available Commands
33+
34+### /ralph-loop <PROMPT> [OPTIONS]
35+
36+Start a Ralph loop in your current session.
37+
38+**Usage:**
39+```
40+/ralph-loop "Refactor the cache layer" --max-iterations 20
41+/ralph-loop "Add tests" --completion-promise "TESTS COMPLETE"
42+```
43+
44+**Options:**
45+- `--max-iterations <n>` - Max iterations before auto-stop
46+- `--completion-promise <text>` - Promise phrase to signal completion
47+
48+**How it works:**
49+1. Creates `.claude/.ralph-loop.local.md` state file
50+2. You work on the task
51+3. When you try to exit, stop hook intercepts
52+4. Same prompt fed back
53+5. You see your previous work
54+6. Continues until promise detected or max iterations
55+
56+---
57+
58+### /cancel-ralph
59+
60+Cancel an active Ralph loop (removes the loop state file).
61+
62+**Usage:**
63+```
64+/cancel-ralph
65+```
66+
67+**How it works:**
68+- Checks for active loop state file
69+- Removes `.claude/.ralph-loop.local.md`
70+- Reports cancellation with iteration count
71+
72+---
73+
74+## Key Concepts
75+
76+### Completion Promises
77+
78+To signal completion, Claude must output a `<promise>` tag:
79+
80+```
81+<promise>TASK COMPLETE</promise>
82+```
83+
84+The stop hook looks for this specific tag. Without it (or `--max-iterations`), Ralph runs infinitely.
85+
86+### Self-Reference Mechanism
87+
88+The "loop" doesn't mean Claude talks to itself. It means:
89+- Same prompt repeated
90+- Claude's work persists in files
91+- Each iteration sees previous attempts
92+- Builds incrementally toward goal
93+
94+## Example
95+
96+### Interactive Bug Fix
97+
98+```
99+/ralph-loop "Fix the token refresh logic in auth.ts. Output <promise>FIXED</promise> when all tests pass." --completion-promise "FIXED" --max-iterations 10
100+```
101+
102+You'll see Ralph:
103+- Attempt fixes
104+- Run tests
105+- See failures
106+- Iterate on solution
107+- In your current session
108+
109+## When to Use Ralph
110+
111+**Good for:**
112+- Well-defined tasks with clear success criteria
113+- Tasks requiring iteration and refinement
114+- Iterative development with self-correction
115+- Greenfield projects
116+
117+**Not good for:**
118+- Tasks requiring human judgment or design decisions
119+- One-shot operations
120+- Tasks with unclear success criteria
121+- Debugging production issues (use targeted debugging instead)
122+
123+## Learn More
124+
125+- Original technique: https://ghuntley.com/ralph/
126+- Ralph Orchestrator: https://github.com/mikeyobrien/ralph-orchestrator
@@ -1,0 +1,48 @@
1+---
2+description: "Start Ralph Wiggum loop in current session"
3+argument-hint: "PROMPT [--max-iterations N] [--completion-promise TEXT]"
4+allowed-tools: ["Bash(${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh)"]
5+hide-from-slash-command-tool: "true"
6+---
7+
8+# Ralph Loop Command
9+
10+Execute the setup script to initialize the Ralph loop:
11+
12+```!
13+"${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh" $ARGUMENTS
14+
15+# Extract and display completion promise if set
16+if [ -f .claude/ralph-loop.local.md ]; then
17+ PROMISE=$(grep '^completion_promise:' .claude/ralph-loop.local.md | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
18+ if [ -n "$PROMISE" ] && [ "$PROMISE" != "null" ]; then
19+ echo ""
20+ echo "═══════════════════════════════════════════════════════════"
21+ echo "CRITICAL - Ralph Loop Completion Promise"
22+ echo "═══════════════════════════════════════════════════════════"
23+ echo ""
24+ echo "To complete this loop, output this EXACT text:"
25+ echo " <promise>$PROMISE</promise>"
26+ echo ""
27+ echo "STRICT REQUIREMENTS (DO NOT VIOLATE):"
28+ echo " ✓ Use <promise> XML tags EXACTLY as shown above"
29+ echo " ✓ The statement MUST be completely and unequivocally TRUE"
30+ echo " ✓ Do NOT output false statements to exit the loop"
31+ echo " ✓ Do NOT lie even if you think you should exit"
32+ echo ""
33+ echo "IMPORTANT - Do not circumvent the loop:"
34+ echo " Even if you believe you're stuck, the task is impossible,"
35+ echo " or you've been running too long - you MUST NOT output a"
36+ echo " false promise statement. The loop is designed to continue"
37+ echo " until the promise is GENUINELY TRUE. Trust the process."
38+ echo ""
39+ echo " If the loop should stop, the promise statement will become"
40+ echo " true naturally. Do not force it by lying."
41+ echo "═══════════════════════════════════════════════════════════"
42+ fi
43+fi
44+```
45+
46+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.
47+
48+CRITICAL RULE: If a completion promise is set, you may ONLY output it when the statement is completely and unequivocally TRUE. Do not output false promises to escape the loop, even if you think you're stuck or should exit for other reasons. The loop is designed to continue until genuine completion.
@@ -1,0 +1,15 @@
1+{
2+ "description": "Ralph Wiggum plugin stop hook for self-referential loops",
3+ "hooks": {
4+ "Stop": [
5+ {
6+ "hooks": [
7+ {
8+ "type": "command",
9+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"
10+ }
11+ ]
12+ }
13+ ]
14+ }
15+}
@@ -1,0 +1,177 @@
1+#!/bin/bash
2+
3+# Ralph Wiggum Stop Hook
4+# Prevents session exit when a ralph-loop is active
5+# Feeds Claude's output back as input to continue the loop
6+
7+set -euo pipefail
8+
9+# Read hook input from stdin (advanced stop hook API)
10+HOOK_INPUT=$(cat)
11+
12+# Check if ralph-loop is active
13+RALPH_STATE_FILE=".claude/ralph-loop.local.md"
14+
15+if [[ ! -f "$RALPH_STATE_FILE" ]]; then
16+ # No active loop - allow exit
17+ exit 0
18+fi
19+
20+# Parse markdown frontmatter (YAML between ---) and extract values
21+FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE")
22+ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
23+MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//')
24+# Extract completion_promise and strip surrounding quotes if present
25+COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
26+
27+# Validate numeric fields before arithmetic operations
28+if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
29+ echo "⚠️ Ralph loop: State file corrupted" >&2
30+ echo " File: $RALPH_STATE_FILE" >&2
31+ echo " Problem: 'iteration' field is not a valid number (got: '$ITERATION')" >&2
32+ echo "" >&2
33+ echo " This usually means the state file was manually edited or corrupted." >&2
34+ echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
35+ rm "$RALPH_STATE_FILE"
36+ exit 0
37+fi
38+
39+if [[ ! "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then
40+ echo "⚠️ Ralph loop: State file corrupted" >&2
41+ echo " File: $RALPH_STATE_FILE" >&2
42+ echo " Problem: 'max_iterations' field is not a valid number (got: '$MAX_ITERATIONS')" >&2
43+ echo "" >&2
44+ echo " This usually means the state file was manually edited or corrupted." >&2
45+ echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
46+ rm "$RALPH_STATE_FILE"
47+ exit 0
48+fi
49+
50+# Check if max iterations reached
51+if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then
52+ echo "🛑 Ralph loop: Max iterations ($MAX_ITERATIONS) reached."
53+ rm "$RALPH_STATE_FILE"
54+ exit 0
55+fi
56+
57+# Get transcript path from hook input
58+TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')
59+
60+if [[ ! -f "$TRANSCRIPT_PATH" ]]; then
61+ echo "⚠️ Ralph loop: Transcript file not found" >&2
62+ echo " Expected: $TRANSCRIPT_PATH" >&2
63+ echo " This is unusual and may indicate a Claude Code internal issue." >&2
64+ echo " Ralph loop is stopping." >&2
65+ rm "$RALPH_STATE_FILE"
66+ exit 0
67+fi
68+
69+# Read last assistant message from transcript (JSONL format - one JSON per line)
70+# First check if there are any assistant messages
71+if ! grep -q '"role":"assistant"' "$TRANSCRIPT_PATH"; then
72+ echo "⚠️ Ralph loop: No assistant messages found in transcript" >&2
73+ echo " Transcript: $TRANSCRIPT_PATH" >&2
74+ echo " This is unusual and may indicate a transcript format issue" >&2
75+ echo " Ralph loop is stopping." >&2
76+ rm "$RALPH_STATE_FILE"
77+ exit 0
78+fi
79+
80+# Extract last assistant message with explicit error handling
81+LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -1)
82+if [[ -z "$LAST_LINE" ]]; then
83+ echo "⚠️ Ralph loop: Failed to extract last assistant message" >&2
84+ echo " Ralph loop is stopping." >&2
85+ rm "$RALPH_STATE_FILE"
86+ exit 0
87+fi
88+
89+# Parse JSON with error handling
90+LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
91+ .message.content |
92+ map(select(.type == "text")) |
93+ map(.text) |
94+ join("\n")
95+' 2>&1)
96+
97+# Check if jq succeeded
98+if [[ $? -ne 0 ]]; then
99+ echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
100+ echo " Error: $LAST_OUTPUT" >&2
101+ echo " This may indicate a transcript format issue" >&2
102+ echo " Ralph loop is stopping." >&2
103+ rm "$RALPH_STATE_FILE"
104+ exit 0
105+fi
106+
107+if [[ -z "$LAST_OUTPUT" ]]; then
108+ echo "⚠️ Ralph loop: Assistant message contained no text content" >&2
109+ echo " Ralph loop is stopping." >&2
110+ rm "$RALPH_STATE_FILE"
111+ exit 0
112+fi
113+
114+# Check for completion promise (only if set)
115+if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
116+ # Extract text from <promise> tags using Perl for multiline support
117+ # -0777 slurps entire input, s flag makes . match newlines
118+ # .*? is non-greedy (takes FIRST tag), whitespace normalized
119+ PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || echo "")
120+
121+ # Use = for literal string comparison (not pattern matching)
122+ # == in [[ ]] does glob pattern matching which breaks with *, ?, [ characters
123+ if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
124+ echo "✅ Ralph loop: Detected <promise>$COMPLETION_PROMISE</promise>"
125+ rm "$RALPH_STATE_FILE"
126+ exit 0
127+ fi
128+fi
129+
130+# Not complete - continue loop with SAME PROMPT
131+NEXT_ITERATION=$((ITERATION + 1))
132+
133+# Extract prompt (everything after the closing ---)
134+# Skip first --- line, skip until second --- line, then print everything after
135+# Use i>=2 instead of i==2 to handle --- in prompt content
136+PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
137+
138+if [[ -z "$PROMPT_TEXT" ]]; then
139+ echo "⚠️ Ralph loop: State file corrupted or incomplete" >&2
140+ echo " File: $RALPH_STATE_FILE" >&2
141+ echo " Problem: No prompt text found" >&2
142+ echo "" >&2
143+ echo " This usually means:" >&2
144+ echo " • State file was manually edited" >&2
145+ echo " • File was corrupted during writing" >&2
146+ echo "" >&2
147+ echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
148+ rm "$RALPH_STATE_FILE"
149+ exit 0
150+fi
151+
152+# Update iteration in frontmatter (portable across macOS and Linux)
153+# Create temp file, then atomically replace
154+TEMP_FILE="${RALPH_STATE_FILE}.tmp.$$"
155+sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"
156+mv "$TEMP_FILE" "$RALPH_STATE_FILE"
157+
158+# Build system message with iteration count and completion promise info
159+if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
160+ SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | To stop: output <promise>$COMPLETION_PROMISE</promise> (ONLY when statement is TRUE - do not lie to exit!)"
161+else
162+ SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | No completion promise set - loop runs infinitely"
163+fi
164+
165+# Output JSON to block the stop and feed prompt back
166+# The "reason" field contains the prompt that will be sent back to Claude
167+jq -n \
168+ --arg prompt "$PROMPT_TEXT" \
169+ --arg msg "$SYSTEM_MSG" \
170+ '{
171+ "decision": "block",
172+ "reason": $prompt,
173+ "systemMessage": $msg
174+ }'
175+
176+# Exit 0 for successful hook execution
177+exit 0
@@ -1,0 +1,176 @@
1+#!/bin/bash
2+
3+# Ralph Loop Setup Script
4+# Creates state file for in-session Ralph loop
5+
6+set -euo pipefail
7+
8+# Parse arguments
9+PROMPT_PARTS=()
10+MAX_ITERATIONS=0
11+COMPLETION_PROMISE="null"
12+
13+# Parse options and positional arguments
14+while [[ $# -gt 0 ]]; do
15+ case $1 in
16+ -h|--help)
17+ cat << 'HELP_EOF'
18+Ralph Loop - Interactive self-referential development loop
19+
20+USAGE:
21+ /ralph-loop [PROMPT...] [OPTIONS]
22+
23+ARGUMENTS:
24+ PROMPT... Initial prompt to start the loop (can be multiple words without quotes)
25+
26+OPTIONS:
27+ --max-iterations <n> Maximum iterations before auto-stop (default: unlimited)
28+ --completion-promise '<text>' Promise phrase (USE QUOTES for multi-word)
29+ -h, --help Show this help message
30+
31+DESCRIPTION:
32+ Starts a Ralph Wiggum loop in your CURRENT session. The stop hook prevents
33+ exit and feeds your output back as input until completion or iteration limit.
34+
35+ To signal completion, you must output: <promise>YOUR_PHRASE</promise>
36+
37+ Use this for:
38+ - Interactive iteration where you want to see progress
39+ - Tasks requiring self-correction and refinement
40+ - Learning how Ralph works
41+
42+EXAMPLES:
43+ /ralph-loop Build a todo API --completion-promise 'DONE' --max-iterations 20
44+ /ralph-loop --max-iterations 10 Fix the auth bug
45+ /ralph-loop Refactor cache layer (runs forever)
46+ /ralph-loop --completion-promise 'TASK COMPLETE' Create a REST API
47+
48+STOPPING:
49+ Only by reaching --max-iterations or detecting --completion-promise
50+ No manual stop - Ralph runs infinitely by default!
51+
52+MONITORING:
53+ # View current iteration:
54+ grep '^iteration:' .claude/ralph-loop.local.md
55+
56+ # View full state:
57+ head -10 .claude/ralph-loop.local.md
58+HELP_EOF
59+ exit 0
60+ ;;
61+ --max-iterations)
62+ if [[ -z "${2:-}" ]]; then
63+ echo "❌ Error: --max-iterations requires a number argument" >&2
64+ echo "" >&2
65+ echo " Valid examples:" >&2
66+ echo " --max-iterations 10" >&2
67+ echo " --max-iterations 50" >&2
68+ echo " --max-iterations 0 (unlimited)" >&2
69+ echo "" >&2
70+ echo " You provided: --max-iterations (with no number)" >&2
71+ exit 1
72+ fi
73+ if ! [[ "$2" =~ ^[0-9]+$ ]]; then
74+ echo "❌ Error: --max-iterations must be a positive integer or 0, got: $2" >&2
75+ echo "" >&2
76+ echo " Valid examples:" >&2
77+ echo " --max-iterations 10" >&2
78+ echo " --max-iterations 50" >&2
79+ echo " --max-iterations 0 (unlimited)" >&2
80+ echo "" >&2
81+ echo " Invalid: decimals (10.5), negative numbers (-5), text" >&2
82+ exit 1
83+ fi
84+ MAX_ITERATIONS="$2"
85+ shift 2
86+ ;;
87+ --completion-promise)
88+ if [[ -z "${2:-}" ]]; then
89+ echo "❌ Error: --completion-promise requires a text argument" >&2
90+ echo "" >&2
91+ echo " Valid examples:" >&2
92+ echo " --completion-promise 'DONE'" >&2
93+ echo " --completion-promise 'TASK COMPLETE'" >&2
94+ echo " --completion-promise 'All tests passing'" >&2
95+ echo "" >&2
96+ echo " You provided: --completion-promise (with no text)" >&2
97+ echo "" >&2
98+ echo " Note: Multi-word promises must be quoted!" >&2
99+ exit 1
100+ fi
101+ COMPLETION_PROMISE="$2"
102+ shift 2
103+ ;;
104+ *)
105+ # Non-option argument - collect all as prompt parts
106+ PROMPT_PARTS+=("$1")
107+ shift
108+ ;;
109+ esac
110+done
111+
112+# Join all prompt parts with spaces
113+PROMPT="${PROMPT_PARTS[*]}"
114+
115+# Validate prompt is non-empty
116+if [[ -z "$PROMPT" ]]; then
117+ echo "❌ Error: No prompt provided" >&2
118+ echo "" >&2
119+ echo " Ralph needs a task description to work on." >&2
120+ echo "" >&2
121+ echo " Examples:" >&2
122+ echo " /ralph-loop Build a REST API for todos" >&2
123+ echo " /ralph-loop Fix the auth bug --max-iterations 20" >&2
124+ echo " /ralph-loop --completion-promise 'DONE' Refactor code" >&2
125+ echo "" >&2
126+ echo " For all options: /ralph-loop --help" >&2
127+ exit 1
128+fi
129+
130+# Create state file for stop hook (markdown with YAML frontmatter)
131+mkdir -p .claude
132+
133+# Quote completion promise for YAML if it contains special chars or is not null
134+if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
135+ COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""
136+else
137+ COMPLETION_PROMISE_YAML="null"
138+fi
139+
140+cat > .claude/ralph-loop.local.md <<EOF
141+---
142+active: true
143+iteration: 1
144+max_iterations: $MAX_ITERATIONS
145+completion_promise: $COMPLETION_PROMISE_YAML
146+started_at: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
147+---
148+
149+$PROMPT
150+EOF
151+
152+# Output setup message
153+cat <<EOF
154+🔄 Ralph loop activated in this session!
155+
156+Iteration: 1
157+Max iterations: $(if [[ $MAX_ITERATIONS -gt 0 ]]; then echo $MAX_ITERATIONS; else echo "unlimited"; fi)
158+Completion promise: $(if [[ "$COMPLETION_PROMISE" != "null" ]]; then echo "${COMPLETION_PROMISE//\"/} (ONLY output when TRUE - do not lie!)"; else echo "none (runs forever)"; fi)
159+
160+The stop hook is now active. When you try to exit, the SAME PROMPT will be
161+fed back to you. You'll see your previous work in files, creating a
162+self-referential loop where you iteratively improve on the same task.
163+
164+To monitor: head -10 .claude/ralph-loop.local.md
165+
166+⚠️ WARNING: This loop cannot be stopped manually! It will run infinitely
167+ unless you set --max-iterations or --completion-promise.
168+
169+🔄
170+EOF
171+
172+# Output the initial prompt if provided
173+if [[ -n "$PROMPT" ]]; then
174+ echo ""
175+ echo "$PROMPT"
176+fi