@@ -5,6 +5,7 @@ import (
@@ -12,25 +13,63 @@ import (
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
16+// compiledHook pairs a HookConfig with its compiled matcher regex. A nil
17+// matcher means "match every tool".
18+type compiledHook struct {
19+ cfg config.HookConfig
20+ matcher *regexp.Regexp
15// Runner executes hook commands and aggregates their results.
23// Runner executes hook commands and aggregates their results.
16type Runner struct {
24type Runner struct {
17− hooks []config.HookConfig
25+ hooks []compiledHook
18 cwd string
26 cwd string
19 projectDir string
27 projectDir string
22−// NewRunner creates a Runner from the given hook configs.
30+// NewRunner creates a Runner from the given hook configs. Each hook's
31+// Matcher is compiled here so the Runner is self-sufficient; callers do
32+// not have to pre-compile matchers on the config, and reloads or merges
33+// that rebuild HookConfig values can't silently strip compiled state.
35+// Hooks whose matcher fails to compile are skipped with a warning rather
36+// than treated as match-everything. ValidateHooks is expected to have
37+// caught syntax errors earlier, so this is defense in depth.
23func NewRunner(hooks []config.HookConfig, cwd, projectDir string) *Runner {
38func NewRunner(hooks []config.HookConfig, cwd, projectDir string) *Runner {
39+ compiled := make([]compiledHook, 0, len(hooks))
40+ for _, h := range hooks {
41+ ch := compiledHook{cfg: h}
43+ re, err := regexp.Compile(h.Matcher)
45+ slog.Warn("Hook matcher failed to compile; skipping hook",
46+ "matcher", h.Matcher,
47+ "command", h.Command,
54+ compiled = append(compiled, ch)
24 return &Runner{
56 return &Runner{
25− hooks: hooks,
57+ hooks: compiled,
27 projectDir: projectDir,
59 projectDir: projectDir,
31−// Hooks returns the hook configs the runner was created with.
63+// Hooks returns the hook configs the runner was created with, in config
64+// order. Hooks whose matcher failed to compile at construction are
65+// omitted. Intended for diagnostics; callers should not rely on ordering
66+// or identity beyond that.
32func (r *Runner) Hooks() []config.HookConfig {
67func (r *Runner) Hooks() []config.HookConfig {
68+ out := make([]config.HookConfig, len(r.hooks))
69+ for i, h := range r.hooks {
33− return r.hooks
72+ return out
36// Run executes all matching hooks for the given event and tool, returning
75// Run executes all matching hooks for the given event and tool, returning
@@ -93,9 +132,8 @@ func (r *Runner) Run(ctx context.Context, eventName, sessionID, toolName, toolIn
93func (r *Runner) matchingHooks(toolName string) []config.HookConfig {
132func (r *Runner) matchingHooks(toolName string) []config.HookConfig {
94 var matched []config.HookConfig
133 var matched []config.HookConfig
95 for _, h := range r.hooks {
134 for _, h := range r.hooks {
96− re := h.MatcherRegex()
97− if re == nil || re.MatchString(toolName) {
135+ if h.matcher == nil || h.matcher.MatchString(toolName) {
98− matched = append(matched, h)
136+ matched = append(matched, h.cfg)
101 return matched
139 return matched