Hooks: What to Watch Out For

Mixing fire and fire_sync in the same call path

HookRegistry exposes two dispatch methods: fire and fire_sync. Calling fire_sync from within an async context — or calling fire where the event loop is not running — produces silent failures or deadlocks rather than obvious errors. Decide on one execution model per call path and use HookExecutorSync only when you are certain there is no surrounding event loop.

Priority ordering silently drops hooks when a matcher rejects them

HookRegistry.register accepts a priority parameter, and get_matching_hooks returns only hooks whose HookMatcher.matches returns True for the current context. A hook registered at high priority but with a narrow matcher will never appear in the results without any error. Before assuming a hook is broken, call get_matching_hooks directly with your context dict to verify that the matcher is satisfied.

HookConfig.from_yaml does not validate handler names at load time

When you load a configuration with HookConfig.from_yaml, Python-handler names referenced in the YAML are not resolved until HookExecutor.execute runs. If you pass a python_handlers dict to HookExecutor that is missing a name defined in the config, the failure occurs at execution time — potentially mid-session. To catch mismatches early, instantiate HookExecutor(python_handlers=your_map) and do a dry-run execute call during startup or in your test suite.

Execution log grows unbounded without explicit management

HookRegistry.get_execution_log defaults to returning the last 100 entries, but the underlying log is not automatically capped. In long-running sessions with frequent events, memory usage grows steadily. Call clear_execution_log at session boundaries (for example, after run_pre_compact completes) or use the limit and event_filter parameters to keep queries focused.

apply_learned_patterns injects context that affects subsequent hook evaluation

apply_learned_patterns generates a context-injection string from learned patterns. If you call this before firing hooks that read from the same context dict, the injected content can change which hooks HookMatcher.matches selects. Run apply_learned_patterns after hook dispatch if your matchers depend on unmodified session context, or make a copy of the context dict before injection.

validate_bash_command and validate_file_path return a tuple, not an exception

scripts.security_guard.validate_bash_command and validate_file_path both return tuple[bool, str]. Callers that check only the boolean and discard the message string lose the reason a command was rejected. Always unpack and log or surface the second element — particularly in hooks that run validate_bash_command against tool inputs, where the rejection reason is the actionable signal.

How to reduce risk