chenhg5/cc-connect · error

hook exec: %w (stderr: %s)

Error message

hook exec: %w (stderr: %s)

What it means

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.

Source

Thrown at agent/claudecode/cc_hooks.go:265

	stdinJSON, err := json.Marshal(stdinData)
	if err != nil {
		return ccHookDecision{}, fmt.Errorf("marshal stdin: %w", err)
	}

	timeoutCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
	defer cancel()

	cmd := exec.CommandContext(timeoutCtx, "sh", "-c", command)
	cmd.Stdin = bytes.NewReader(stdinJSON)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	// Strip the skip flag so the hook does real work when cc-connect
	// calls it (even if the host environment has it set).
	cmd.Env = filterEnv(os.Environ(), "CC_CONNECT_PERMISSION_HOOK_SKIP")

	if err := cmd.Run(); err != nil {
		return ccHookDecision{}, fmt.Errorf("hook exec: %w (stderr: %s)", err, truncateStr(strings.TrimSpace(stderr.String()), 200))
	}

	return parseHookOutput(stdout.Bytes())
}

// parseHookOutput parses hook stdout into a decision.
func parseHookOutput(data []byte) (ccHookDecision, error) {
	trimmed := bytes.TrimSpace(data)
	if len(trimmed) == 0 {
		return ccHookDecision{}, nil // empty = ask/fallthrough
	}

	// Try plain text first: "allow", "deny", "ask".
	text := strings.ToLower(string(trimmed))
	switch text {
	case "allow":
		return ccHookDecision{Behavior: "allow"}, nil
	case "deny":

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Read the stderr excerpt in the error message — it names the failing script and line; fix the hook script's bug
  2. Verify every binary the hook command invokes exists in PATH for the cc-connect process (`which <bin>` as the same user)
  3. If the error is 'signal: killed', the hook exceeded 60s — make the hook faster or remove blocking calls (e.g. network waits)
  4. Test the command standalone: `sh -c '<command>' < test-input.json` and confirm exit code 0 with valid JSON stdout
Defensive patterns

Strategy: try-catch

Validate before calling

if _, err := exec.LookPath("sh"); err != nil { return err }
// dry-run the hook command before wiring it:
cmd := exec.Command("sh", "-c", command); cmd.Stdin = strings.NewReader(`{}`); if err := cmd.Run(); err != nil { return fmt.Errorf("hook dry-run failed: %w", err) }

Try / catch

decision, err := runHookCommand(ctx, command, stdinData)
if err != nil {
    var exitErr *exec.ExitError
    if errors.As(err, &exitErr) {
        log.Errorf("hook exited %d; stderr: %s", exitErr.ExitCode(), exitErr.Stderr)
    } else if ctx.Err() == context.DeadlineExceeded {
        log.Error("hook exceeded 60s timeout")
    }
    return err
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "git command failed": what it means when a tool shells out to git and git exits non-zero — this error's family across 21 libraries.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/b0a1fc3b78989827. Report an issue: GitHub.