{"record":{"id":"35d8649a4a765866","repo":"charmbracelet/crush","slug":"hook-s-d-command-is-required","errorCode":null,"errorMessage":"hook %s[%d]: command is required","messagePattern":"hook (.+?)\\[(.+?)\\]: command is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/load.go","lineNumber":1446,"sourceCode":"// ValidateHooks normalizes event names and checks that every configured\n// hook has a command and a syntactically valid matcher regex. Matcher\n// compilation used for matching is owned by hooks.Runner; this function\n// only validates up front so the user sees config errors at load time\n// rather than on the first tool call.\nfunc (c *Config) ValidateHooks() error {\n\t// Normalize event name keys.\n\tfor event, eventHooks := range c.Hooks {\n\t\tcanonical := normalizeHookEvent(event)\n\t\tif canonical != event {\n\t\t\tc.Hooks[canonical] = append(c.Hooks[canonical], eventHooks...)\n\t\t\tdelete(c.Hooks, event)\n\t\t}\n\t}\n\n\tfor event, eventHooks := range c.Hooks {\n\t\tfor i, h := range eventHooks {\n\t\t\tif h.Command == \"\" {\n\t\t\t\treturn fmt.Errorf(\"hook %s[%d]: command is required\", event, i)\n\t\t\t}\n\t\t\tif h.Matcher == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, err := regexp.Compile(h.Matcher); err != nil {\n\t\t\t\treturn fmt.Errorf(\"hook %s[%d]: invalid matcher regex %q: %w\", event, i, h.Matcher, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n","sourceCodeStart":1428,"sourceCodeEnd":1458,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/load.go#L1428-L1458","documentation":"ValidateHooks iterates c.Hooks per event and index. Every hook must have a non-empty Command; a hook entry that only sets a matcher (or is otherwise empty) fails validation with 'hook <event>[<i>]: command is required'. This runs during Load and reloadFromDiskLocked before hooks are registered with the runner.","triggerScenarios":"Load/ValidateHooks encounters a hook in c.Hooks (from the hook builtin in crushrc or the hooks JSON block) whose Command field is the empty string — e.g. hook defined with only a matcher, type, or timeout.","commonSituations":"Copy-pasted hook block with the command line removed or commented out; YAML/JSON key typo like 'cmd' instead of 'command' so Command stays empty; a hook shell variable that expanded to nothing in crushrc.","solutions":["Set the command field for the hook named by <event>[<i>] in the error message","Check for key typos ('cmd' vs 'command') that leave Command empty","If the shell variable holding the command is empty at eval time, guard it or give a default","Remove the hook entry entirely if it is no longer needed"],"exampleFix":"# before (crushrc)\nhook pre_tool_use {\n  matcher \"bash\"\n}\n# after\nhook pre_tool_use {\n  matcher \"bash\"\n  command \"echo 'bash used'\"\n}","handlingStrategy":"validation","validationCode":"for event, hooks := range cfg.Hooks {\n  for i, h := range hooks {\n    if h.Command == \"\" {\n      return fmt.Errorf(\"hook %s[%d] missing command\", event, i)\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"if err := config.Load(ctx, ...); err != nil {\n  var msg string\n  if errors.As(err, new(error)) && strings.Contains(err.Error(), \"command is required\") {\n    // point the user at the exact event[index] named in the message\n  }\n  _ = msg\n}","preventionTips":["Always set both matcher and command together in hook blocks","Use the exact key 'command', not 'cmd'","Guard shell variables used as commands with defaults: ${CMD:-echo}"],"tags":["config","hooks","validation"],"backgroundTag":"hook-command-required","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}