Hookify — Python Rule Engine for Custom Hooks
Event-Driven Validation with Markdown Rules
Define validation rules in human-readable markdown with YAML frontmatter. A Python rule engine evaluates patterns against tool operations in real time, enabling warn/block actions across 4 hook event types.
🔒 Rule Engine🐍 Python Core📝 Markdown Rules⚡ Real-time Validation
📚 What is this & why does it exist?
Hookify bridges the gap between Claude Code's hook system and custom project rules. Instead of writing raw Python for every validation, you define rules as markdown files with YAML frontmatter specifying events, patterns, and actions. The Python rule_engine.py loads these rules via config_loader.py, evaluates each condition using 6 operators (equals, contains, matches, startsWith, endsWith, not), and returns warn or block decisions through JSON stdout.
💡 Real-world analogy
Think of airport security screening. Each security rule (no liquids over 100ml, no sharp objects) is written on a simple card that any officer can read. The X-ray machine (rule engine) scans every bag (tool operation) and checks it against all the rules. If something matches a “warn” rule, the officer flags it for inspection. If it matches a “block” rule, the bag doesn't get through. Hookify works the same way — readable rules, automated enforcement.
Architecture
flowchart LR
subgraph Events["Hook Events"]
E1["PreToolUse"]
E2["PostToolUse"]
E3["Stop"]
E4["UserPromptSubmit"]
end
subgraph HookScripts["Python Hook Scripts"]
E1 --> H1["pretooluse.py"]
E2 --> H2["posttooluse.py"]
E3 --> H3["stop.py"]
E4 --> H4["userpromptsubmit.py"]
end
subgraph Engine["Rule Engine Core"]
H1 --> CL["config_loader.py"]
H2 --> CL
H3 --> CL
H4 --> CL
CL --> RE["rule_engine.py"]
RE --> OP["6 Operators"]
OP --> D{"Decision"}
end
subgraph Actions["Output"]
D -->|match| W["Warn / Block"]
D -->|no match| A["Allow"]
end
Key Components
| Component | File | Role |
|---|---|---|
config_loader.py | core/config_loader.py | Parses markdown rules, loads YAML frontmatter into Rule and Condition dataclasses |
rule_engine.py | core/rule_engine.py | RuleEngine class with evaluate_rules() and _check_condition() supporting 6 operators |
pretooluse.py | hooks/pretooluse.py | Intercepts tool calls before execution, can block or warn |
posttooluse.py | hooks/posttooluse.py | Validates tool output after execution, logs warnings |
stop.py | hooks/stop.py | Fires when conversation ends, triggers cleanup rules |
userpromptsubmit.py | hooks/userpromptsubmit.py | Validates user prompts before they reach the model |
hooks.json | .claude/hooks.json | Hook registration mapping events to Python scripts |
conversation-analyzer | agents/conversation-analyzer.md | Analyzes conversation patterns to suggest new rules |
writing-rules | skills/writing-rules/SKILL.md | Teaches Claude how to author markdown rules with YAML frontmatter |
no-sudo-rule | rules/no-sudo.md | Example rule: blocks sudo commands in bash tool |
file-size-rule | rules/file-size-check.md | Example rule: warns when writing files over 500 lines |
test-required-rule | rules/test-required.md | Example rule: warns when editing src/ without matching test |
no-secrets-rule | rules/no-secrets.md | Example rule: blocks patterns matching API keys or tokens |
plugin.json | .claude-plugin/plugin.json | Plugin manifest with hook, agent, skill, and command registration |
Rule Authoring Guide
Markdown Rule Format
Rules are markdown files in the rules/ directory with YAML frontmatter defining the rule metadata and a markdown body providing the human-readable description and rationale.
---
name: no-sudo
enabled: true
event: bash
tool_matcher: Bash
conditions:
- field: command
operator: contains
pattern: sudo
action: block
message: "sudo commands are not allowed in this project"
---
# No Sudo Rule
This rule prevents execution of sudo commands through the Bash tool.
Elevated privileges should not be needed for development tasks.
If you need sudo access, contact the infrastructure team.
Operator Usage Examples
equals
conditions:
- field: tool_name
operator: equals
pattern: "Write"
Exact match — tool name must be exactly “Write”.
matches (regex)
conditions:
- field: file_path
operator: matches
pattern: "\\.(env|secret|key)$"
Regex match — catches .env, .secret, and .key files.
startsWith
conditions:
- field: file_path
operator: startsWith
pattern: "/etc/"
Prefix match — blocks writes to system directories.
not (negation)
conditions:
- field: file_path
operator: not
pattern: "src/"
Negated match — triggers when path does NOT contain “src/”.
Multi-Condition Rules
Rules can specify multiple conditions. All conditions must match (AND logic) for the rule to trigger:
---
name: large-file-warning
enabled: true
event: file
tool_matcher: Write
conditions:
- field: file_path
operator: matches
pattern: "\\.(ts|py|js)$"
- field: content_lines
operator: gt
pattern: "500"
action: warn
message: "File exceeds 500 lines - consider splitting"
---
Hook Event Reference
| Event | Script | Trigger | Payload Fields |
|---|---|---|---|
| PreToolUse | pretooluse.py | Before any tool executes | tool_name, tool_input, command, file_path |
| PostToolUse | posttooluse.py | After tool completes | tool_name, tool_input, tool_output, exit_code |
| Stop | stop.py | Conversation ends | reason, turn_count, session_id |
| UserPromptSubmit | userpromptsubmit.py | User sends message | prompt, session_id, turn_number |
Data Flow
sequenceDiagram
participant CC as Claude Code
participant Hook as Python Hook Script
participant CL as config_loader.py
participant RE as rule_engine.py
participant Rules as Markdown Rules
CC->>Hook: JSON stdin (tool name, input, event)
Hook->>CL: load_rules(rules_dir)
CL->>Rules: Parse YAML frontmatter
Rules-->>CL: List of Rule dataclasses
CL-->>Hook: Loaded rules
Hook->>RE: evaluate_rules(event, tool_data, rules)
RE->>RE: Filter rules by event type
RE->>RE: Match tool_matcher pattern
RE->>RE: _check_condition() x6 operators
RE-->>Hook: Decision (allow / warn / block)
Hook-->>CC: JSON stdout (decision + message)
Operator Reference
| Operator | Python Method | Description |
|---|---|---|
equals | == pattern | Exact string match against field value |
contains | in field | Substring match within the field value |
matches | re.search() | Regular expression match against field value |
startsWith | .startswith() | Field value starts with the given prefix |
endsWith | .endswith() | Field value ends with the given suffix |
not | not match | Negates the inner condition result |
Interactions
| Module | Interface | Data Exchanged |
|---|---|---|
| Claude Code Hook System | JSON stdin/stdout | Tool event payload in, decision JSON out |
| File System | .claude/ directory | Reads hooks.json config and markdown rule files |
| Python stdlib | re, json, sys, dataclasses | Regex matching, JSON parsing, data structures |
| YAML frontmatter | Markdown parsing | Rule metadata: event, pattern, conditions, action |
| conversation-analyzer agent | Agent orchestration | Conversation history analysis for rule suggestions |
Configuration Reference
| Parameter | Default | Description |
|---|---|---|
rule.name | (required) | Human-readable rule identifier |
rule.enabled | true | Toggle rule on/off without deleting |
rule.event | all | Target event: bash, file, stop, prompt, or all |
rule.pattern | * | Regex pattern for initial tool name matching |
rule.conditions[] | [] | Array of condition objects to evaluate |
condition.field | (required) | JSON path to the field in the tool event payload |
condition.operator | contains | One of 6 operators: equals, contains, matches, startsWith, endsWith, not |
condition.pattern | (required) | Value or regex to match against the field |
rule.action | warn | Action on match: warn (log and continue) or block (reject tool call) |
rule.tool_matcher | * | Glob pattern for matching tool names (e.g., Bash, Write) |
Directory Map
Source tree for Hookify
hookify/
├── .claude-plugin/plugin.json ← Plugin manifest
├── .claude/hooks.json ← Hook event registration
├── README.md ← Usage documentation
├── core/
│ ├── config_loader.py ← Rule & Condition dataclasses, load_rules()
│ └── rule_engine.py ← RuleEngine, evaluate_rules(), _check_condition()
├── hooks/
│ ├── pretooluse.py ← PreToolUse event handler
│ ├── posttooluse.py ← PostToolUse event handler
│ ├── stop.py ← Stop event handler
│ └── userpromptsubmit.py ← UserPromptSubmit event handler
├── rules/
│ ├── no-sudo.md ← Example: block sudo commands
│ ├── file-size-check.md ← Example: warn on large files
│ ├── test-required.md ← Example: require test for src changes
│ └── no-secrets.md ← Example: block API key patterns
├── agents/
│ └── conversation-analyzer.md ← Suggests rules from conversation patterns
├── skills/
│ └── writing-rules/SKILL.md ← Guide for authoring markdown rules
└── commands/
├── hookify.md ← Main command
├── hookify_list.md ← List active rules
├── hookify_configure.md ← Configure rule engine
└── hookify_help.md ← Help and documentation