docker/cli · error

plugin candidate

Error message

plugin candidate %q: %w

What it means

Returned by newPlugin when trimExeSuffix fails for the candidate. On Windows trimExeSuffix requires a .exe extension; on other platforms it is a no-op. The underlying trimExeSuffix error is wrapped with the candidate path for context.

Solutions

  1. On Windows, ensure the plugin binary has a .exe extension.
  2. Rebuild the plugin for Windows so the produced artifact carries .exe.
  3. Rename the file to append .exe if it is a valid executable.
Defensive patterns

Strategy: validation

Try / catch

p, err := newPlugin(candidate, cmds)
if err != nil {
    continue // skip candidate
}

Prevention

When it happens

Trigger: On Windows, a plugin candidate whose filename lacks the .exe extension. On non-Windows this branch is effectively unreachable because trimExeSuffix always succeeds.

Common situations: A plugin binary built/copied on Windows without the .exe suffix, or a script file placed in the plugin directory without the expected extension.

Related errors


AI-assisted analysis of docker/cli@4f84911bfe (2026-08-07). Data as JSON: /api/errors/242bde14c2fb0b37. Report an issue: GitHub.

Appendix: source

Thrown at cli-plugins/manager/plugin.go:75

// Plugin.  If the candidate fails one of the tests then `Plugin.Err`
// is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a
// non-recoverable error.
func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
	path := c.Path()
	if path == "" {
		return Plugin{}, errors.New("plugin candidate path cannot be empty")
	}

	// The candidate listing process should have skipped anything
	// which would fail here, so there are all real errors.
	fullname := filepath.Base(path)
	if fullname == "." {
		return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path)
	}
	var err error
	if fullname, err = trimExeSuffix(fullname); err != nil {
		return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err)
	}
	if !strings.HasPrefix(fullname, metadata.NamePrefix) {
		return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix)
	}

	p := Plugin{
		Name: strings.TrimPrefix(fullname, metadata.NamePrefix),
		Path: path,
	}

	// Now apply the candidate tests, so these update p.Err.
	if !isValidPluginName(p.Name) {
		p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat)
		return p, nil
	}

	for _, cmd := range cmds {
		// Ignore conflicts with commands which are

View on GitHub (pinned to 4f84911bfe)