chenhg5/cc-connect · error

write stdin: %w

Error message

write stdin: %w

What it means

writeJSON wraps a failure of cs.stdin.Write as 'write stdin: %w'. The claude process's stdin pipe rejected the write — the process has exited (broken pipe/EPIPE) or the pipe was closed during session teardown. This is the low-level failure behind 'the CLI died while I was sending it a message'.

Source

Thrown at agent/claudecode/session.go:1098

			"request_id": requestID,
			"response":   permResponse,
		},
	}

	slog.Debug("claudeSession: permission response", "request_id", requestID, "behavior", result.Behavior)
	return cs.writeJSON(controlResponse)
}

func (cs *claudeSession) writeJSON(v any) error {
	cs.stdinMu.Lock()
	defer cs.stdinMu.Unlock()

	data, err := json.Marshal(v)
	if err != nil {
		return fmt.Errorf("marshal: %w", err)
	}
	if _, err := cs.stdin.Write(append(data, '\n')); err != nil {
		return fmt.Errorf("write stdin: %w", err)
	}
	return nil
}

func isClaudeEditTool(toolName string) bool {
	switch toolName {
	case "Edit", "Write", "NotebookEdit", "MultiEdit":
		return true
	default:
		return false
	}
}

func (cs *claudeSession) setPermissionMode(mode string) {
	cs.permissionMode.Store(mode)
	cs.autoApprove.Store(mode == "bypassPermissions")
	cs.acceptEditsOnly.Store(mode == "acceptEdits")
	cs.dontAsk.Store(mode == "dontAsk")

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Confirm the claude process is gone (`ps -p <pid>`); if so, recreate the session with StartSession and resend
  2. Handle EPIPE ('broken pipe' in the wrapped error) as 'session died' — mark the session dead and stop further Sends
  3. Check preceding EventError events for the CLI's stderr explaining the crash, and fix that root cause
  4. If writes race Close() in your code, serialize them with the session lifecycle (don't Send after calling Close)

Example fix

// before
if err := cs.writeJSON(msg); err != nil {
    return err // broken pipe repeatedly retried
}
// after
if err := cs.writeJSON(msg); err != nil {
    cs.alive.Store(false) // pipe dead → session dead
    return fmt.Errorf("send to claude session failed, recreate session: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the process is still writable right before sending
func stdinWritable(pid int) bool {
    return pid > 0 && syscall.Kill(pid, 0) == nil // ESRCH means process gone
}

Try / catch

if err := sess.Send(prompt, msgID, nil, nil); err != nil {
    var perr *net.OpError
    if strings.Contains(err.Error(), "write stdin") || errors.Is(err, syscall.EPIPE) {
        // broken pipe: process died mid-write
        markSessionDead(sess)
        sess = recreateSession()
        err = sess.Send(prompt, msgID, nil, nil)
    }
    _ = perr
}

Prevention

When it happens

Trigger: Send or RespondPermission passed the alive check, but the claude process exited just before/concurrently with the write (race), or the write raced Close() closing stdin; large payloads split across writes where the process died mid-write.

Common situations: CLI crash mid-conversation (auth expiry, OOM kill); user sends a message at the same moment the process exits; kill/SIGKILL during teardown; very long prompts hitting a dead process after a timeout.

Related errors


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