sipeed/picoclaw · error

process hook %q closed while waiting for %s

Error message

process hook %q closed while waiting for %s

What it means

Returned by ProcessHook.call when the pending-response channel was closed while a request was outstanding: the hook's read loop exited (subprocess died, stdout closed, or Close() called) and cleaned up pending calls before the response for `method` arrived. The request was sent but can never be answered by this hook instance.

Source

Thrown at pkg/agent/hook_process.go:358

	}
	if params != nil {
		body, err := json.Marshal(params)
		if err != nil {
			ph.removePending(id)
			return err
		}
		msg.Params = body
	}

	if err := ph.send(ctx, msg); err != nil {
		ph.removePending(id)
		return err
	}

	select {
	case resp, ok := <-respCh:
		if !ok {
			return fmt.Errorf("process hook %q closed while waiting for %s", ph.name, method)
		}
		if resp.Error != nil {
			return fmt.Errorf("process hook %q %s failed: %s", ph.name, method, resp.Error.Message)
		}
		if out != nil && len(resp.Result) > 0 {
			if err := json.Unmarshal(resp.Result, out); err != nil {
				return fmt.Errorf("decode process hook %q %s result: %w", ph.name, method, err)
			}
		}
		return nil
	case <-ctx.Done():
		ph.removePending(id)
		return ctx.Err()
	}
}

func (ph *ProcessHook) send(ctx context.Context, msg processHookRPCMessage) error {
	body, err := json.Marshal(msg)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check the hook process's stderr/logs to find why it exited (panic, exit code, unhandled input)
  2. Ensure the hook stays alive across requests: a stdio JSON-RPC hook must loop reading newline-delimited messages, never exit after one
  3. Guard against malformed payloads that crash the hook (validate params server-side in the hook)
  4. If shutdown-triggered, cancel the calling context before closing hooks so RPCs stop gracefully
Defensive patterns

Strategy: retry

Type guard

func isHookClosedWhileWaiting(err error) bool {
    return err != nil && strings.Contains(err.Error(), "closed while waiting for")
}

Try / catch

err := ph.CallApproveTool(ctx, payload)
if err != nil {
    if isHookClosedWhileWaiting(err) {
        // process died mid-RPC: recreate hook and retry once
        ph2, mkErr := NewProcessHook(ctx, ph.Name(), ph.Options())
        if mkErr != nil { return err }
        defer ph2.Close()
        return ph2.CallApproveTool(ctx, payload)
    }
    return err
}

Prevention

When it happens

Trigger: The hook process crashes, exits, or is closed between send() of the JSON-RPC request and delivery of the response — e.g. the hook binary hits a panic, exits after handling a previous method, or the runtime is shutting down mid-RPC.

Common situations: Hook script that calls sys.exit/os.Exit after one request; hook crashing on a specific method's payload; hook killed by the isolation layer or an external supervisor; parent agent shutting down during a tool-approval call.

Related errors


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