{"record":{"id":"b0a1fc3b78989827","repo":"chenhg5/cc-connect","slug":"hook-exec-w-stderr-s","errorCode":null,"errorMessage":"hook exec: %w (stderr: %s)","messagePattern":"hook exec: %w \\(stderr: (.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agent/claudecode/cc_hooks.go","lineNumber":265,"sourceCode":"\tstdinJSON, err := json.Marshal(stdinData)\n\tif err != nil {\n\t\treturn ccHookDecision{}, fmt.Errorf(\"marshal stdin: %w\", err)\n\t}\n\n\ttimeoutCtx, cancel := context.WithTimeout(ctx, 60*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(timeoutCtx, \"sh\", \"-c\", command)\n\tcmd.Stdin = bytes.NewReader(stdinJSON)\n\tvar stdout, stderr bytes.Buffer\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\t// Strip the skip flag so the hook does real work when cc-connect\n\t// calls it (even if the host environment has it set).\n\tcmd.Env = filterEnv(os.Environ(), \"CC_CONNECT_PERMISSION_HOOK_SKIP\")\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn ccHookDecision{}, fmt.Errorf(\"hook exec: %w (stderr: %s)\", err, truncateStr(strings.TrimSpace(stderr.String()), 200))\n\t}\n\n\treturn parseHookOutput(stdout.Bytes())\n}\n\n// parseHookOutput parses hook stdout into a decision.\nfunc parseHookOutput(data []byte) (ccHookDecision, error) {\n\ttrimmed := bytes.TrimSpace(data)\n\tif len(trimmed) == 0 {\n\t\treturn ccHookDecision{}, nil // empty = ask/fallthrough\n\t}\n\n\t// Try plain text first: \"allow\", \"deny\", \"ask\".\n\ttext := strings.ToLower(string(trimmed))\n\tswitch text {\n\tcase \"allow\":\n\t\treturn ccHookDecision{Behavior: \"allow\"}, nil\n\tcase \"deny\":","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/agent/claudecode/cc_hooks.go#L247-L283","documentation":"runHookCommand executes the configured permission hook via `sh -c command` with a 60s timeout and reports failure as `hook exec: %w (stderr: %s)`, wrapping the exec error plus up to 200 bytes of the hook's stderr. This error means the hook command itself failed to run or exited non-zero — the wrapper is surfacing the hook's own diagnostics.","triggerScenarios":"tryHook → runHookCommand → cmd.Run() returns an error: the command string references a binary not in PATH, the script exits non-zero (e.g. `deny` via exit code or a crash), or the 60s timeoutCtx kills it (signal: killed).","commonSituations":"User configured a hook command in Claude Code settings that references a script/interpreter missing on the cc-connect host; hook script has a bug and prints an error; hook script waits for input that never arrives and hits the 60s timeout; CC_CONNECT_PERMISSION_HOOK_SKIP filtering interacts with a hook that expects different env.","solutions":["Read the stderr excerpt in the error message — it names the failing script and line; fix the hook script's bug","Verify every binary the hook command invokes exists in PATH for the cc-connect process (`which <bin>` as the same user)","If the error is 'signal: killed', the hook exceeded 60s — make the hook faster or remove blocking calls (e.g. network waits)","Test the command standalone: `sh -c '<command>' < test-input.json` and confirm exit code 0 with valid JSON stdout"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if _, err := exec.LookPath(\"sh\"); err != nil { return err }\n// dry-run the hook command before wiring it:\ncmd := exec.Command(\"sh\", \"-c\", command); cmd.Stdin = strings.NewReader(`{}`); if err := cmd.Run(); err != nil { return fmt.Errorf(\"hook dry-run failed: %w\", err) }","typeGuard":null,"tryCatchPattern":"decision, err := runHookCommand(ctx, command, stdinData)\nif err != nil {\n    var exitErr *exec.ExitError\n    if errors.As(err, &exitErr) {\n        log.Errorf(\"hook exited %d; stderr: %s\", exitErr.ExitCode(), exitErr.Stderr)\n    } else if ctx.Err() == context.DeadlineExceeded {\n        log.Error(\"hook exceeded 60s timeout\")\n    }\n    return err\n}","preventionTips":["Verify hook command binaries exist in the daemon's PATH, not just your interactive shell","Keep hooks fast and non-blocking; never wait on stdin/network inside a permission hook","Test hooks standalone with `sh -c '<command>'` before registering them","Move hook diagnostics to stderr so they surface in the error's stderr excerpt"],"tags":["exec","hooks","subprocess"],"backgroundTag":"git-command-failed","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}