diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index a99123f6..5b88dab1 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -112,6 +112,17 @@
},
"source": "./plugins/ralph-wiggum",
"category": "development"
+ },
+ {
+ "name": "hookify",
+ "description": "Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or from explicit instructions. Define rules via simple markdown files.",
+ "version": "0.1.0",
+ "author": {
+ "name": "Daisy Hollman",
+ "email": "daisy@anthropic.com"
+ },
+ "source": "./plugins/hookify",
+ "category": "productivity"
}
]
}
diff --git a/plugins/hookify/.claude-plugin/plugin.json b/plugins/hookify/.claude-plugin/plugin.json
new file mode 100644
index 00000000..3cf8b5b8
--- /dev/null
+++ b/plugins/hookify/.claude-plugin/plugin.json
@@ -0,0 +1,9 @@
+{
+ "name": "hookify",
+ "version": "0.1.0",
+ "description": "Easily create hooks to prevent unwanted behaviors by analyzing conversation patterns",
+ "author": {
+ "name": "Daisy Hollman",
+ "email": "daisy@anthropic.com"
+ }
+}
diff --git a/plugins/hookify/.gitignore b/plugins/hookify/.gitignore
new file mode 100644
index 00000000..6d5f8af5
--- /dev/null
+++ b/plugins/hookify/.gitignore
@@ -0,0 +1,30 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+
+# Virtual environments
+venv/
+env/
+ENV/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+
+# Local configuration (should not be committed)
+.claude/*.local.md
+.claude/*.local.json
diff --git a/plugins/hookify/README.md b/plugins/hookify/README.md
new file mode 100644
index 00000000..1aca6cdf
--- /dev/null
+++ b/plugins/hookify/README.md
@@ -0,0 +1,340 @@
+# Hookify Plugin
+
+Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or from explicit instructions.
+
+## Overview
+
+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.
+
+**Key features:**
+- 🎯 Analyze conversations to find unwanted behaviors automatically
+- 📝 Simple markdown configuration files with YAML frontmatter
+- 🔍 Regex pattern matching for powerful rules
+- 🚀 No coding required - just describe the behavior
+- 🔄 Easy enable/disable without restarting
+
+## Quick Start
+
+### 1. Create Your First Rule
+
+```bash
+/hookify Warn me when I use rm -rf commands
+```
+
+This analyzes your request and creates `.claude/hookify.warn-rm.local.md`.
+
+### 2. Test It Immediately
+
+**No restart needed!** Rules take effect on the very next tool use.
+
+Ask Claude to run a command that should trigger the rule:
+```
+Run rm -rf /tmp/test
+```
+
+You should see the warning message immediately!
+
+## Usage
+
+### Main Command: /hookify
+
+**With arguments:**
+```
+/hookify Don't use console.log in TypeScript files
+```
+Creates a rule from your explicit instructions.
+
+**Without arguments:**
+```
+/hookify
+```
+Analyzes recent conversation to find behaviors you've corrected or been frustrated by.
+
+### Helper Commands
+
+**List all rules:**
+```
+/hookify:list
+```
+
+**Configure rules interactively:**
+```
+/hookify:configure
+```
+Enable/disable existing rules through an interactive interface.
+
+**Get help:**
+```
+/hookify:help
+```
+
+## Rule Configuration Format
+
+### Simple Rule (Single Pattern)
+
+`.claude/hookify.dangerous-rm.local.md`:
+```markdown
+---
+name: block-dangerous-rm
+enabled: true
+event: bash
+pattern: rm\s+-rf
+action: block
+---
+
+⚠️ **Dangerous rm command detected!**
+
+This command could delete important files. Please:
+- Verify the path is correct
+- Consider using a safer approach
+- Make sure you have backups
+```
+
+**Action field:**
+- `warn`: Shows warning but allows operation (default)
+- `block`: Prevents operation from executing (PreToolUse) or stops session (Stop events)
+
+### Advanced Rule (Multiple Conditions)
+
+`.claude/hookify.sensitive-files.local.md`:
+```markdown
+---
+name: warn-sensitive-files
+enabled: true
+event: file
+action: warn
+conditions:
+ - field: file_path
+ operator: regex_match
+ pattern: \.env$|credentials|secrets
+ - field: new_text
+ operator: contains
+ pattern: KEY
+---
+
+🔐 **Sensitive file edit detected!**
+
+Ensure credentials are not hardcoded and file is in .gitignore.
+```
+
+**All conditions must match** for the rule to trigger.
+
+## Event Types
+
+- **`bash`**: Triggers on Bash tool commands
+- **`file`**: Triggers on Edit, Write, MultiEdit tools
+- **`stop`**: Triggers when Claude wants to stop (for completion checks)
+- **`prompt`**: Triggers on user prompt submission
+- **`all`**: Triggers on all events
+
+## Pattern Syntax
+
+Use Python regex syntax:
+
+| Pattern | Matches | Example |
+|---------|---------|---------|
+| `rm\s+-rf` | rm -rf | rm -rf /tmp |
+| `console\.log\(` | console.log( | console.log("test") |
+| `(eval\|exec)\(` | eval( or exec( | eval("code") |
+| `\.env$` | files ending in .env | .env, .env.local |
+| `chmod\s+777` | chmod 777 | chmod 777 file.txt |
+
+**Tips:**
+- Use `\s` for whitespace
+- Escape special chars: `\.` for literal dot
+- Use `|` for OR: `(foo|bar)`
+- Use `.*` to match anything
+- Set `action: block` for dangerous operations
+- Set `action: warn` (or omit) for informational warnings
+
+## Examples
+
+### Example 1: Block Dangerous Commands
+
+```markdown
+---
+name: block-destructive-ops
+enabled: true
+event: bash
+pattern: rm\s+-rf|dd\s+if=|mkfs|format
+action: block
+---
+
+🛑 **Destructive operation detected!**
+
+This command can cause data loss. Operation blocked for safety.
+Please verify the exact path and use a safer approach.
+```
+
+**This rule blocks the operation** - Claude will not be allowed to execute these commands.
+
+### Example 2: Warn About Debug Code
+
+```markdown
+---
+name: warn-debug-code
+enabled: true
+event: file
+pattern: console\.log\(|debugger;|print\(
+action: warn
+---
+
+🐛 **Debug code detected**
+
+Remember to remove debugging statements before committing.
+```
+
+**This rule warns but allows** - Claude sees the message but can still proceed.
+
+### Example 3: Require Tests Before Stopping
+
+```markdown
+---
+name: require-tests-run
+enabled: false
+event: stop
+action: block
+conditions:
+ - field: transcript
+ operator: not_contains
+ pattern: npm test|pytest|cargo test
+---
+
+**Tests not detected in transcript!**
+
+Before stopping, please run tests to verify your changes work correctly.
+```
+
+**This blocks Claude from stopping** if no test commands appear in the session transcript. Enable only when you want strict enforcement.
+
+## Advanced Usage
+
+### Multiple Conditions
+
+Check multiple fields simultaneously:
+
+```markdown
+---
+name: api-key-in-typescript
+enabled: true
+event: file
+conditions:
+ - field: file_path
+ operator: regex_match
+ pattern: \.tsx?$
+ - field: new_text
+ operator: regex_match
+ pattern: (API_KEY|SECRET|TOKEN)\s*=\s*["']
+---
+
+🔐 **Hardcoded credential in TypeScript!**
+
+Use environment variables instead of hardcoded values.
+```
+
+### Operators Reference
+
+- `regex_match`: Pattern must match (most common)
+- `contains`: String must contain pattern
+- `equals`: Exact string match
+- `not_contains`: String must NOT contain pattern
+- `starts_with`: String starts with pattern
+- `ends_with`: String ends with pattern
+
+### Field Reference
+
+**For bash events:**
+- `command`: The bash command string
+
+**For file events:**
+- `file_path`: Path to file being edited
+- `new_text`: New content being added (Edit, Write)
+- `old_text`: Old content being replaced (Edit only)
+- `content`: File content (Write only)
+
+**For prompt events:**
+- `user_prompt`: The user's submitted prompt text
+
+**For stop events:**
+- Use general matching on session state
+
+## Management
+
+### Enable/Disable Rules
+
+**Temporarily disable:**
+Edit the `.local.md` file and set `enabled: false`
+
+**Re-enable:**
+Set `enabled: true`
+
+**Or use interactive tool:**
+```
+/hookify:configure
+```
+
+### Delete Rules
+
+Simply delete the `.local.md` file:
+```bash
+rm .claude/hookify.my-rule.local.md
+```
+
+### View All Rules
+
+```
+/hookify:list
+```
+
+## Installation
+
+This plugin is part of the Claude Code Marketplace. It should be auto-discovered when the marketplace is installed.
+
+**Manual testing:**
+```bash
+cc --plugin-dir /path/to/hookify
+```
+
+## Requirements
+
+- Python 3.7+
+- No external dependencies (uses stdlib only)
+
+## Troubleshooting
+
+**Rule not triggering:**
+1. Check rule file exists in `.claude/` directory (in project root, not plugin directory)
+2. Verify `enabled: true` in frontmatter
+3. Test regex pattern separately
+4. Rules should work immediately - no restart needed
+5. Try `/hookify:list` to see if rule is loaded
+
+**Import errors:**
+- Ensure Python 3 is available: `python3 --version`
+- Check hookify plugin is installed
+
+**Pattern not matching:**
+- Test regex: `python3 -c "import re; print(re.search(r'pattern', 'text'))"`
+- Use unquoted patterns in YAML to avoid escaping issues
+- Start simple, then add complexity
+
+**Hook seems slow:**
+- Keep patterns simple (avoid complex regex)
+- Use specific event types (bash, file) instead of "all"
+- Limit number of active rules
+
+## Contributing
+
+Found a useful rule pattern? Consider sharing example files via PR!
+
+## Future Enhancements
+
+- Severity levels (error/warning/info distinctions)
+- Rule templates library
+- Interactive pattern builder
+- Hook testing utilities
+- JSON format support (in addition to markdown)
+
+## License
+
+MIT License
diff --git a/plugins/hookify/agents/conversation-analyzer.md b/plugins/hookify/agents/conversation-analyzer.md
new file mode 100644
index 00000000..cb91a41a
--- /dev/null
+++ b/plugins/hookify/agents/conversation-analyzer.md
@@ -0,0 +1,176 @@
+---
+name: conversation-analyzer
+description: Use this agent when analyzing conversation transcripts to find behaviors worth preventing with hooks. Examples: Context: User is running /hookify command without arguments\nuser: "/hookify"\nassistant: "I'll analyze the conversation to find behaviors you want to prevent"\nThe /hookify command without arguments triggers conversation analysis to find unwanted behaviors.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."\nUser explicitly asks to analyze conversation for mistakes that should be prevented.
+model: inherit
+color: yellow
+tools: ["Read", "Grep"]
+---
+
+You are a conversation analysis specialist that identifies problematic behaviors in Claude Code sessions that could be prevented with hooks.
+
+**Your Core Responsibilities:**
+1. Read and analyze user messages to find frustration signals
+2. Identify specific tool usage patterns that caused issues
+3. Extract actionable patterns that can be matched with regex
+4. Categorize issues by severity and type
+5. Provide structured findings for hook rule generation
+
+**Analysis Process:**
+
+### 1. Search for User Messages Indicating Issues
+
+Read through user messages in reverse chronological order (most recent first). Look for:
+
+**Explicit correction requests:**
+- "Don't use X"
+- "Stop doing Y"
+- "Please don't Z"
+- "Avoid..."
+- "Never..."
+
+**Frustrated reactions:**
+- "Why did you do X?"
+- "I didn't ask for that"
+- "That's not what I meant"
+- "That was wrong"
+
+**Corrections and reversions:**
+- User reverting changes Claude made
+- User fixing issues Claude created
+- User providing step-by-step corrections
+
+**Repeated issues:**
+- Same type of mistake multiple times
+- User having to remind multiple times
+- Pattern of similar problems
+
+### 2. Identify Tool Usage Patterns
+
+For each issue, determine:
+- **Which tool**: Bash, Edit, Write, MultiEdit
+- **What action**: Specific command or code pattern
+- **When it happened**: During what task/phase
+- **Why problematic**: User's stated reason or implicit concern
+
+**Extract concrete examples:**
+- For Bash: Actual command that was problematic
+- For Edit/Write: Code pattern that was added
+- For Stop: What was missing before stopping
+
+### 3. Create Regex Patterns
+
+Convert behaviors into matchable patterns:
+
+**Bash command patterns:**
+- `rm\s+-rf` for dangerous deletes
+- `sudo\s+` for privilege escalation
+- `chmod\s+777` for permission issues
+
+**Code patterns (Edit/Write):**
+- `console\.log\(` for debug logging
+- `eval\(|new Function\(` for dangerous eval
+- `innerHTML\s*=` for XSS risks
+
+**File path patterns:**
+- `\.env$` for environment files
+- `/node_modules/` for dependency files
+- `dist/|build/` for generated files
+
+### 4. Categorize Severity
+
+**High severity (should block in future):**
+- Dangerous commands (rm -rf, chmod 777)
+- Security issues (hardcoded secrets, eval)
+- Data loss risks
+
+**Medium severity (warn):**
+- Style violations (console.log in production)
+- Wrong file types (editing generated files)
+- Missing best practices
+
+**Low severity (optional):**
+- Preferences (coding style)
+- Non-critical patterns
+
+### 5. Output Format
+
+Return your findings as structured text in this format:
+
+```
+## Hookify Analysis Results
+
+### Issue 1: Dangerous rm Commands
+**Severity**: High
+**Tool**: Bash
+**Pattern**: `rm\s+-rf`
+**Occurrences**: 3 times
+**Context**: Used rm -rf on /tmp directories without verification
+**User Reaction**: "Please be more careful with rm commands"
+
+**Suggested Rule:**
+- Name: warn-dangerous-rm
+- Event: bash
+- Pattern: rm\s+-rf
+- Message: "Dangerous rm command detected. Verify path before proceeding."
+
+---
+
+### Issue 2: Console.log in TypeScript
+**Severity**: Medium
+**Tool**: Edit/Write
+**Pattern**: `console\.log\(`
+**Occurrences**: 2 times
+**Context**: Added console.log statements to production TypeScript files
+**User Reaction**: "Don't use console.log in production code"
+
+**Suggested Rule:**
+- Name: warn-console-log
+- Event: file
+- Pattern: console\.log\(
+- Message: "Console.log detected. Use proper logging library instead."
+
+---
+
+[Continue for each issue found...]
+
+## Summary
+
+Found {N} behaviors worth preventing:
+- {N} high severity
+- {N} medium severity
+- {N} low severity
+
+Recommend creating rules for high and medium severity issues.
+```
+
+**Quality Standards:**
+- Be specific about patterns (don't be overly broad)
+- Include actual examples from conversation
+- Explain why each issue matters
+- Provide ready-to-use regex patterns
+- Don't false-positive on discussions about what NOT to do
+
+**Edge Cases:**
+
+**User discussing hypotheticals:**
+- "What would happen if I used rm -rf?"
+- Don't treat as problematic behavior
+
+**Teaching moments:**
+- "Here's what you shouldn't do: ..."
+- Context indicates explanation, not actual problem
+
+**One-time accidents:**
+- Single occurrence, already fixed
+- Mention but mark as low priority
+
+**Subjective preferences:**
+- "I prefer X over Y"
+- Mark as low severity, let user decide
+
+**Return Results:**
+Provide your analysis in the structured format above. The /hookify command will use this to:
+1. Present findings to user
+2. Ask which rules to create
+3. Generate .local.md configuration files
+4. Save rules to .claude directory
diff --git a/plugins/hookify/commands/configure.md b/plugins/hookify/commands/configure.md
new file mode 100644
index 00000000..ccc7e472
--- /dev/null
+++ b/plugins/hookify/commands/configure.md
@@ -0,0 +1,128 @@
+---
+description: Enable or disable hookify rules interactively
+allowed-tools: ["Glob", "Read", "Edit", "AskUserQuestion", "Skill"]
+---
+
+# Configure Hookify Rules
+
+**Load hookify:writing-rules skill first** to understand rule format.
+
+Enable or disable existing hookify rules using an interactive interface.
+
+## Steps
+
+### 1. Find Existing Rules
+
+Use Glob tool to find all hookify rule files:
+```
+pattern: ".claude/hookify.*.local.md"
+```
+
+If no rules found, inform user:
+```
+No hookify rules configured yet. Use `/hookify` to create your first rule.
+```
+
+### 2. Read Current State
+
+For each rule file:
+- Read the file
+- Extract `name` and `enabled` fields from frontmatter
+- Build list of rules with current state
+
+### 3. Ask User Which Rules to Toggle
+
+Use AskUserQuestion to let user select rules:
+
+```json
+{
+ "questions": [
+ {
+ "question": "Which rules would you like to enable or disable?",
+ "header": "Configure",
+ "multiSelect": true,
+ "options": [
+ {
+ "label": "warn-dangerous-rm (currently enabled)",
+ "description": "Warns about rm -rf commands"
+ },
+ {
+ "label": "warn-console-log (currently disabled)",
+ "description": "Warns about console.log in code"
+ },
+ {
+ "label": "require-tests (currently enabled)",
+ "description": "Requires tests before stopping"
+ }
+ ]
+ }
+ ]
+}
+```
+
+**Option format:**
+- Label: `{rule-name} (currently {enabled|disabled})`
+- Description: Brief description from rule's message or pattern
+
+### 4. Parse User Selection
+
+For each selected rule:
+- Determine current state from label (enabled/disabled)
+- Toggle state: enabled → disabled, disabled → enabled
+
+### 5. Update Rule Files
+
+For each rule to toggle:
+- Use Read tool to read current content
+- Use Edit tool to change `enabled: true` to `enabled: false` (or vice versa)
+- Handle both with and without quotes
+
+**Edit pattern for enabling:**
+```
+old_string: "enabled: false"
+new_string: "enabled: true"
+```
+
+**Edit pattern for disabling:**
+```
+old_string: "enabled: true"
+new_string: "enabled: false"
+```
+
+### 6. Confirm Changes
+
+Show user what was changed:
+
+```
+## Hookify Rules Updated
+
+**Enabled:**
+- warn-console-log
+
+**Disabled:**
+- warn-dangerous-rm
+
+**Unchanged:**
+- require-tests
+
+Changes apply immediately - no restart needed
+```
+
+## Important Notes
+
+- Changes take effect immediately on next tool use
+- You can also manually edit .claude/hookify.*.local.md files
+- To permanently remove a rule, delete its .local.md file
+- Use `/hookify:list` to see all configured rules
+
+## Edge Cases
+
+**No rules to configure:**
+- Show message about using `/hookify` to create rules first
+
+**User selects no rules:**
+- Inform that no changes were made
+
+**File read/write errors:**
+- Inform user of specific error
+- Suggest manual editing as fallback
diff --git a/plugins/hookify/commands/help.md b/plugins/hookify/commands/help.md
new file mode 100644
index 00000000..ae6e94b1
--- /dev/null
+++ b/plugins/hookify/commands/help.md
@@ -0,0 +1,175 @@
+---
+description: Get help with the hookify plugin
+allowed-tools: ["Read"]
+---
+
+# Hookify Plugin Help
+
+Explain how the hookify plugin works and how to use it.
+
+## Overview
+
+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.
+
+## How It Works
+
+### 1. Hook System
+
+Hookify installs generic hooks that run on these events:
+- **PreToolUse**: Before any tool executes (Bash, Edit, Write, etc.)
+- **PostToolUse**: After a tool executes
+- **Stop**: When Claude wants to stop working
+- **UserPromptSubmit**: When user submits a prompt
+
+These hooks read configuration files from `.claude/hookify.*.local.md` and check if any rules match the current operation.
+
+### 2. Configuration Files
+
+Users create rules in `.claude/hookify.{rule-name}.local.md` files:
+
+```markdown
+---
+name: warn-dangerous-rm
+enabled: true
+event: bash
+pattern: rm\s+-rf
+---
+
+⚠️ **Dangerous rm command detected!**
+
+This command could delete important files. Please verify the path.
+```
+
+**Key fields:**
+- `name`: Unique identifier for the rule
+- `enabled`: true/false to activate/deactivate
+- `event`: bash, file, stop, prompt, or all
+- `pattern`: Regex pattern to match
+
+The message body is what Claude sees when the rule triggers.
+
+### 3. Creating Rules
+
+**Option A: Use /hookify command**
+```
+/hookify Don't use console.log in production files
+```
+
+This analyzes your request and creates the appropriate rule file.
+
+**Option B: Create manually**
+Create `.claude/hookify.my-rule.local.md` with the format above.
+
+**Option C: Analyze conversation**
+```
+/hookify
+```
+
+Without arguments, hookify analyzes recent conversation to find behaviors you want to prevent.
+
+## Available Commands
+
+- **`/hookify`** - Create hooks from conversation analysis or explicit instructions
+- **`/hookify:help`** - Show this help (what you're reading now)
+- **`/hookify:list`** - List all configured hooks
+- **`/hookify:configure`** - Enable/disable existing hooks interactively
+
+## Example Use Cases
+
+**Prevent dangerous commands:**
+```markdown
+---
+name: block-chmod-777
+enabled: true
+event: bash
+pattern: chmod\s+777
+---
+
+Don't use chmod 777 - it's a security risk. Use specific permissions instead.
+```
+
+**Warn about debugging code:**
+```markdown
+---
+name: warn-console-log
+enabled: true
+event: file
+pattern: console\.log\(
+---
+
+Console.log detected. Remember to remove debug logging before committing.
+```
+
+**Require tests before stopping:**
+```markdown
+---
+name: require-tests
+enabled: true
+event: stop
+pattern: .*
+---
+
+Did you run tests before finishing? Make sure `npm test` or equivalent was executed.
+```
+
+## Pattern Syntax
+
+Use Python regex syntax:
+- `\s` - whitespace
+- `\.` - literal dot
+- `|` - OR
+- `+` - one or more
+- `*` - zero or more
+- `\d` - digit
+- `[abc]` - character class
+
+**Examples:**
+- `rm\s+-rf` - matches "rm -rf"
+- `console\.log\(` - matches "console.log("
+- `(eval|exec)\(` - matches "eval(" or "exec("
+- `\.env$` - matches files ending in .env
+
+## Important Notes
+
+**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.
+
+**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.
+
+**Rule Files**: Keep rules in `.claude/hookify.*.local.md` - they should be git-ignored (add to .gitignore if needed).
+
+**Disable Rules**: Set `enabled: false` in frontmatter or delete the file.
+
+## Troubleshooting
+
+**Hook not triggering:**
+- Check rule file is in `.claude/` directory
+- Verify `enabled: true` in frontmatter
+- Confirm pattern is valid regex
+- Test pattern: `python3 -c "import re; print(re.search('your_pattern', 'test_text'))"`
+- Rules take effect immediately - no restart needed
+
+**Import errors:**
+- Check Python 3 is available: `python3 --version`
+- Verify hookify plugin is installed correctly
+
+**Pattern not matching:**
+- Test regex separately
+- Check for escaping issues (use unquoted patterns in YAML)
+- Try simpler pattern first, then refine
+
+## Getting Started
+
+1. Create your first rule:
+ ```
+ /hookify Warn me when I try to use rm -rf
+ ```
+
+2. Try to trigger it:
+ - Ask Claude to run `rm -rf /tmp/test`
+ - You should see the warning
+
+4. Refine the rule by editing `.claude/hookify.warn-rm.local.md`
+
+5. Create more rules as you encounter unwanted behaviors
+
+For more examples, check the `${CLAUDE_PLUGIN_ROOT}/examples/` directory.
diff --git a/plugins/hookify/commands/hookify.md b/plugins/hookify/commands/hookify.md
new file mode 100644
index 00000000..e5fc645a
--- /dev/null
+++ b/plugins/hookify/commands/hookify.md
@@ -0,0 +1,231 @@
+---
+description: Create hooks to prevent unwanted behaviors from conversation analysis or explicit instructions
+argument-hint: Optional specific behavior to address
+allowed-tools: ["Read", "Write", "AskUserQuestion", "Task", "Grep", "TodoWrite", "Skill"]
+---
+
+# Hookify - Create Hooks from Unwanted Behaviors
+
+**FIRST: Load the hookify:writing-rules skill** using the Skill tool to understand rule file format and syntax.
+
+Create hook rules to prevent problematic behaviors by analyzing the conversation or from explicit user instructions.
+
+## Your Task
+
+You will help the user create hookify rules to prevent unwanted behaviors. Follow these steps:
+
+### Step 1: Gather Behavior Information
+
+**If $ARGUMENTS is provided:**
+- User has given specific instructions: `$ARGUMENTS`
+- Still analyze recent conversation (last 10-15 user messages) for additional context
+- Look for examples of the behavior happening
+
+**If $ARGUMENTS is empty:**
+- Launch the conversation-analyzer agent to find problematic behaviors
+- Agent will scan user prompts for frustration signals
+- Agent will return structured findings
+
+**To analyze conversation:**
+Use the Task tool to launch conversation-analyzer agent:
+```
+{
+ "subagent_type": "general-purpose",
+ "description": "Analyze conversation for unwanted behaviors",
+ "prompt": "You are analyzing a Claude Code conversation to find behaviors the user wants to prevent.
+
+Read user messages in the current conversation and identify:
+1. Explicit requests to avoid something (\"don't do X\", \"stop doing Y\")
+2. Corrections or reversions (user fixing Claude's actions)
+3. Frustrated reactions (\"why did you do X?\", \"I didn't ask for that\")
+4. Repeated issues (same problem multiple times)
+
+For each issue found, extract:
+- What tool was used (Bash, Edit, Write, etc.)
+- Specific pattern or command
+- Why it was problematic
+- User's stated reason
+
+Return findings as a structured list with:
+- category: Type of issue
+- tool: Which tool was involved
+- pattern: Regex or literal pattern to match
+- context: What happened
+- severity: high/medium/low
+
+Focus on the most recent issues (last 20-30 messages). Don't go back further unless explicitly asked."
+}
+```
+
+### Step 2: Present Findings to User
+
+After gathering behaviors (from arguments or agent), present to user using AskUserQuestion:
+
+**Question 1: Which behaviors to hookify?**
+- Header: "Create Rules"
+- multiSelect: true
+- Options: List each detected behavior (max 4)
+ - Label: Short description (e.g., "Block rm -rf")
+ - Description: Why it's problematic
+
+**Question 2: For each selected behavior, ask about action:**
+- "Should this block the operation or just warn?"
+- Options:
+ - "Just warn" (action: warn - shows message but allows)
+ - "Block operation" (action: block - prevents execution)
+
+**Question 3: Ask for example patterns:**
+- "What patterns should trigger this rule?"
+- Show detected patterns
+- Allow user to refine or add more
+
+### Step 3: Generate Rule Files
+
+For each confirmed behavior, create a `.claude/hookify.{rule-name}.local.md` file:
+
+**Rule naming convention:**
+- Use kebab-case
+- Be descriptive: `block-dangerous-rm`, `warn-console-log`, `require-tests-before-stop`
+- Start with action verb: block, warn, prevent, require
+
+**File format:**
+```markdown
+---
+name: {rule-name}
+enabled: true
+event: {bash|file|stop|prompt|all}
+pattern: {regex pattern}
+action: {warn|block}
+---
+
+{Message to show Claude when rule triggers}
+```
+
+**Action values:**
+- `warn`: Show message but allow operation (default)
+- `block`: Prevent operation or stop session
+
+**For more complex rules (multiple conditions):**
+```markdown
+---
+name: {rule-name}
+enabled: true
+event: file
+conditions:
+ - field: file_path
+ operator: regex_match
+ pattern: \.env$
+ - field: new_text
+ operator: contains
+ pattern: API_KEY
+---
+
+{Warning message}
+```
+
+### Step 4: Create Files and Confirm
+
+**IMPORTANT**: Rule files must be created in the current working directory's `.claude/` folder, NOT the plugin directory.
+
+Use the current working directory (where Claude Code was started) as the base path.
+
+1. Check if `.claude/` directory exists in current working directory
+ - If not, create it first with: `mkdir -p .claude`
+
+2. Use Write tool to create each `.claude/hookify.{name}.local.md` file
+ - Use relative path from current working directory: `.claude/hookify.{name}.local.md`
+ - The path should resolve to the project's .claude directory, not the plugin's
+
+3. Show user what was created:
+ ```
+ Created 3 hookify rules:
+ - .claude/hookify.dangerous-rm.local.md
+ - .claude/hookify.console-log.local.md
+ - .claude/hookify.sensitive-files.local.md
+
+ These rules will trigger on:
+ - dangerous-rm: Bash commands matching "rm -rf"
+ - console-log: Edits adding console.log statements
+ - sensitive-files: Edits to .env or credentials files
+ ```
+
+4. Verify files were created in the correct location by listing them
+
+5. Inform user: **"Rules are active immediately - no restart needed!"**
+
+ The hookify hooks are already loaded and will read your new rules on the next tool use.
+
+## Event Types Reference
+
+- **bash**: Matches Bash tool commands
+- **file**: Matches Edit, Write, MultiEdit tools
+- **stop**: Matches when agent wants to stop (use for completion checks)
+- **prompt**: Matches when user submits prompts
+- **all**: Matches all events
+
+## Pattern Writing Tips
+
+**Bash patterns:**
+- Match dangerous commands: `rm\s+-rf|chmod\s+777|dd\s+if=`
+- Match specific tools: `npm\s+install\s+|pip\s+install`
+
+**File patterns:**
+- Match code patterns: `console\.log\(|eval\(|innerHTML\s*=`
+- Match file paths: `\.env$|\.git/|node_modules/`
+
+**Stop patterns:**
+- Check for missing steps: (check transcript or completion criteria)
+
+## Example Workflow
+
+**User says**: "/hookify Don't use rm -rf without asking me first"
+
+**Your response**:
+1. Analyze: User wants to prevent rm -rf commands
+2. Ask: "Should I block this command or just warn you?"
+3. User selects: "Just warn"
+4. Create `.claude/hookify.dangerous-rm.local.md`:
+ ```markdown
+ ---
+ name: warn-dangerous-rm
+ enabled: true
+ event: bash
+ pattern: rm\s+-rf
+ ---
+
+ ⚠️ **Dangerous rm command detected**
+
+ You requested to be warned before using rm -rf.
+ Please verify the path is correct.
+ ```
+5. Confirm: "Created hookify rule. It's active immediately - try triggering it!"
+
+## Important Notes
+
+- **No restart needed**: Rules take effect immediately on the next tool use
+- **File location**: Create files in project's `.claude/` directory (current working directory), NOT the plugin's .claude/
+- **Regex syntax**: Use Python regex syntax (raw strings, no need to escape in YAML)
+- **Action types**: Rules can `warn` (default) or `block` operations
+- **Testing**: Test rules immediately after creating them
+
+## Troubleshooting
+
+**If rule file creation fails:**
+1. Check current working directory with pwd
+2. Ensure `.claude/` directory exists (create with mkdir if needed)
+3. Use absolute path if needed: `{cwd}/.claude/hookify.{name}.local.md`
+4. Verify file was created with Glob or ls
+
+**If rule doesn't trigger after creation:**
+1. Verify file is in project `.claude/` not plugin `.claude/`
+2. Check file with Read tool to ensure pattern is correct
+3. Test pattern with: `python3 -c "import re; print(re.search(r'pattern', 'test text'))"`
+4. Verify `enabled: true` in frontmatter
+5. Remember: Rules work immediately, no restart needed
+
+**If blocking seems too strict:**
+1. Change `action: block` to `action: warn` in the rule file
+2. Or adjust the pattern to be more specific
+3. Changes take effect on next tool use
+
+Use TodoWrite to track your progress through the steps.
diff --git a/plugins/hookify/commands/list.md b/plugins/hookify/commands/list.md
new file mode 100644
index 00000000..d6f810fb
--- /dev/null
+++ b/plugins/hookify/commands/list.md
@@ -0,0 +1,82 @@
+---
+description: List all configured hookify rules
+allowed-tools: ["Glob", "Read", "Skill"]
+---
+
+# List Hookify Rules
+
+**Load hookify:writing-rules skill first** to understand rule format.
+
+Show all configured hookify rules in the project.
+
+## Steps
+
+1. Use Glob tool to find all hookify rule files:
+ ```
+ pattern: ".claude/hookify.*.local.md"
+ ```
+
+2. For each file found:
+ - Use Read tool to read the file
+ - Extract frontmatter fields: name, enabled, event, pattern
+ - Extract message preview (first 100 chars)
+
+3. Present results in a table:
+
+```
+## Configured Hookify Rules
+
+| Name | Enabled | Event | Pattern | File |
+|------|---------|-------|---------|------|
+| warn-dangerous-rm | ✅ Yes | bash | rm\s+-rf | hookify.dangerous-rm.local.md |
+| warn-console-log | ✅ Yes | file | console\.log\( | hookify.console-log.local.md |
+| check-tests | ❌ No | stop | .* | hookify.require-tests.local.md |
+
+**Total**: 3 rules (2 enabled, 1 disabled)
+```
+
+4. For each rule, show a brief preview:
+```
+### warn-dangerous-rm
+**Event**: bash
+**Pattern**: `rm\s+-rf`
+**Message**: "⚠️ **Dangerous rm command detected!** This command could delete..."
+
+**Status**: ✅ Active
+**File**: .claude/hookify.dangerous-rm.local.md
+```
+
+5. Add helpful footer:
+```
+---
+
+To modify a rule: Edit the .local.md file directly
+To disable a rule: Set `enabled: false` in frontmatter
+To enable a rule: Set `enabled: true` in frontmatter
+To delete a rule: Remove the .local.md file
+To create a rule: Use `/hookify` command
+
+**Remember**: Changes take effect immediately - no restart needed
+```
+
+## If No Rules Found
+
+If no hookify rules exist:
+
+```
+## No Hookify Rules Configured
+
+You haven't created any hookify rules yet.
+
+To get started:
+1. Use `/hookify` to analyze conversation and create rules
+2. Or manually create `.claude/hookify.my-rule.local.md` files
+3. See `/hookify:help` for documentation
+
+Example:
+```
+/hookify Warn me when I use console.log
+```
+
+Check `${CLAUDE_PLUGIN_ROOT}/examples/` for example rule files.
+```
diff --git a/plugins/hookify/core/__init__.py b/plugins/hookify/core/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/hookify/core/config_loader.py b/plugins/hookify/core/config_loader.py
new file mode 100644
index 00000000..fa2fc3e3
--- /dev/null
+++ b/plugins/hookify/core/config_loader.py
@@ -0,0 +1,297 @@
+#!/usr/bin/env python3
+"""Configuration loader for hookify plugin.
+
+Loads and parses .claude/hookify.*.local.md files.
+"""
+
+import os
+import sys
+import glob
+import re
+from typing import List, Optional, Dict, Any
+from dataclasses import dataclass, field
+
+
+@dataclass
+class Condition:
+ """A single condition for matching."""
+ field: str # "command", "new_text", "old_text", "file_path", etc.
+ operator: str # "regex_match", "contains", "equals", etc.
+ pattern: str # Pattern to match
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> 'Condition':
+ """Create Condition from dict."""
+ return cls(
+ field=data.get('field', ''),
+ operator=data.get('operator', 'regex_match'),
+ pattern=data.get('pattern', '')
+ )
+
+
+@dataclass
+class Rule:
+ """A hookify rule."""
+ name: str
+ enabled: bool
+ event: str # "bash", "file", "stop", "all", etc.
+ pattern: Optional[str] = None # Simple pattern (legacy)
+ conditions: List[Condition] = field(default_factory=list)
+ action: str = "warn" # "warn" or "block" (future)
+ tool_matcher: Optional[str] = None # Override tool matching
+ message: str = "" # Message body from markdown
+
+ @classmethod
+ def from_dict(cls, frontmatter: Dict[str, Any], message: str) -> 'Rule':
+ """Create Rule from frontmatter dict and message body."""
+ # Handle both simple pattern and complex conditions
+ conditions = []
+
+ # New style: explicit conditions list
+ if 'conditions' in frontmatter:
+ cond_list = frontmatter['conditions']
+ if isinstance(cond_list, list):
+ conditions = [Condition.from_dict(c) for c in cond_list]
+
+ # Legacy style: simple pattern field
+ simple_pattern = frontmatter.get('pattern')
+ if simple_pattern and not conditions:
+ # Convert simple pattern to condition
+ # Infer field from event
+ event = frontmatter.get('event', 'all')
+ if event == 'bash':
+ field = 'command'
+ elif event == 'file':
+ field = 'new_text'
+ else:
+ field = 'content'
+
+ conditions = [Condition(
+ field=field,
+ operator='regex_match',
+ pattern=simple_pattern
+ )]
+
+ return cls(
+ name=frontmatter.get('name', 'unnamed'),
+ enabled=frontmatter.get('enabled', True),
+ event=frontmatter.get('event', 'all'),
+ pattern=simple_pattern,
+ conditions=conditions,
+ action=frontmatter.get('action', 'warn'),
+ tool_matcher=frontmatter.get('tool_matcher'),
+ message=message.strip()
+ )
+
+
+def extract_frontmatter(content: str) -> tuple[Dict[str, Any], str]:
+ """Extract YAML frontmatter and message body from markdown.
+
+ Returns (frontmatter_dict, message_body).
+
+ Supports multi-line dictionary items in lists by preserving indentation.
+ """
+ if not content.startswith('---'):
+ return {}, content
+
+ # Split on --- markers
+ parts = content.split('---', 2)
+ if len(parts) < 3:
+ return {}, content
+
+ frontmatter_text = parts[1]
+ message = parts[2].strip()
+
+ # Simple YAML parser that handles indented list items
+ frontmatter = {}
+ lines = frontmatter_text.split('\n')
+
+ current_key = None
+ current_list = []
+ current_dict = {}
+ in_list = False
+ in_dict_item = False
+
+ for line in lines:
+ # Skip empty lines and comments
+ stripped = line.strip()
+ if not stripped or stripped.startswith('#'):
+ continue
+
+ # Check indentation level
+ indent = len(line) - len(line.lstrip())
+
+ # Top-level key (no indentation or minimal)
+ if indent == 0 and ':' in line and not line.strip().startswith('-'):
+ # Save previous list/dict if any
+ if in_list and current_key:
+ if in_dict_item and current_dict:
+ current_list.append(current_dict)
+ current_dict = {}
+ frontmatter[current_key] = current_list
+ in_list = False
+ in_dict_item = False
+ current_list = []
+
+ key, value = line.split(':', 1)
+ key = key.strip()
+ value = value.strip()
+
+ if not value:
+ # Empty value - list or nested structure follows
+ current_key = key
+ in_list = True
+ current_list = []
+ else:
+ # Simple key-value pair
+ value = value.strip('"').strip("'")
+ if value.lower() == 'true':
+ value = True
+ elif value.lower() == 'false':
+ value = False
+ frontmatter[key] = value
+
+ # List item (starts with -)
+ elif stripped.startswith('-') and in_list:
+ # Save previous dict item if any
+ if in_dict_item and current_dict:
+ current_list.append(current_dict)
+ current_dict = {}
+
+ item_text = stripped[1:].strip()
+
+ # Check if this is an inline dict (key: value on same line)
+ if ':' in item_text and ',' in item_text:
+ # Inline comma-separated dict: "- field: command, operator: regex_match"
+ item_dict = {}
+ for part in item_text.split(','):
+ if ':' in part:
+ k, v = part.split(':', 1)
+ item_dict[k.strip()] = v.strip().strip('"').strip("'")
+ current_list.append(item_dict)
+ in_dict_item = False
+ elif ':' in item_text:
+ # Start of multi-line dict item: "- field: command"
+ in_dict_item = True
+ k, v = item_text.split(':', 1)
+ current_dict = {k.strip(): v.strip().strip('"').strip("'")}
+ else:
+ # Simple list item
+ current_list.append(item_text.strip('"').strip("'"))
+ in_dict_item = False
+
+ # Continuation of dict item (indented under list item)
+ elif indent > 2 and in_dict_item and ':' in line:
+ # This is a field of the current dict item
+ k, v = stripped.split(':', 1)
+ current_dict[k.strip()] = v.strip().strip('"').strip("'")
+
+ # Save final list/dict if any
+ if in_list and current_key:
+ if in_dict_item and current_dict:
+ current_list.append(current_dict)
+ frontmatter[current_key] = current_list
+
+ return frontmatter, message
+
+
+def load_rules(event: Optional[str] = None) -> List[Rule]:
+ """Load all hookify rules from .claude directory.
+
+ Args:
+ event: Optional event filter ("bash", "file", "stop", etc.)
+
+ Returns:
+ List of enabled Rule objects matching the event.
+ """
+ rules = []
+
+ # Find all hookify.*.local.md files
+ pattern = os.path.join('.claude', 'hookify.*.local.md')
+ files = glob.glob(pattern)
+
+ for file_path in files:
+ try:
+ rule = load_rule_file(file_path)
+ if not rule:
+ continue
+
+ # Filter by event if specified
+ if event:
+ if rule.event != 'all' and rule.event != event:
+ continue
+
+ # Only include enabled rules
+ if rule.enabled:
+ rules.append(rule)
+
+ except (IOError, OSError, PermissionError) as e:
+ # File I/O errors - log and continue
+ print(f"Warning: Failed to read {file_path}: {e}", file=sys.stderr)
+ continue
+ except (ValueError, KeyError, AttributeError, TypeError) as e:
+ # Parsing errors - log and continue
+ print(f"Warning: Failed to parse {file_path}: {e}", file=sys.stderr)
+ continue
+ except Exception as e:
+ # Unexpected errors - log with type details
+ print(f"Warning: Unexpected error loading {file_path} ({type(e).__name__}): {e}", file=sys.stderr)
+ continue
+
+ return rules
+
+
+def load_rule_file(file_path: str) -> Optional[Rule]:
+ """Load a single rule file.
+
+ Returns:
+ Rule object or None if file is invalid.
+ """
+ try:
+ with open(file_path, 'r') as f:
+ content = f.read()
+
+ frontmatter, message = extract_frontmatter(content)
+
+ if not frontmatter:
+ print(f"Warning: {file_path} missing YAML frontmatter (must start with ---)", file=sys.stderr)
+ return None
+
+ rule = Rule.from_dict(frontmatter, message)
+ return rule
+
+ except (IOError, OSError, PermissionError) as e:
+ print(f"Error: Cannot read {file_path}: {e}", file=sys.stderr)
+ return None
+ except (ValueError, KeyError, AttributeError, TypeError) as e:
+ print(f"Error: Malformed rule file {file_path}: {e}", file=sys.stderr)
+ return None
+ except UnicodeDecodeError as e:
+ print(f"Error: Invalid encoding in {file_path}: {e}", file=sys.stderr)
+ return None
+ except Exception as e:
+ print(f"Error: Unexpected error parsing {file_path} ({type(e).__name__}): {e}", file=sys.stderr)
+ return None
+
+
+# For testing
+if __name__ == '__main__':
+ import sys
+
+ # Test frontmatter parsing
+ test_content = """---
+name: test-rule
+enabled: true
+event: bash
+pattern: "rm -rf"
+---
+
+⚠️ Dangerous command detected!
+"""
+
+ fm, msg = extract_frontmatter(test_content)
+ print("Frontmatter:", fm)
+ print("Message:", msg)
+
+ rule = Rule.from_dict(fm, msg)
+ print("Rule:", rule)
diff --git a/plugins/hookify/core/rule_engine.py b/plugins/hookify/core/rule_engine.py
new file mode 100644
index 00000000..8244c005
--- /dev/null
+++ b/plugins/hookify/core/rule_engine.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+"""Rule evaluation engine for hookify plugin."""
+
+import re
+import sys
+from functools import lru_cache
+from typing import List, Dict, Any, Optional
+
+# Import from local module
+from hookify.core.config_loader import Rule, Condition
+
+
+# Cache compiled regexes (max 128 patterns)
+@lru_cache(maxsize=128)
+def compile_regex(pattern: str) -> re.Pattern:
+ """Compile regex pattern with caching.
+
+ Args:
+ pattern: Regex pattern string
+
+ Returns:
+ Compiled regex pattern
+ """
+ return re.compile(pattern, re.IGNORECASE)
+
+
+class RuleEngine:
+ """Evaluates rules against hook input data."""
+
+ def __init__(self):
+ """Initialize rule engine."""
+ # No need for instance cache anymore - using global lru_cache
+ pass
+
+ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[str, Any]:
+ """Evaluate all rules and return combined results.
+
+ Checks all rules and accumulates matches. Blocking rules take priority
+ over warning rules. All matching rule messages are combined.
+
+ Args:
+ rules: List of Rule objects to evaluate
+ input_data: Hook input JSON (tool_name, tool_input, etc.)
+
+ Returns:
+ Response dict with systemMessage, hookSpecificOutput, etc.
+ Empty dict {} if no rules match.
+ """
+ hook_event = input_data.get('hook_event_name', '')
+ blocking_rules = []
+ warning_rules = []
+
+ for rule in rules:
+ if self._rule_matches(rule, input_data):
+ if rule.action == 'block':
+ blocking_rules.append(rule)
+ else:
+ warning_rules.append(rule)
+
+ # If any blocking rules matched, block the operation
+ if blocking_rules:
+ messages = [f"**[{r.name}]**\n{r.message}" for r in blocking_rules]
+ combined_message = "\n\n".join(messages)
+
+ # Use appropriate blocking format based on event type
+ if hook_event == 'Stop':
+ return {
+ "decision": "block",
+ "reason": combined_message,
+ "systemMessage": combined_message
+ }
+ elif hook_event in ['PreToolUse', 'PostToolUse']:
+ return {
+ "hookSpecificOutput": {
+ "hookEventName": hook_event,
+ "permissionDecision": "deny"
+ },
+ "systemMessage": combined_message
+ }
+ else:
+ # For other events, just show message
+ return {
+ "systemMessage": combined_message
+ }
+
+ # If only warnings, show them but allow operation
+ if warning_rules:
+ messages = [f"**[{r.name}]**\n{r.message}" for r in warning_rules]
+ return {
+ "systemMessage": "\n\n".join(messages)
+ }
+
+ # No matches - allow operation
+ return {}
+
+ def _rule_matches(self, rule: Rule, input_data: Dict[str, Any]) -> bool:
+ """Check if rule matches input data.
+
+ Args:
+ rule: Rule to evaluate
+ input_data: Hook input data
+
+ Returns:
+ True if rule matches, False otherwise
+ """
+ # Extract tool information
+ tool_name = input_data.get('tool_name', '')
+ tool_input = input_data.get('tool_input', {})
+
+ # Check tool matcher if specified
+ if rule.tool_matcher:
+ if not self._matches_tool(rule.tool_matcher, tool_name):
+ return False
+
+ # If no conditions, don't match
+ # (Rules must have at least one condition to be valid)
+ if not rule.conditions:
+ return False
+
+ # All conditions must match
+ for condition in rule.conditions:
+ if not self._check_condition(condition, tool_name, tool_input, input_data):
+ return False
+
+ return True
+
+ def _matches_tool(self, matcher: str, tool_name: str) -> bool:
+ """Check if tool_name matches the matcher pattern.
+
+ Args:
+ matcher: Pattern like "Bash", "Edit|Write", "*"
+ tool_name: Actual tool name
+
+ Returns:
+ True if matches
+ """
+ if matcher == '*':
+ return True
+
+ # Split on | for OR matching
+ patterns = matcher.split('|')
+ return tool_name in patterns
+
+ def _check_condition(self, condition: Condition, tool_name: str,
+ tool_input: Dict[str, Any], input_data: Dict[str, Any] = None) -> bool:
+ """Check if a single condition matches.
+
+ Args:
+ condition: Condition to check
+ tool_name: Tool being used
+ tool_input: Tool input dict
+ input_data: Full hook input data (for Stop events, etc.)
+
+ Returns:
+ True if condition matches
+ """
+ # Extract the field value to check
+ field_value = self._extract_field(condition.field, tool_name, tool_input, input_data)
+ if field_value is None:
+ return False
+
+ # Apply operator
+ operator = condition.operator
+ pattern = condition.pattern
+
+ if operator == 'regex_match':
+ return self._regex_match(pattern, field_value)
+ elif operator == 'contains':
+ return pattern in field_value
+ elif operator == 'equals':
+ return pattern == field_value
+ elif operator == 'not_contains':
+ return pattern not in field_value
+ elif operator == 'starts_with':
+ return field_value.startswith(pattern)
+ elif operator == 'ends_with':
+ return field_value.endswith(pattern)
+ else:
+ # Unknown operator
+ return False
+
+ def _extract_field(self, field: str, tool_name: str,
+ tool_input: Dict[str, Any], input_data: Dict[str, Any] = None) -> Optional[str]:
+ """Extract field value from tool input or hook input data.
+
+ Args:
+ field: Field name like "command", "new_text", "file_path", "reason", "transcript"
+ tool_name: Tool being used (may be empty for Stop events)
+ tool_input: Tool input dict
+ input_data: Full hook input (for accessing transcript_path, reason, etc.)
+
+ Returns:
+ Field value as string, or None if not found
+ """
+ # Direct tool_input fields
+ if field in tool_input:
+ value = tool_input[field]
+ if isinstance(value, str):
+ return value
+ return str(value)
+
+ # For Stop events and other non-tool events, check input_data
+ if input_data:
+ # Stop event specific fields
+ if field == 'reason':
+ return input_data.get('reason', '')
+ elif field == 'transcript':
+ # Read transcript file if path provided
+ transcript_path = input_data.get('transcript_path')
+ if transcript_path:
+ try:
+ with open(transcript_path, 'r') as f:
+ return f.read()
+ except FileNotFoundError:
+ print(f"Warning: Transcript file not found: {transcript_path}", file=sys.stderr)
+ return ''
+ except PermissionError:
+ print(f"Warning: Permission denied reading transcript: {transcript_path}", file=sys.stderr)
+ return ''
+ except (IOError, OSError) as e:
+ print(f"Warning: Error reading transcript {transcript_path}: {e}", file=sys.stderr)
+ return ''
+ except UnicodeDecodeError as e:
+ print(f"Warning: Encoding error in transcript {transcript_path}: {e}", file=sys.stderr)
+ return ''
+ elif field == 'user_prompt':
+ # For UserPromptSubmit events
+ return input_data.get('user_prompt', '')
+
+ # Handle special cases by tool type
+ if tool_name == 'Bash':
+ if field == 'command':
+ return tool_input.get('command', '')
+
+ elif tool_name in ['Write', 'Edit']:
+ if field == 'content':
+ # Write uses 'content', Edit has 'new_string'
+ return tool_input.get('content') or tool_input.get('new_string', '')
+ elif field == 'new_text' or field == 'new_string':
+ return tool_input.get('new_string', '')
+ elif field == 'old_text' or field == 'old_string':
+ return tool_input.get('old_string', '')
+ elif field == 'file_path':
+ return tool_input.get('file_path', '')
+
+ elif tool_name == 'MultiEdit':
+ if field == 'file_path':
+ return tool_input.get('file_path', '')
+ elif field in ['new_text', 'content']:
+ # Concatenate all edits
+ edits = tool_input.get('edits', [])
+ return ' '.join(e.get('new_string', '') for e in edits)
+
+ return None
+
+ def _regex_match(self, pattern: str, text: str) -> bool:
+ """Check if pattern matches text using regex.
+
+ Args:
+ pattern: Regex pattern
+ text: Text to match against
+
+ Returns:
+ True if pattern matches
+ """
+ try:
+ # Use cached compiled regex (LRU cache with max 128 patterns)
+ regex = compile_regex(pattern)
+ return bool(regex.search(text))
+
+ except re.error as e:
+ print(f"Invalid regex pattern '{pattern}': {e}", file=sys.stderr)
+ return False
+
+
+# For testing
+if __name__ == '__main__':
+ from hookify.core.config_loader import Condition, Rule
+
+ # Test rule evaluation
+ rule = Rule(
+ name="test-rm",
+ enabled=True,
+ event="bash",
+ conditions=[
+ Condition(field="command", operator="regex_match", pattern=r"rm\s+-rf")
+ ],
+ message="Dangerous rm command!"
+ )
+
+ engine = RuleEngine()
+
+ # Test matching input
+ test_input = {
+ "tool_name": "Bash",
+ "tool_input": {
+ "command": "rm -rf /tmp/test"
+ }
+ }
+
+ result = engine.evaluate_rules([rule], test_input)
+ print("Match result:", result)
+
+ # Test non-matching input
+ test_input2 = {
+ "tool_name": "Bash",
+ "tool_input": {
+ "command": "ls -la"
+ }
+ }
+
+ result2 = engine.evaluate_rules([rule], test_input2)
+ print("Non-match result:", result2)
diff --git a/plugins/hookify/examples/console-log-warning.local.md b/plugins/hookify/examples/console-log-warning.local.md
new file mode 100644
index 00000000..c9352e75
--- /dev/null
+++ b/plugins/hookify/examples/console-log-warning.local.md
@@ -0,0 +1,14 @@
+---
+name: warn-console-log
+enabled: true
+event: file
+pattern: console\.log\(
+action: warn
+---
+
+🔍 **Console.log detected**
+
+You're adding a console.log statement. Please consider:
+- Is this for debugging or should it be proper logging?
+- Will this ship to production?
+- Should this use a logging library instead?
diff --git a/plugins/hookify/examples/dangerous-rm.local.md b/plugins/hookify/examples/dangerous-rm.local.md
new file mode 100644
index 00000000..8226eb19
--- /dev/null
+++ b/plugins/hookify/examples/dangerous-rm.local.md
@@ -0,0 +1,14 @@
+---
+name: block-dangerous-rm
+enabled: true
+event: bash
+pattern: rm\s+-rf
+action: block
+---
+
+⚠️ **Dangerous rm command detected!**
+
+This command could delete important files. Please:
+- Verify the path is correct
+- Consider using a safer approach
+- Make sure you have backups
diff --git a/plugins/hookify/examples/require-tests-stop.local.md b/plugins/hookify/examples/require-tests-stop.local.md
new file mode 100644
index 00000000..87039186
--- /dev/null
+++ b/plugins/hookify/examples/require-tests-stop.local.md
@@ -0,0 +1,22 @@
+---
+name: require-tests-run
+enabled: false
+event: stop
+action: block
+conditions:
+ - field: transcript
+ operator: not_contains
+ pattern: npm test|pytest|cargo test
+---
+
+**Tests not detected in transcript!**
+
+Before stopping, please run tests to verify your changes work correctly.
+
+Look for test commands like:
+- `npm test`
+- `pytest`
+- `cargo test`
+
+**Note:** This rule blocks stopping if no test commands appear in the transcript.
+Enable this rule only when you want strict test enforcement.
diff --git a/plugins/hookify/examples/sensitive-files-warning.local.md b/plugins/hookify/examples/sensitive-files-warning.local.md
new file mode 100644
index 00000000..ae92971d
--- /dev/null
+++ b/plugins/hookify/examples/sensitive-files-warning.local.md
@@ -0,0 +1,18 @@
+---
+name: warn-sensitive-files
+enabled: true
+event: file
+action: warn
+conditions:
+ - field: file_path
+ operator: regex_match
+ pattern: \.env$|\.env\.|credentials|secrets
+---
+
+🔐 **Sensitive file detected**
+
+You're editing a file that may contain sensitive data:
+- Ensure credentials are not hardcoded
+- Use environment variables for secrets
+- Verify this file is in .gitignore
+- Consider using a secrets manager
diff --git a/plugins/hookify/hooks/__init__.py b/plugins/hookify/hooks/__init__.py
new file mode 100755
index 00000000..e69de29b
diff --git a/plugins/hookify/hooks/hooks.json b/plugins/hookify/hooks/hooks.json
new file mode 100644
index 00000000..d65daca7
--- /dev/null
+++ b/plugins/hookify/hooks/hooks.json
@@ -0,0 +1,49 @@
+{
+ "description": "Hookify plugin - User-configurable hooks from .local.md files",
+ "hooks": {
+ "PreToolUse": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.py",
+ "timeout": 10
+ }
+ ]
+ }
+ ],
+ "PostToolUse": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/posttooluse.py",
+ "timeout": 10
+ }
+ ]
+ }
+ ],
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop.py",
+ "timeout": 10
+ }
+ ]
+ }
+ ],
+ "UserPromptSubmit": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/userpromptsubmit.py",
+ "timeout": 10
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/plugins/hookify/hooks/posttooluse.py b/plugins/hookify/hooks/posttooluse.py
new file mode 100755
index 00000000..a9e12cc7
--- /dev/null
+++ b/plugins/hookify/hooks/posttooluse.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+"""PostToolUse hook executor for hookify plugin.
+
+This script is called by Claude Code after a tool executes.
+It reads .claude/hookify.*.local.md files and evaluates rules.
+"""
+
+import os
+import sys
+import json
+
+# CRITICAL: Add plugin root to Python path for imports
+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
+if PLUGIN_ROOT:
+ parent_dir = os.path.dirname(PLUGIN_ROOT)
+ if parent_dir not in sys.path:
+ sys.path.insert(0, parent_dir)
+ if PLUGIN_ROOT not in sys.path:
+ sys.path.insert(0, PLUGIN_ROOT)
+
+try:
+ from hookify.core.config_loader import load_rules
+ from hookify.core.rule_engine import RuleEngine
+except ImportError as e:
+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
+ print(json.dumps(error_msg), file=sys.stdout)
+ sys.exit(0)
+
+
+def main():
+ """Main entry point for PostToolUse hook."""
+ try:
+ # Read input from stdin
+ input_data = json.load(sys.stdin)
+
+ # Determine event type based on tool
+ tool_name = input_data.get('tool_name', '')
+ event = None
+ if tool_name == 'Bash':
+ event = 'bash'
+ elif tool_name in ['Edit', 'Write', 'MultiEdit']:
+ event = 'file'
+
+ # Load rules
+ rules = load_rules(event=event)
+
+ # Evaluate rules
+ engine = RuleEngine()
+ result = engine.evaluate_rules(rules, input_data)
+
+ # Always output JSON (even if empty)
+ print(json.dumps(result), file=sys.stdout)
+
+ except Exception as e:
+ error_output = {
+ "systemMessage": f"Hookify error: {str(e)}"
+ }
+ print(json.dumps(error_output), file=sys.stdout)
+
+ finally:
+ # ALWAYS exit 0
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/plugins/hookify/hooks/pretooluse.py b/plugins/hookify/hooks/pretooluse.py
new file mode 100755
index 00000000..f265c277
--- /dev/null
+++ b/plugins/hookify/hooks/pretooluse.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+"""PreToolUse hook executor for hookify plugin.
+
+This script is called by Claude Code before any tool executes.
+It reads .claude/hookify.*.local.md files and evaluates rules.
+"""
+
+import os
+import sys
+import json
+
+# CRITICAL: Add plugin root to Python path for imports
+# We need to add the parent of the plugin directory so Python can find "hookify" package
+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
+if PLUGIN_ROOT:
+ # Add the parent directory of the plugin
+ parent_dir = os.path.dirname(PLUGIN_ROOT)
+ if parent_dir not in sys.path:
+ sys.path.insert(0, parent_dir)
+
+ # Also add PLUGIN_ROOT itself in case we have other scripts
+ if PLUGIN_ROOT not in sys.path:
+ sys.path.insert(0, PLUGIN_ROOT)
+
+try:
+ from hookify.core.config_loader import load_rules
+ from hookify.core.rule_engine import RuleEngine
+except ImportError as e:
+ # If imports fail, allow operation and log error
+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
+ print(json.dumps(error_msg), file=sys.stdout)
+ sys.exit(0)
+
+
+def main():
+ """Main entry point for PreToolUse hook."""
+ try:
+ # Read input from stdin
+ input_data = json.load(sys.stdin)
+
+ # Determine event type for filtering
+ # For PreToolUse, we use tool_name to determine "bash" vs "file" event
+ tool_name = input_data.get('tool_name', '')
+
+ event = None
+ if tool_name == 'Bash':
+ event = 'bash'
+ elif tool_name in ['Edit', 'Write', 'MultiEdit']:
+ event = 'file'
+
+ # Load rules
+ rules = load_rules(event=event)
+
+ # Evaluate rules
+ engine = RuleEngine()
+ result = engine.evaluate_rules(rules, input_data)
+
+ # Always output JSON (even if empty)
+ print(json.dumps(result), file=sys.stdout)
+
+ except Exception as e:
+ # On any error, allow the operation and log
+ error_output = {
+ "systemMessage": f"Hookify error: {str(e)}"
+ }
+ print(json.dumps(error_output), file=sys.stdout)
+
+ finally:
+ # ALWAYS exit 0 - never block operations due to hook errors
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/plugins/hookify/hooks/stop.py b/plugins/hookify/hooks/stop.py
new file mode 100755
index 00000000..fc299bc6
--- /dev/null
+++ b/plugins/hookify/hooks/stop.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+"""Stop hook executor for hookify plugin.
+
+This script is called by Claude Code when agent wants to stop.
+It reads .claude/hookify.*.local.md files and evaluates stop rules.
+"""
+
+import os
+import sys
+import json
+
+# CRITICAL: Add plugin root to Python path for imports
+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
+if PLUGIN_ROOT:
+ parent_dir = os.path.dirname(PLUGIN_ROOT)
+ if parent_dir not in sys.path:
+ sys.path.insert(0, parent_dir)
+ if PLUGIN_ROOT not in sys.path:
+ sys.path.insert(0, PLUGIN_ROOT)
+
+try:
+ from hookify.core.config_loader import load_rules
+ from hookify.core.rule_engine import RuleEngine
+except ImportError as e:
+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
+ print(json.dumps(error_msg), file=sys.stdout)
+ sys.exit(0)
+
+
+def main():
+ """Main entry point for Stop hook."""
+ try:
+ # Read input from stdin
+ input_data = json.load(sys.stdin)
+
+ # Load stop rules
+ rules = load_rules(event='stop')
+
+ # Evaluate rules
+ engine = RuleEngine()
+ result = engine.evaluate_rules(rules, input_data)
+
+ # Always output JSON (even if empty)
+ print(json.dumps(result), file=sys.stdout)
+
+ except Exception as e:
+ # On any error, allow the operation
+ error_output = {
+ "systemMessage": f"Hookify error: {str(e)}"
+ }
+ print(json.dumps(error_output), file=sys.stdout)
+
+ finally:
+ # ALWAYS exit 0
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/plugins/hookify/hooks/userpromptsubmit.py b/plugins/hookify/hooks/userpromptsubmit.py
new file mode 100755
index 00000000..28ee51fd
--- /dev/null
+++ b/plugins/hookify/hooks/userpromptsubmit.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+"""UserPromptSubmit hook executor for hookify plugin.
+
+This script is called by Claude Code when user submits a prompt.
+It reads .claude/hookify.*.local.md files and evaluates rules.
+"""
+
+import os
+import sys
+import json
+
+# CRITICAL: Add plugin root to Python path for imports
+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
+if PLUGIN_ROOT:
+ parent_dir = os.path.dirname(PLUGIN_ROOT)
+ if parent_dir not in sys.path:
+ sys.path.insert(0, parent_dir)
+ if PLUGIN_ROOT not in sys.path:
+ sys.path.insert(0, PLUGIN_ROOT)
+
+try:
+ from hookify.core.config_loader import load_rules
+ from hookify.core.rule_engine import RuleEngine
+except ImportError as e:
+ error_msg = {"systemMessage": f"Hookify import error: {e}"}
+ print(json.dumps(error_msg), file=sys.stdout)
+ sys.exit(0)
+
+
+def main():
+ """Main entry point for UserPromptSubmit hook."""
+ try:
+ # Read input from stdin
+ input_data = json.load(sys.stdin)
+
+ # Load user prompt rules
+ rules = load_rules(event='prompt')
+
+ # Evaluate rules
+ engine = RuleEngine()
+ result = engine.evaluate_rules(rules, input_data)
+
+ # Always output JSON (even if empty)
+ print(json.dumps(result), file=sys.stdout)
+
+ except Exception as e:
+ error_output = {
+ "systemMessage": f"Hookify error: {str(e)}"
+ }
+ print(json.dumps(error_output), file=sys.stdout)
+
+ finally:
+ # ALWAYS exit 0
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/plugins/hookify/matchers/__init__.py b/plugins/hookify/matchers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/hookify/skills/writing-rules/SKILL.md b/plugins/hookify/skills/writing-rules/SKILL.md
new file mode 100644
index 00000000..008168a4
--- /dev/null
+++ b/plugins/hookify/skills/writing-rules/SKILL.md
@@ -0,0 +1,374 @@
+---
+name: Writing Hookify Rules
+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.
+version: 0.1.0
+---
+
+# Writing Hookify Rules
+
+## Overview
+
+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.
+
+## Rule File Format
+
+### Basic Structure
+
+```markdown
+---
+name: rule-identifier
+enabled: true
+event: bash|file|stop|prompt|all
+pattern: regex-pattern-here
+---
+
+Message to show Claude when this rule triggers.
+Can include markdown formatting, warnings, suggestions, etc.
+```
+
+### Frontmatter Fields
+
+**name** (required): Unique identifier for the rule
+- Use kebab-case: `warn-dangerous-rm`, `block-console-log`
+- Be descriptive and action-oriented
+- Start with verb: warn, prevent, block, require, check
+
+**enabled** (required): Boolean to activate/deactivate
+- `true`: Rule is active
+- `false`: Rule is disabled (won't trigger)
+- Can toggle without deleting rule
+
+**event** (required): Which hook event to trigger on
+- `bash`: Bash tool commands
+- `file`: Edit, Write, MultiEdit tools
+- `stop`: When agent wants to stop
+- `prompt`: When user submits a prompt
+- `all`: All events
+
+**action** (optional): What to do when rule matches
+- `warn`: Show message but allow operation (default)
+- `block`: Prevent operation (PreToolUse) or stop session (Stop events)
+- If omitted, defaults to `warn`
+
+**pattern** (simple format): Regex pattern to match
+- Used for simple single-condition rules
+- Matches against command (bash) or new_text (file)
+- Python regex syntax
+
+**Example:**
+```yaml
+event: bash
+pattern: rm\s+-rf
+```
+
+### Advanced Format (Multiple Conditions)
+
+For complex rules with multiple conditions:
+
+```markdown
+---
+name: warn-env-file-edits
+enabled: true
+event: file
+conditions:
+ - field: file_path
+ operator: regex_match
+ pattern: \.env$
+ - field: new_text
+ operator: contains
+ pattern: API_KEY
+---
+
+You're adding an API key to a .env file. Ensure this file is in .gitignore!
+```
+
+**Condition fields:**
+- `field`: Which field to check
+ - For bash: `command`
+ - For file: `file_path`, `new_text`, `old_text`, `content`
+- `operator`: How to match
+ - `regex_match`: Regex pattern matching
+ - `contains`: Substring check
+ - `equals`: Exact match
+ - `not_contains`: Substring must NOT be present
+ - `starts_with`: Prefix check
+ - `ends_with`: Suffix check
+- `pattern`: Pattern or string to match
+
+**All conditions must match for rule to trigger.**
+
+## Message Body
+
+The markdown content after frontmatter is shown to Claude when the rule triggers.
+
+**Good messages:**
+- Explain what was detected
+- Explain why it's problematic
+- Suggest alternatives or best practices
+- Use formatting for clarity (bold, lists, etc.)
+
+**Example:**
+```markdown
+⚠️ **Console.log detected!**
+
+You're adding console.log to production code.
+
+**Why this matters:**
+- Debug logs shouldn't ship to production
+- Console.log can expose sensitive data
+- Impacts browser performance
+
+**Alternatives:**
+- Use a proper logging library
+- Remove before committing
+- Use conditional debug builds
+```
+
+## Event Type Guide
+
+### bash Events
+
+Match Bash command patterns:
+
+```markdown
+---
+event: bash
+pattern: sudo\s+|rm\s+-rf|chmod\s+777
+---
+
+Dangerous command detected!
+```
+
+**Common patterns:**
+- Dangerous commands: `rm\s+-rf`, `dd\s+if=`, `mkfs`
+- Privilege escalation: `sudo\s+`, `su\s+`
+- Permission issues: `chmod\s+777`, `chown\s+root`
+
+### file Events
+
+Match Edit/Write/MultiEdit operations:
+
+```markdown
+---
+event: file
+pattern: console\.log\(|eval\(|innerHTML\s*=
+---
+
+Potentially problematic code pattern detected!
+```
+
+**Match on different fields:**
+```markdown
+---
+event: file
+conditions:
+ - field: file_path
+ operator: regex_match
+ pattern: \.tsx?$
+ - field: new_text
+ operator: regex_match
+ pattern: console\.log\(
+---
+
+Console.log in TypeScript file!
+```
+
+**Common patterns:**
+- Debug code: `console\.log\(`, `debugger`, `print\(`
+- Security risks: `eval\(`, `innerHTML\s*=`, `dangerouslySetInnerHTML`
+- Sensitive files: `\.env$`, `credentials`, `\.pem$`
+- Generated files: `node_modules/`, `dist/`, `build/`
+
+### stop Events
+
+Match when agent wants to stop (completion checks):
+
+```markdown
+---
+event: stop
+pattern: .*
+---
+
+Before stopping, verify:
+- [ ] Tests were run
+- [ ] Build succeeded
+- [ ] Documentation updated
+```
+
+**Use for:**
+- Reminders about required steps
+- Completion checklists
+- Process enforcement
+
+### prompt Events
+
+Match user prompt content (advanced):
+
+```markdown
+---
+event: prompt
+conditions:
+ - field: user_prompt
+ operator: contains
+ pattern: deploy to production
+---
+
+Production deployment checklist:
+- [ ] Tests passing?
+- [ ] Reviewed by team?
+- [ ] Monitoring ready?
+```
+
+## Pattern Writing Tips
+
+### Regex Basics
+
+**Literal characters:** Most characters match themselves
+- `rm` matches "rm"
+- `console.log` matches "console.log"
+
+**Special characters need escaping:**
+- `.` (any char) → `\.` (literal dot)
+- `(` `)` → `\(` `\)` (literal parens)
+- `[` `]` → `\[` `\]` (literal brackets)
+
+**Common metacharacters:**
+- `\s` - whitespace (space, tab, newline)
+- `\d` - digit (0-9)
+- `\w` - word character (a-z, A-Z, 0-9, _)
+- `.` - any character
+- `+` - one or more
+- `*` - zero or more
+- `?` - zero or one
+- `|` - OR
+
+**Examples:**
+```
+rm\s+-rf Matches: rm -rf, rm -rf
+console\.log\( Matches: console.log(
+(eval|exec)\( Matches: eval( or exec(
+chmod\s+777 Matches: chmod 777, chmod 777
+API_KEY\s*= Matches: API_KEY=, API_KEY =
+```
+
+### Testing Patterns
+
+Test regex patterns before using:
+
+```bash
+python3 -c "import re; print(re.search(r'your_pattern', 'test text'))"
+```
+
+Or use online regex testers (regex101.com with Python flavor).
+
+### Common Pitfalls
+
+**Too broad:**
+```yaml
+pattern: log # Matches "log", "login", "dialog", "catalog"
+```
+Better: `console\.log\(|logger\.`
+
+**Too specific:**
+```yaml
+pattern: rm -rf /tmp # Only matches exact path
+```
+Better: `rm\s+-rf`
+
+**Escaping issues:**
+- YAML quoted strings: `"pattern"` requires double backslashes `\\s`
+- YAML unquoted: `pattern: \s` works as-is
+- **Recommendation**: Use unquoted patterns in YAML
+
+## File Organization
+
+**Location:** All rules in `.claude/` directory
+**Naming:** `.claude/hookify.{descriptive-name}.local.md`
+**Gitignore:** Add `.claude/*.local.md` to `.gitignore`
+
+**Good names:**
+- `hookify.dangerous-rm.local.md`
+- `hookify.console-log.local.md`
+- `hookify.require-tests.local.md`
+- `hookify.sensitive-files.local.md`
+
+**Bad names:**
+- `hookify.rule1.local.md` (not descriptive)
+- `hookify.md` (missing .local)
+- `danger.local.md` (missing hookify prefix)
+
+## Workflow
+
+### Creating a Rule
+
+1. Identify unwanted behavior
+2. Determine which tool is involved (Bash, Edit, etc.)
+3. Choose event type (bash, file, stop, etc.)
+4. Write regex pattern
+5. Create `.claude/hookify.{name}.local.md` file in project root
+6. Test immediately - rules are read dynamically on next tool use
+
+### Refining a Rule
+
+1. Edit the `.local.md` file
+2. Adjust pattern or message
+3. Test immediately - changes take effect on next tool use
+
+### Disabling a Rule
+
+**Temporary:** Set `enabled: false` in frontmatter
+**Permanent:** Delete the `.local.md` file
+
+## Examples
+
+See `${CLAUDE_PLUGIN_ROOT}/examples/` for complete examples:
+- `dangerous-rm.local.md` - Block dangerous rm commands
+- `console-log-warning.local.md` - Warn about console.log
+- `sensitive-files-warning.local.md` - Warn about editing .env files
+
+## Quick Reference
+
+**Minimum viable rule:**
+```markdown
+---
+name: my-rule
+enabled: true
+event: bash
+pattern: dangerous_command
+---
+
+Warning message here
+```
+
+**Rule with conditions:**
+```markdown
+---
+name: my-rule
+enabled: true
+event: file
+conditions:
+ - field: file_path
+ operator: regex_match
+ pattern: \.ts$
+ - field: new_text
+ operator: contains
+ pattern: any
+---
+
+Warning message
+```
+
+**Event types:**
+- `bash` - Bash commands
+- `file` - File edits
+- `stop` - Completion checks
+- `prompt` - User input
+- `all` - All events
+
+**Field options:**
+- Bash: `command`
+- File: `file_path`, `new_text`, `old_text`, `content`
+- Prompt: `user_prompt`
+
+**Operators:**
+- `regex_match`, `contains`, `equals`, `not_contains`, `starts_with`, `ends_with`
diff --git a/plugins/hookify/utils/__init__.py b/plugins/hookify/utils/__init__.py
new file mode 100644
index 00000000..e69de29b