sipeed/picoclaw · error

create process hook stdout: %w

Error message

create process hook stdout: %w

What it means

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.

Source

Thrown at pkg/agent/hook_process.go:121

}

func NewProcessHook(ctx context.Context, name string, opts ProcessHookOptions) (*ProcessHook, error) {
	if len(opts.Command) == 0 {
		return nil, fmt.Errorf("process hook command is required")
	}

	cmd := exec.Command(opts.Command[0], opts.Command[1:]...)
	cmd.Dir = opts.Dir
	if len(opts.Env) > 0 {
		cmd.Env = append(os.Environ(), opts.Env...)
	}
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, fmt.Errorf("create process hook stdin: %w", err)
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, fmt.Errorf("create process hook stdout: %w", err)
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, fmt.Errorf("create process hook stderr: %w", err)
	}
	// Route hook subprocess startup through the shared isolation entry point so
	// process hooks inherit the same isolation behavior as other child processes.
	if err := isolation.Start(cmd); err != nil {
		return nil, fmt.Errorf("start process hook: %w", err)
	}

	ph := &ProcessHook{
		name:         name,
		opts:         opts,
		cmd:          cmd,
		stdin:        stdin,
		observeKinds: newProcessHookObserveKinds(opts.ObserveKinds),
		pending:      make(map[uint64]chan processHookRPCMessage),

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Raise the fd limit (ulimit -n / container nofile) so all three hook pipes can be created
  2. Close or reuse existing ProcessHook instances and other fd holders before mounting new hooks
  3. If it recurs, audit the process for leaked pipes with lsof and fix the leak
Defensive patterns

Strategy: try-catch

Type guard

func isStdoutPipeError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "create process hook stdout:")
}

Try / catch

ph, err := NewProcessHook(ctx, name, opts)
if err != nil {
    if isStdoutPipeError(err) {
        return fmt.Errorf("fd exhaustion while mounting hook %q: raise ulimit -n: %w", name, err)
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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