{"record":{"id":"16332ad109a8b4b7","repo":"sipeed/picoclaw","slug":"write-process-hook-q-message-w","errorCode":null,"errorMessage":"write process hook %q message: %w","messagePattern":"write process hook %q message: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/agent/hook_process.go","lineNumber":398,"sourceCode":"\tbody = append(body, '\\n')\n\n\tph.writeMu.Lock()\n\tdefer ph.writeMu.Unlock()\n\n\tif ph.closed.Load() {\n\t\treturn fmt.Errorf(\"process hook %q is closed\", ph.name)\n\t}\n\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\t_, writeErr := ph.stdin.Write(body)\n\t\tdone <- writeErr\n\t}()\n\n\tselect {\n\tcase err := <-done:\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"write process hook %q message: %w\", ph.name, err)\n\t\t}\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}\n\nfunc (ph *ProcessHook) readLoop(stdout io.Reader) {\n\tscanner := bufio.NewScanner(stdout)\n\tscanner.Buffer(make([]byte, 0, 64*1024), processHookReadBufferSize)\n\n\tfor scanner.Scan() {\n\t\tvar msg processHookRPCMessage\n\t\tif err := json.Unmarshal(scanner.Bytes(), &msg); err != nil {\n\t\t\tlogger.WarnCF(\"hooks\", \"Failed to decode process hook message\", map[string]any{\n\t\t\t\t\"hook\":  ph.name,\n\t\t\t\t\"error\": err.Error(),\n\t\t\t})","sourceCodeStart":380,"sourceCodeEnd":416,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/agent/hook_process.go#L380-L416","documentation":"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.","triggerScenarios":"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.","commonSituations":"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().","solutions":["Find out why the hook exited: its stderr pipe and exit status carry the cause (panic, os.Exit, crash)","Ensure the hook reads stdin continuously and never exits while mounted","Recreate the hook with NewProcessHook once the exit cause is fixed — the old instance is unusable"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":"func isHookWriteError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"write process hook\")\n}","tryCatchPattern":"if err := ph.Notify(ctx, event); err != nil {\n    if isHookWriteError(err) {\n        // broken pipe: hook process is gone — fall back to no-hook behavior\n        log.Printf(\"hook unreachable (%v); continuing without it\", err)\n        return nil\n    }\n    return err\n}","preventionTips":["Supervise hook subprocesses and remount on unexpected exit","Keep hooks reading stdin for their whole lifetime","Don't treat a dead logging/observe hook as fatal to the main flow"],"tags":["hooks","rpc","broken-pipe","process-crash","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}