sipeed/picoclaw · error

mount builtin hook %q: %w

Error message

mount builtin hook %q: %w

What it means

After a builtin hook builds, it is attached via AgentLoop.MountHook -> HookManager.Mount, which only fails when the hook manager is nil, the name is empty, or the hook value is nil (pkg/agent/hooks.go:274). In this config-driven path the name comes from the config map key and the hook was just built non-nil, so the realistic cause is al.hooks being uninitialized — a hand-constructed AgentLoop that skipped the normal constructor wiring.

Source

Thrown at pkg/agent/hook_mount.go:160

	builtinNames := enabledBuiltinHookNames(al.cfg.Hooks.Builtins)
	for _, name := range builtinNames {
		spec := al.cfg.Hooks.Builtins[name]
		factory, ok := lookupBuiltinHook(name)
		if !ok {
			return fmt.Errorf("builtin hook %q is not registered", name)
		}

		hook, factoryErr := factory(ctx, spec)
		if factoryErr != nil {
			return fmt.Errorf("build builtin hook %q: %w", name, factoryErr)
		}
		if err := al.MountHook(HookRegistration{
			Name:     name,
			Priority: spec.Priority,
			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,

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Construct the AgentLoop through the standard constructor so the hook manager is created
  2. If hooks are not wanted, set hooks.enabled=false rather than building a hook-less loop with hooks on in config
  3. In tests, either use the full constructor or disable hooks in the test config
  4. Check the wrapped message — 'hook manager is not initialized' confirms the wiring diagnosis
Defensive patterns

Strategy: try-catch

Validate before calling

if cfg.Hooks.Enabled && len(cfg.Hooks.Builtins) > 0 {
    if al.HooksManager() == nil { // adapt to your embedding's accessor
        return fmt.Errorf("hooks enabled in config but AgentLoop has no hook manager; use the standard constructor")
    }
}

Try / catch

if err := al.MountHook(reg); err != nil {
    if strings.Contains(err.Error(), "hook manager is not initialized") {
        // wiring bug: rebuild the loop with the standard constructor; not a runtime-transient error
        return fmt.Errorf("loop constructed without hook manager: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: hooks.enabled=true with builtins enabled on an AgentLoop built without a HookManager (custom construction in tests or forks bypassing NewAgentLoop), making MountHook return 'hook manager is not initialized' which gets wrapped here.

Common situations: Unit tests assembling AgentLoop structs literally; forks that build the loop with reflection or partial constructors; disabling subsystems during construction but leaving hooks on in config.

Related errors


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