sipeed/picoclaw · error

start process hook %q: %w

Error message

start process hook %q: %w

What it means

After config validation, NewProcessHook spawns the hook's command and speaks stdio to it. This wrapper means process startup failed — the binary could not be executed or the process died during startup/handshake. The hook name plus the wrapped exec error identify the culprit; the partially started hook is not mounted.

Source

Thrown at pkg/agent/hook_mount.go:175

			Source:   HookSourceInProcess,
			Hook:     hook,
		}); err != nil {
			return fmt.Errorf("mount builtin hook %q: %w", name, err)
		}
		mounted = append(mounted, name)
	}

	processNames := enabledProcessHookNames(al.cfg.Hooks.Processes)
	for _, name := range processNames {
		spec := al.cfg.Hooks.Processes[name]
		opts, buildErr := processHookOptionsFromConfig(spec)
		if buildErr != nil {
			return fmt.Errorf("configure process hook %q: %w", name, buildErr)
		}

		processHook, buildErr := NewProcessHook(ctx, name, opts)
		if buildErr != nil {
			return fmt.Errorf("start process hook %q: %w", name, buildErr)
		}
		if err := al.MountHook(HookRegistration{
			Name:     name,
			Priority: spec.Priority,
			Source:   HookSourceProcess,
			Hook:     processHook,
		}); err != nil {
			_ = processHook.Close()
			return fmt.Errorf("mount process hook %q: %w", name, err)
		}
		mounted = append(mounted, name)
	}

	return nil
}

func enabledBuiltinHookNames(specs map[string]config.BuiltinHookConfig) []string {
	if len(specs) == 0 {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Run the exact command array from config in the same environment (same user, PATH, dir) to reproduce
  2. Use an absolute path for command[0] instead of relying on PATH
  3. chmod +x the script and ensure a shebang (or invoke the interpreter explicitly: ["python", "/opt/hook.py"])
  4. Verify spec.dir exists and add required env entries under the hook's env map
  5. Check the hook's own stderr/startup logs — an immediately exiting process surfaces here

Example fix

// before
"command": ["audit-hook"]

// after
"command": ["/usr/local/bin/audit-hook"]
// or for scripts:
"command": ["python3", "/opt/picoclaw/hooks/audit_hook.py"]
Defensive patterns

Strategy: validation

Validate before calling

for name, p := range cfg.Hooks.Processes {
    if !p.Enabled || len(p.Command) == 0 {
        continue
    }
    if filepath.IsAbs(p.Command[0]) {
        if _, err := os.Stat(p.Command[0]); err != nil {
            return fmt.Errorf("hooks.processes.%s: binary missing: %w", name, err)
        }
    } else if _, err := exec.LookPath(p.Command[0]); err != nil {
        return fmt.Errorf("hooks.processes.%s: %s not in PATH: %w", name, p.Command[0], err)
    }
    if p.Dir != "" {
        if _, err := os.Stat(p.Dir); err != nil {
            return fmt.Errorf("hooks.processes.%s: dir missing: %w", name, err)
        }
    }
}

Prevention

When it happens

Trigger: hooks.processes.<name>.enabled=true with a command whose binary does not exist in PATH, is not executable, targets the wrong architecture, runs from a nonexistent dir, or exits immediately (missing interpreter like python/node, missing env).

Common situations: PATH differences under systemd/docker (works in shell, fails as a service); script without a shebang or without +x; relative dir that only exists in another cwd; missing env var the hook needs at boot.

Related errors


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