Common Performance Bottlenecks in Claude Plugin Architecture
Claude plugin hooks execute synchronously before every tool call, so latency in regex compilation, file I/O, and rule evaluation directly blocks the user interface.
Every tool invocation in Claude Code triggers a sandboxed hook script that must complete before the operation proceeds. In the anthropics/claude-plugins-official repository, these hooks—pretooluse.py, posttooluse.py, and stop.py—follow a rigid three-step workflow: read JSON from stdin, load rule files via load_rules(), and evaluate conditions through RuleEngine.evaluate_rules(). Because this process runs inside a fresh interpreter for each call, inefficiencies in the core libraries compound into user-visible delays.
The Synchronous Hook Lifecycle
Understanding the execution path is critical to identifying bottlenecks. When Claude Code invokes a tool, the sandbox spawns a new Python process that executes the relevant hook script in plugins/hookify/hooks/. The script orchestrates three sequential operations:
- Input deserialization:
json.load(sys.stdin)parses the tool request (line 30 ofpretooluse.py). - Rule hydration:
load_rules()inplugins/hookify/core/config_loader.pydiscovers and parses every.claude/hookify.*.local.mdfile usingglob.glob()(lines 13‑28). - Condition evaluation:
RuleEngine.evaluate_rules()inplugins/hookify/core/rule_engine.pytests each rule against the input, invoking_regex_match()and_extract_field()for every condition.
The process terminates with sys.exit(0) (line 62 of pretooluse.py), forcing the sandbox to spawn a fresh interpreter for the next tool call. Any latency in steps 1‑3 blocks the user interface.
Six Critical Performance Bottlenecks
The following inefficiencies dominate execution time in high-frequency workflows.
Repeated Regex Compilation
The _regex_match() method historically compiled regular expressions on every evaluation before an optimization introduced an LRU cache. Even with @lru_cache(maxsize=128) in compile_regex() (lines 13‑24 of rule_engine.py), cache misses for large rule sets trigger expensive compilation. Each microsecond of compilation accumulates across dozens of rules per hook call.
Redundant File System Scanning
load_rules() executes glob.glob('.claude/hookify.*.local.md') on every hook invocation (lines 13‑28 of config_loader.py). On networked file systems or high-latency storage, this directory traversal and subsequent YAML parsing creates multi-millisecond stalls before any rule logic executes.
Per-Hook JSON Parsing
While json.load(sys.stdin) in pretooluse.py (lines 30‑33) is relatively lightweight, the repeated allocation and parsing of identical schema structures adds unnecessary overhead when combined with file I/O and regex work.
Expensive Field Extraction
The _extract_field() method in rule_engine.py (lines 202‑226) traverses a long if/elif chain and may read entire transcript files via with open(transcript_path, 'r') for Stop events. Loading large text files into memory for simple metadata checks consumes significant RAM and CPU cycles.
Linear Rule Set Scaling
The load_rules() function iterates over every discovered rule file in a linear for file_path in files: loop. Users with dozens or hundreds of .local.md files incur O(N) loading costs per hook, with no early-exit optimization for event-specific subsets.
Process Startup Penalty
Each hook script terminates with sys.exit(0), destroying the Python interpreter. The sandbox must then spawn a new process, re-import modules, and re-initialize the environment for the subsequent tool call, adding tens to hundreds of milliseconds of fixed overhead.
Optimization Strategies
Mitigating these bottlenecks requires caching, pre-compilation, and architectural shifts toward long-lived processes.
Cache Rule Files in Memory
Persist loaded rules across hook calls using a module-level dictionary. This eliminates disk I/O after the first invocation:
# plugins/hookify/core/config_loader.py
from typing import List, Optional
from dataclasses import dataclass
_RULE_CACHE: dict[str, List[Rule]] = {}
def load_rules(event: Optional[str] = None) -> List[Rule]:
cache_key = event or "all"
if cache_key in _RULE_CACHE:
return _RULE_CACHE[cache_key] # Return cached rules
rules: List[Rule] = []
# Existing glob and YAML parsing logic...
# (lines 13-28 original implementation)
_RULE_CACHE[cache_key] = rules # Store for subsequent calls
return rules
Pre-Compile Regex Patterns at Load Time
Move regex compilation from evaluation time to rule loading time. Store compiled patterns on the condition objects:
# plugins/hookify/core/config_loader.py
def _precompile_conditions(rules: List[Rule]) -> None:
for rule in rules:
for cond in rule.conditions:
cond._compiled = compile_regex(cond.pattern)
# Call immediately before caching
_precompile_conditions(rules)
Then modify the matching logic to use the stored pattern:
# plugins/hookify/core/rule_engine.py
def _regex_match(self, condition: Condition, text: str) -> bool:
return bool(condition._compiled.search(text))
Minimize Field Extraction Scope
Refactor _extract_field() to accept a whitelist of required fields based on the active rule set. Skip the if/elif chain for tool types not referenced by current rules, and replace full transcript file reads with pre-computed metadata hashes.
Implement Rule Batching
Group rules by tool_matcher or event type during loading. Evaluate only the subset relevant to the current hook context rather than testing every rule against every input.
Adopt the Agent SDK for Persistence
The plugins/agent-sdk-dev/ directory provides a framework for long-running plugin processes. Instead of spawning a fresh interpreter per tool call, implement a persistent agent that imports RuleEngine once and processes hook requests via IPC:
# Start the persistent agent once
python -m plugins.agent_sdk_dev.agents.agent_sdk_verifier_py
This eliminates interpreter startup costs and allows the rule cache to persist across the entire Claude Code session.
Key Source Files for Performance Tuning
| File | Critical Function | Performance Impact |
|---|---|---|
plugins/hookify/core/rule_engine.py |
evaluate_rules(), _regex_match(), _extract_field() |
Contains the regex LRU cache and field extraction logic; optimize here to reduce per-rule CPU time. |
plugins/hookify/core/config_loader.py |
load_rules() |
Controls file discovery and YAML parsing; primary source of I/O latency. |
plugins/hookify/hooks/pretooluse.py |
Main execution flow (lines 30‑62) | Orchestrates the bottleneck pipeline; termination via sys.exit(0) forces process respawn. |
plugins/hookify/hooks/stop.py |
Transcript file handling | Demonstrates expensive file reads in _extract_field() for Stop events. |
plugins/agent-sdk-dev/ |
Persistent process framework | Offers the architectural path to eliminate startup overhead. |
Summary
- Regex compilation is mitigated by pre-compiling patterns during rule loading and utilizing the LRU cache in
rule_engine.py. - File I/O latency dominates hook execution; cache rule files in memory using a global dictionary in
config_loader.py. - Process spawning adds fixed overhead per tool call; migrate to the Agent SDK in
plugins/agent-sdk-dev/for persistent execution. - Field extraction costs escalate with transcript file sizes; limit extraction to referenced fields only.
- Rule set scaling is linear; batch rules by event type and impose limits on active rule counts.
Frequently Asked Questions
Why does every Claude tool call feel slower when plugins are enabled?
Each tool invocation triggers a fresh Python process that must rediscover and reload all rule files from disk before executing. The synchronous load_rules() and evaluate_rules() calls in pretooluse.py block the Claude Code interface until completion, making file system latency and regex compilation directly perceptible as UI delays.
How much performance improvement can I expect from caching rule files?
Caching eliminates the glob.glob() and YAML parsing overhead in config_loader.py (lines 13‑28). On networked file systems, this typically reduces hook execution from 50‑200 ms to under 5 ms for subsequent calls within the same session. The first call remains uncached, but all subsequent tool invocations benefit.
Is the LRU cache for regexes sufficient for large rule sets?
The default @lru_cache(maxsize=128) in rule_engine.py handles moderate rule sets effectively. However, if users maintain more than 128 unique regex patterns across all .local.md files, cache eviction will trigger re-compilation. Pre-compiling patterns during rule loading (storing them as cond._compiled) bypasses this limit entirely.
Can I use the Agent SDK with existing hookify plugins?
Yes. The plugins/agent-sdk-dev/ framework is designed to replace the spawn-and-exit model of traditional hooks. Instead of writing scripts that terminate with sys.exit(0), you implement a long-lived agent that imports the RuleEngine once and processes requests via the SDK's IPC mechanism, maintaining your rule cache and compiled regexes across hundreds of tool calls.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →