sipeed/picoclaw · error

decode process hook %q %s result: %w

Error message

decode process hook %q %s result: %w

What it means

Returned by ProcessHook.call when the hook did return a successful JSON-RPC result, but json.Unmarshal of resp.Result into the expected Go type failed. The hook's result payload does not match the schema the host expects for that method.

Source

Thrown at pkg/agent/hook_process.go:365

		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)
	if err != nil {
		return err
	}
	body = append(body, '\n')

	ph.writeMu.Lock()
	defer ph.writeMu.Unlock()

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Make the hook return exactly the documented result shape for that method — correct types, no extra wrapping
  2. Pin host and hook to matching versions so the result schema agrees
  3. Reproduce locally: send the same request to the hook and inspect the raw JSON it emits
  4. Watch for string/bool and string/number coercion mistakes in JS/Python hook implementations

Example fix

// hook response — before
{"result": {"approved": "yes"}}

// hook response — after
{"result": {"approved": true}}
Defensive patterns

Strategy: validation

Validate before calling

// Hook side: self-check the result shape against expected types before returning
func assertResultShape(result any, expected string) error {
    b, _ := json.Marshal(result)
    var probe any
    if err := json.Unmarshal(b, &probe); err != nil {
        return err
    }
    _ = expected // compare keys/types against the documented schema here
    return nil
}

Type guard

func isHookResultDecodeError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "decode process hook")
}

Try / catch

err := ph.CallBeforeLLM(ctx, req, &out)
if err != nil {
    if isHookResultDecodeError(err) {
        // hook returned a malformed result: skip hook rather than fail the turn
        log.Printf("hook %T returned unparseable result: %v", ph, err)
    } else {
        return err
    }
}

Prevention

When it happens

Trigger: A hook returning the wrong JSON shape: e.g. a string where a struct is expected, {"approved": "yes"} (string) instead of {"approved": true}, extra nesting, or numbers as strings. Fires only when `out != nil` and the result body is non-empty.

Common situations: Hand-written hooks that guess the response format; version mismatch between host and hook (schema changed); hooks written in dynamically typed languages (Node/Python) emitting loose types; null vs {} confusion.

Related errors


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