1package config
2
3import (
4 "fmt"
5 "log/slog"
6)
7
8// HookEventType represents the lifecycle event when a hook should run.
9type HookEventType string
10
11const (
12 // PreToolUse runs before tool calls and can block them.
13 PreToolUse HookEventType = "pre_tool_use"
14 // PostToolUse runs after tool calls complete.
15 PostToolUse HookEventType = "post_tool_use"
16 // UserPromptSubmit runs when the user submits a prompt, before processing.
17 UserPromptSubmit HookEventType = "user_prompt_submit"
18 // Stop runs when Crush finishes responding.
19 Stop HookEventType = "stop"
20 // SubagentStop runs when subagent tasks complete.
21 SubagentStop HookEventType = "subagent_stop"
22 // PreCompact runs before running a compact operation.
23 PreCompact HookEventType = "pre_compact"
24 // PermissionRequested runs when a permission is requested from the user.
25 PermissionRequested HookEventType = "permission_requested"
26)
27
28// Hook represents a single hook command configuration.
29type Hook struct {
30 // Type is the hook type: "command" or "prompt".
31 Type string `json:"type" jsonschema:"description=Hook type,enum=command,enum=prompt,default=command"`
32 // Command is the shell command to execute (for type: "command").
33 // WARNING: Hook commands execute with Crush's full permissions. Only use trusted commands.
34 Command string `json:"command,omitempty" jsonschema:"description=Shell command to execute for this hook (executes with Crush's permissions),example=echo 'Hook executed'"`
35 // Prompt is the LLM prompt to execute (for type: "prompt").
36 // Use $ARGUMENTS placeholder to include hook context JSON.
37 Prompt string `json:"prompt,omitempty" jsonschema:"description=LLM prompt for intelligent decision making,example=Analyze if all tasks are complete. Context: $ARGUMENTS. Return JSON with decision and reason."`
38 // Timeout is the maximum time in seconds to wait for the hook to complete.
39 // Default is 30 seconds.
40 Timeout *int `json:"timeout,omitempty" jsonschema:"description=Maximum time in seconds to wait for hook completion,default=30,minimum=1,maximum=300"`
41}
42
43// Validate checks hook configuration invariants.
44func (h *Hook) Validate() error {
45 switch h.Type {
46 case "prompt":
47 if h.Prompt == "" {
48 return fmt.Errorf("prompt-based hook missing 'prompt' field")
49 }
50 case "", "command":
51 if h.Command == "" {
52 return fmt.Errorf("command-based hook missing 'command' field")
53 }
54 default:
55 return fmt.Errorf("unsupported hook type: %s", h.Type)
56 }
57 if h.Timeout != nil {
58 if *h.Timeout < 1 {
59 slog.Warn("Hook timeout too low, using minimum",
60 "configured", *h.Timeout, "minimum", 1)
61 v := 1
62 h.Timeout = &v
63 }
64 if *h.Timeout > 300 {
65 slog.Warn("Hook timeout too high, using maximum",
66 "configured", *h.Timeout, "maximum", 300)
67 v := 300
68 h.Timeout = &v
69 }
70 }
71 return nil
72}
73
74// HookMatcher represents a matcher for a specific event type.
75type HookMatcher struct {
76 // Matcher is the tool name or pattern to match (for tool events).
77 // For non-tool events, this can be empty or "*" to match all.
78 // Supports pipe-separated tool names like "edit|write|multiedit".
79 Matcher string `json:"matcher,omitempty" jsonschema:"description=Tool name or pattern to match (e.g. 'bash' 'edit|write' for multiple or '*' for all),example=bash,example=edit|write|multiedit,example=*"`
80 // Hooks is the list of hooks to execute when the matcher matches.
81 Hooks []Hook `json:"hooks" jsonschema:"required,description=List of hooks to execute when matcher matches"`
82}
83
84// HookConfig holds the complete hook configuration.
85type HookConfig map[HookEventType][]HookMatcher
86
87// Validate validates the entire hook configuration.
88func (c HookConfig) Validate() error {
89 for eventType, matchers := range c {
90 for i, matcher := range matchers {
91 for j := range matcher.Hooks {
92 if err := matcher.Hooks[j].Validate(); err != nil {
93 return fmt.Errorf("invalid hook config for %s matcher %d hook %d: %w",
94 eventType, i, j, err)
95 }
96 }
97 }
98 }
99 return nil
100}