chenhg5/cc-connect · error

marshal stdin: %w

Error message

marshal stdin: %w

What it means

runHookCommand marshals the hook's stdin payload (map[string]any) to JSON before piping it to the configured hook command. This error wraps json.Marshal failure — practically only when the map contains values JSON cannot represent, such as channels, funcs, or NaN/Inf floats injected by a caller.

Source

Thrown at agent/claudecode/cc_hooks.go:249

// matchHookEntry checks if toolName matches the matcher.
// Empty or "*" matcher matches everything. Otherwise exact match.
func matchHookEntry(matcher, toolName string) bool {
	if matcher == "" || matcher == "*" {
		return true
	}
	return strings.EqualFold(matcher, toolName)
}

// runHookCommand executes a hook command with tool info on stdin.
// Returns the parsed decision. Timeout: 60s (matching Claude Code's own).
func runHookCommand(
	ctx context.Context,
	command string,
	stdinData map[string]any,
) (ccHookDecision, error) {
	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))
	}

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Inspect what was put into stdinData and remove/convert non-JSON-serializable values (use a plain string/number/bool/map/slice)
  2. Sanitize numeric values before marshaling — replace NaN/Inf with 0 or omit the field
  3. If adding a new hook field, marshal it in a unit test first to catch UnsupportedTypeError early

Example fix

// before
stdinData["deadline"] = time.Time{} // or a func/NaN value
// after
stdinData["deadline"] = t.UTC().Format(time.RFC3339)
Defensive patterns

Strategy: validation

Validate before calling

if err := json.Valid(mustJSON(stdinData)); err != nil { ... } // or pre-check:
for k, v := range stdinData { if !jsonSerializable(v) { return fmt.Errorf("field %q not JSON-serializable", k) } }

Type guard

func jsonSerializable(v any) bool { _, err := json.Marshal(v); return err == nil }

Try / catch

decision, err := runHookCommand(ctx, cmd, stdinData)
if err != nil {
    var ute *json.UnsupportedTypeError
    if errors.As(err, &ute) { log.Errorf("non-serializable value in hook input: %v", ute.Value) }
    return err
}

Prevention

When it happens

Trigger: tryHook → runHookCommand → json.Marshal(stdinData) returns an UnsupportedTypeError/UnsupportedValueError because the caller populated stdinData with a non-JSON-serializable value (channel, func, cyclic structure, NaN).

Common situations: A developer extended the hook-input construction and passed a non-serializable field (e.g. a time.Time in a legacy format, a func value, or a struct with unexported/marshal-failing types); floats produced by division became NaN.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


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