hooks.go

  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}