{"record":{"id":"498758875fb7eebe","repo":"sipeed/picoclaw","slug":"create-process-hook-stdout-w","errorCode":null,"errorMessage":"create process hook stdout: %w","messagePattern":"create process hook stdout: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/agent/hook_process.go","lineNumber":121,"sourceCode":"}\n\nfunc NewProcessHook(ctx context.Context, name string, opts ProcessHookOptions) (*ProcessHook, error) {\n\tif len(opts.Command) == 0 {\n\t\treturn nil, fmt.Errorf(\"process hook command is required\")\n\t}\n\n\tcmd := exec.Command(opts.Command[0], opts.Command[1:]...)\n\tcmd.Dir = opts.Dir\n\tif len(opts.Env) > 0 {\n\t\tcmd.Env = append(os.Environ(), opts.Env...)\n\t}\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create process hook stdin: %w\", err)\n\t}\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create process hook stdout: %w\", err)\n\t}\n\tstderr, err := cmd.StderrPipe()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create process hook stderr: %w\", err)\n\t}\n\t// Route hook subprocess startup through the shared isolation entry point so\n\t// process hooks inherit the same isolation behavior as other child processes.\n\tif err := isolation.Start(cmd); err != nil {\n\t\treturn nil, fmt.Errorf(\"start process hook: %w\", err)\n\t}\n\n\tph := &ProcessHook{\n\t\tname:         name,\n\t\topts:         opts,\n\t\tcmd:          cmd,\n\t\tstdin:        stdin,\n\t\tobserveKinds: newProcessHookObserveKinds(opts.ObserveKinds),\n\t\tpending:      make(map[uint64]chan processHookRPCMessage),","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/agent/hook_process.go#L103-L139","documentation":"Returned by NewProcessHook when exec.Cmd.StdoutPipe() fails while creating the pipe that carries the hook's JSON-RPC responses back. Like the stdin case, it is an OS pipe creation failure — almost always fd exhaustion (EMFILE/ENFILE) at the moment of the call.","triggerScenarios":"NewProcessHook invoked after stdin succeeded but the process fd table is full, so the second pipe allocation fails. Wraps the raw os error with the \"create process hook stdout\" context.","commonSituations":"Same class as the stdin failure: fd leaks in long-lived daemons, low container nofile limits, or mounting a large burst of process hooks concurrently.","solutions":["Raise the fd limit (ulimit -n / container nofile) so all three hook pipes can be created","Close or reuse existing ProcessHook instances and other fd holders before mounting new hooks","If it recurs, audit the process for leaked pipes with lsof and fix the leak"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isStdoutPipeError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"create process hook stdout:\")\n}","tryCatchPattern":"ph, err := NewProcessHook(ctx, name, opts)\nif err != nil {\n    if isStdoutPipeError(err) {\n        return fmt.Errorf(\"fd exhaustion while mounting hook %q: raise ulimit -n: %w\", name, err)\n    }\n    return err\n}","preventionTips":["Raise nofile limits in container specs proactively","Reuse a single hook process instead of one per session","Alert on fd usage trends before exhaustion hits"],"tags":["os","process","file-descriptors","pipes","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}