sipeed/picoclaw · error
process hook %q is closed
Error message
process hook %q is closed
What it means
Returned by ProcessHook.call when an RPC method is invoked on a hook whose closed atomic flag is already set — i.e. Close() was called (or the hook's read loop terminated and marked it closed) before this call. The hook process is gone or shutting down, so no request can be sent.
Source
Thrown at pkg/agent/hook_process.go:327
func (ph *ProcessHook) notify(ctx context.Context, method string, params any) error {
msg := processHookRPCMessage{
JSONRPC: processHookJSONRPCVersion,
Method: method,
}
if params != nil {
body, err := json.Marshal(params)
if err != nil {
return err
}
msg.Params = body
}
return ph.send(ctx, msg)
}
func (ph *ProcessHook) call(ctx context.Context, method string, params any, out any) error {
if ph.closed.Load() {
return fmt.Errorf("process hook %q is closed", ph.name)
}
id := ph.nextID.Add(1)
respCh := make(chan processHookRPCMessage, 1)
ph.pendingMu.Lock()
ph.pending[id] = respCh
ph.pendingMu.Unlock()
msg := processHookRPCMessage{
JSONRPC: processHookJSONRPCVersion,
ID: id,
Method: method,
}
if params != nil {
body, err := json.Marshal(params)
if err != nil {
ph.removePending(id)
return errView on GitHub (pinned to 49183d7e8d)
Solutions
- Don't reuse the ProcessHook after Close(); create a fresh one via NewProcessHook if the hook must keep serving
- Order shutdown so in-flight turns finish before hooks are closed, or cancel the turn context first so callers stop invoking the hook
- Treat this error as terminal for the hook instance — retrying the same call on the same handle will fail again
Defensive patterns
Strategy: try-catch
Type guard
func isHookClosedError(err error) bool {
return err != nil && strings.Contains(err.Error(), "is closed")
} Try / catch
err := ph.CallApproveTool(ctx, payload)
if err != nil {
if isHookClosedError(err) {
// hook instance is dead: rebuild or skip this hook
ph, err = remountHook(ctx)
if err != nil { return err }
err = ph.CallApproveTool(ctx, payload)
}
if err != nil { return err }
} Prevention
- Never share a ProcessHook across goroutines past its Close()
- Close hooks only after all in-flight RPC callers have stopped
- Wrap hook usage so a closed-hook error triggers remount instead of crash
When it happens
Trigger: Calling any hook RPC method (e.g. a before_tool intercept approval) after explicitly calling ph.Close(), or after the hook subprocess exited and the reader marked it closed. Typical in shutdown races: the pipeline is draining while a concurrent turn still tries to invoke the hook.
Common situations: Agent shutdown racing in-flight tool calls; reusing a hook handle obtained before a reconnect; test code that closes the hook in t.Cleanup while a goroutine still uses it.
Related errors
- process hook %q closed while waiting for %s
- process hook %q %s failed: %s
- decode process hook %q %s result: %w
- write process hook %q message: %w
- events: bus is closed
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/e1d3ece389183ca0.
Report an issue: GitHub.