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 err

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Don't reuse the ProcessHook after Close(); create a fresh one via NewProcessHook if the hook must keep serving
  2. Order shutdown so in-flight turns finish before hooks are closed, or cancel the turn context first so callers stop invoking the hook
  3. 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

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


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