{"record":{"id":"3ec1b156c15a5181","repo":"sipeed/picoclaw","slug":"no-hook-modes-enabled","errorCode":null,"errorMessage":"no hook modes enabled","messagePattern":"no hook modes enabled","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/agent/hook_mount.go","lineNumber":263,"sourceCode":"\topts.ObserveKinds = observeKinds\n\n\tfor _, intercept := range spec.Intercept {\n\t\tswitch intercept {\n\t\tcase \"before_llm\", \"after_llm\":\n\t\t\topts.InterceptLLM = true\n\t\tcase \"before_tool\", \"after_tool\":\n\t\t\topts.InterceptTool = true\n\t\tcase \"approve_tool\":\n\t\t\topts.ApproveTool = true\n\t\tcase \"\":\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn ProcessHookOptions{}, fmt.Errorf(\"unsupported intercept %q\", intercept)\n\t\t}\n\t}\n\n\tif !opts.Observe && !opts.InterceptLLM && !opts.InterceptTool && !opts.ApproveTool {\n\t\treturn ProcessHookOptions{}, fmt.Errorf(\"no hook modes enabled\")\n\t}\n\n\treturn opts, nil\n}\n\nfunc processHookEnvFromMap(envMap map[string]string) []string {\n\tif len(envMap) == 0 {\n\t\treturn nil\n\t}\n\n\tkeys := make([]string, 0, len(envMap))\n\tfor key := range envMap {\n\t\tkeys = append(keys, key)\n\t}\n\tsort.Strings(keys)\n\n\tenv := make([]string, 0, len(keys))\n\tfor _, key := range keys {","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/agent/hook_mount.go#L245-L281","documentation":"Thrown by processHookOptionsFromConfig when a process hook is configured but ends up with zero modes enabled: no observe entries produced a mode and no intercept entries matched. The library refuses to mount a hook that would receive no events at all, since it could never do anything.","triggerScenarios":"A hook config with `enabled: true` (and a command) that declares neither `observe` nor `intercept`, or declares only empty strings — e.g. observe: [\"\"] or intercept: [\"\"], both of which are skipped. Note that observe: [] is falsy for the Observe flag.","commonSituations":"Minimal copy-pasted hook config that only sets command; a config where observe/intercept keys exist but are commented out; refactoring that accidentally drops the mode lists.","solutions":["Add at least one mode to the hook: an observe list (e.g. observe: [\"*\"]) or an intercept entry (e.g. intercept: [\"before_tool\"])","If the hook is not meant to run, remove the entry or set enabled: false instead of leaving a mode-less enabled hook"],"exampleFix":"# before\nhooks:\n  process:\n    my-hook:\n      enabled: true\n      command: [\"./hook\"]\n\n# after\nhooks:\n  process:\n    my-hook:\n      enabled: true\n      command: [\"./hook\"]\n      observe: [\"*\"]\n      intercept: [\"before_tool\"]","handlingStrategy":"validation","validationCode":"func hookHasMode(spec config.ProcessHookConfig) bool {\n    if len(spec.Observe) > 0 {\n        return true // note: observe: [\"\"] alone does NOT enable a mode\n    }\n    for _, v := range spec.Intercept {\n        if v != \"\" {\n            return true\n        }\n    }\n    return false\n}\n\nif spec.Enabled && !hookHasMode(spec) {\n    return fmt.Errorf(\"hook %s enabled without observe or intercept\", name)\n}","typeGuard":null,"tryCatchPattern":"opts, err := processHookOptionsFromConfig(spec)\nif err != nil {\n    return fmt.Errorf(\"hook %q misconfigured: %w\", name, err)\n}","preventionTips":["Never enable a process hook without declaring observe or intercept","Treat observe: [\"\"] as disabled — only non-empty kinds or \"*\" enable observing","Add a startup config assertion that every enabled hook has at least one mode"],"tags":["config","hooks","validation","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}