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
- Check the hook process's stderr/logs to find why it exited (panic, exit code, unhandled input)
- Ensure the hook stays alive across requests: a stdio JSON-RPC hook must loop reading newline-delimited messages, never exit after one
- Guard against malformed payloads that crash the hook (validate params server-side in the hook)
- 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
- Keep hook processes alive across requests (loop on stdin, never exit after one message)
- Capture hook stderr so crashes are diagnosable
- Retry only after recreating the hook — the old instance can never answer
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
- process hook %q is closed
- write process hook %q message: %w
- process hook %q %s failed: %s
- decode process hook %q %s result: %w
- session already completed
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/47cdccadcb8ec4e3.
Report an issue: GitHub.