sipeed/picoclaw · error

write process hook %q message: %w

Error message

write process hook %q message: %w

What it means

Returned by ProcessHook.send when the actual write of a marshaled JSON-RPC message to the hook's stdin fails — classically a broken pipe (EPIPE) because the hook subprocess already exited and closed its read end. The write runs in a goroutine with a context-race against ctx cancellation, and this error means the write itself errored.

Source

Thrown at pkg/agent/hook_process.go:398

	body = append(body, '\n')

	ph.writeMu.Lock()
	defer ph.writeMu.Unlock()

	if ph.closed.Load() {
		return fmt.Errorf("process hook %q is closed", ph.name)
	}

	done := make(chan error, 1)
	go func() {
		_, writeErr := ph.stdin.Write(body)
		done <- writeErr
	}()

	select {
	case err := <-done:
		if err != nil {
			return fmt.Errorf("write process hook %q message: %w", ph.name, err)
		}
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (ph *ProcessHook) readLoop(stdout io.Reader) {
	scanner := bufio.NewScanner(stdout)
	scanner.Buffer(make([]byte, 0, 64*1024), processHookReadBufferSize)

	for scanner.Scan() {
		var msg processHookRPCMessage
		if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil {
			logger.WarnCF("hooks", "Failed to decode process hook message", map[string]any{
				"hook":  ph.name,
				"error": err.Error(),
			})

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Find out why the hook exited: its stderr pipe and exit status carry the cause (panic, os.Exit, crash)
  2. Ensure the hook reads stdin continuously and never exits while mounted
  3. Recreate the hook with NewProcessHook once the exit cause is fixed — the old instance is unusable
Defensive patterns

Strategy: fallback

Type guard

func isHookWriteError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "write process hook")
}

Try / catch

if err := ph.Notify(ctx, event); err != nil {
    if isHookWriteError(err) {
        // broken pipe: hook process is gone — fall back to no-hook behavior
        log.Printf("hook unreachable (%v); continuing without it", err)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Any send/notification to a hook whose process has terminated — the OS pipe is still open on the host side but the reader is gone, so Write returns EPIPE/EIO. Distinct from error 271, which fires before writing when the closed flag is already observed.

Common situations: Hook process crashed between calls; hook exited normally but the host didn't notice yet; hook that doesn't keep stdin open; sends racing Close().

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/16332ad109a8b4b7. Report an issue: GitHub.