{"record":{"id":"e1d3ece389183ca0","repo":"sipeed/picoclaw","slug":"process-hook-q-is-closed","errorCode":null,"errorMessage":"process hook %q is closed","messagePattern":"process hook %q is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/agent/hook_process.go","lineNumber":327,"sourceCode":"\nfunc (ph *ProcessHook) notify(ctx context.Context, method string, params any) error {\n\tmsg := processHookRPCMessage{\n\t\tJSONRPC: processHookJSONRPCVersion,\n\t\tMethod:  method,\n\t}\n\tif params != nil {\n\t\tbody, err := json.Marshal(params)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmsg.Params = body\n\t}\n\treturn ph.send(ctx, msg)\n}\n\nfunc (ph *ProcessHook) call(ctx context.Context, method string, params any, out any) error {\n\tif ph.closed.Load() {\n\t\treturn fmt.Errorf(\"process hook %q is closed\", ph.name)\n\t}\n\n\tid := ph.nextID.Add(1)\n\trespCh := make(chan processHookRPCMessage, 1)\n\tph.pendingMu.Lock()\n\tph.pending[id] = respCh\n\tph.pendingMu.Unlock()\n\n\tmsg := processHookRPCMessage{\n\t\tJSONRPC: processHookJSONRPCVersion,\n\t\tID:      id,\n\t\tMethod:  method,\n\t}\n\tif params != nil {\n\t\tbody, err := json.Marshal(params)\n\t\tif err != nil {\n\t\t\tph.removePending(id)\n\t\t\treturn err","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/agent/hook_process.go#L309-L345","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isHookClosedError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"is closed\")\n}","tryCatchPattern":"err := ph.CallApproveTool(ctx, payload)\nif err != nil {\n    if isHookClosedError(err) {\n        // hook instance is dead: rebuild or skip this hook\n        ph, err = remountHook(ctx)\n        if err != nil { return err }\n        err = ph.CallApproveTool(ctx, payload)\n    }\n    if err != nil { return err }\n}","preventionTips":["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"],"tags":["hooks","lifecycle","rpc","concurrency","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}