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:

  1. Input deserialization: json.load(sys.stdin) parses the tool request (line 30 of pretooluse.py).
  2. Rule hydration: load_rules() in plugins/hookify/core/config_loader.py discovers and parses every .claude/hookify.*.local.md file using glob.glob() (lines 13‑28).
  3. Condition evaluation: RuleEngine.evaluate_rules() in plugins/hookify/core/rule_engine.py tests 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →