canopy-network/canopy · error
plugin launcher not found for %q; checked: %s
Error message
plugin launcher not found for %q; checked: %s
What it means
resolvePluginCtlPath searches known locations (working directory, executable-relative paths, and the source tree) for plugin/<name>/pluginctl.sh and returns this error when none of the candidates exists as a regular file. The message lists every path attempted so the missing location is diagnosable.
Source
Thrown at controller/controller.go:402
candidates = append(candidates,
filepath.Join(exeDir, relPath),
filepath.Join(filepath.Dir(exeDir), relPath),
)
}
// add a path relative to the source tree for local development
if _, sourceFile, _, ok := runtime.Caller(0); ok {
repoRoot := filepath.Dir(filepath.Dir(sourceFile))
candidates = append(candidates, filepath.Join(repoRoot, relPath))
}
// return the first existing file path
for _, candidate := range candidates {
info, err := os.Stat(candidate)
if err == nil && !info.IsDir() {
return candidate, nil
}
}
// exit with a descriptive error containing all attempted paths
return "", fmt.Errorf("plugin launcher not found for %q; checked: %s", plugin, strings.Join(candidates, ", "))
}
// INTERNAL CALLS BELOW
// LoadIsOwnRoot() returns if this chain is its own root (base)
func (c *Controller) LoadIsOwnRoot() (isOwnRoot bool) {
// use the state machine to check if this chain is the root chain
isOwnRoot, err := c.FSM.LoadIsOwnRoot()
// if an error occurred
if err != nil {
// log the error
c.log.Error(err.Error())
}
// exit
return
}
// RootChainId() returns the root chain id according to the FSMView on GitHub (pinned to ee8197d91d)
Solutions
- Check the 'checked:' path list in the error and create/copy pluginctl.sh at one of those exact locations
- Run the binary from the repository root where ./plugin/<name>/pluginctl.sh exists
- Verify the plugin name is spelled correctly (it maps to a plugin subdirectory)
- Install the plugin assets next to the executable (exe_dir/plugin/<name>/pluginctl.sh) for deployed binaries
Example fix
// before $ ./bin/canopyd plugin-enable indexer # run from $HOME // after $ cd /path/to/repo && ./bin/canopyd plugin-enable indexer # or install assets: $ mkdir -p bin/plugin/indexer && cp plugin/indexer/pluginctl.sh bin/plugin/indexer/
Defensive patterns
Strategy: validation
Validate before calling
for _, dir := range candidateDirs() {
p := filepath.Join(dir, "plugin", pluginName, "pluginctl.sh")
if info, err := os.Stat(p); err == nil && !info.IsDir() { return p, nil }
}
return "", fmt.Errorf("pluginctl.sh for %q missing; run make install-plugins", pluginName) Try / catch
path, err := resolvePluginCtlPath(plugin)
if err != nil {
log.Fatalf("plugin install problem: %v", err) // error lists all checked paths
} Prevention
- Install plugin assets alongside the binary at deploy time
- Run daemons from the repo root in dev
- Validate plugin names against a known list before invoking
When it happens
Trigger: runPluginCtl invoking a plugin whose pluginctl.sh is not installed in any of the checked locations: ./plugin/<plugin>/pluginctl.sh, relative to the executable dir, its parent, or the repo root.
Common situations: Running a compiled binary outside the repo without copying the plugin directory, running from the wrong working directory, a plugin name typo, or an incomplete build/install that omitted the plugin assets.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
AI-assisted analysis of canopy-network/canopy@ee8197d91d (2026-09-06).
Data as JSON: /api/errors/f8a94d1594200e3c.
Report an issue: GitHub.