{"id":"d18bf2ccde61d592","repo":"docker/cli","slug":"plugin-candidate-path-cannot-be-empty","errorCode":null,"errorMessage":"plugin candidate path cannot be empty","messagePattern":"plugin candidate path cannot be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli-plugins/manager/plugin.go","lineNumber":64,"sourceCode":"\n\treturn json.Marshal((*Alias)(&cp))\n}\n\n// pluginCandidate represents a possible plugin candidate, for mocking purposes.\ntype pluginCandidate interface {\n\tPath() string\n\tMetadata() ([]byte, error)\n}\n\n// newPlugin determines if the given candidate is valid and returns a\n// Plugin.  If the candidate fails one of the tests then `Plugin.Err`\n// is set, and is always a `pluginError`, but the `Plugin` is still\n// returned with no error. An error is only returned due to a\n// non-recoverable error.\nfunc newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {\n\tpath := c.Path()\n\tif path == \"\" {\n\t\treturn Plugin{}, errors.New(\"plugin candidate path cannot be empty\")\n\t}\n\n\t// The candidate listing process should have skipped anything\n\t// which would fail here, so there are all real errors.\n\tfullname := filepath.Base(path)\n\tif fullname == \".\" {\n\t\treturn Plugin{}, fmt.Errorf(\"unable to determine basename of plugin candidate %q\", path)\n\t}\n\tvar err error\n\tif fullname, err = trimExeSuffix(fullname); err != nil {\n\t\treturn Plugin{}, fmt.Errorf(\"plugin candidate %q: %w\", path, err)\n\t}\n\tif !strings.HasPrefix(fullname, metadata.NamePrefix) {\n\t\treturn Plugin{}, fmt.Errorf(\"plugin candidate %q: does not have %q prefix\", path, metadata.NamePrefix)\n\t}\n\n\tp := Plugin{\n\t\tName: strings.TrimPrefix(fullname, metadata.NamePrefix),","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/docker/cli/blob/e9452d6e785f6e365712b9d71bd7517591773c86/cli-plugins/manager/plugin.go#L46-L82","documentation":"newPlugin in cli-plugins/manager/plugin.go validates a plugin candidate before listing/executing it. An empty Path() from the candidate is treated as non-recoverable (unlike name/metadata problems, which are recorded on Plugin.Err), because without a filesystem path the CLI cannot derive the plugin name or execute the binary. It signals a broken candidate source rather than a broken plugin.","triggerScenarios":"A pluginCandidate implementation returns \"\" from Path() — in practice a bug in code feeding candidates to newPlugin: a test/mock candidate with no path set, or a custom candidate enumeration that constructs candidates from empty directory-walk results.","commonSituations":"Mostly hit by developers embedding or extending the cli-plugins/manager package (mocks in tests, custom plugin loaders); virtually never seen by end users of the stock docker CLI, since directory scanning only yields real file paths.","solutions":["Ensure the code constructing the plugin candidate passes the actual binary path (e.g. filepath.Join(dir, entry.Name()))","In tests, set a non-empty path on mock candidates before calling newPlugin","Check the plugin directory enumeration logic for entries with empty names being converted into candidates"],"exampleFix":"// before\nc := &candidate{path: \"\"}\np, err := newPlugin(c, cmds)\n// after\nc := &candidate{path: \"/usr/libexec/docker/cli-plugins/docker-example\"}\np, err := newPlugin(c, cmds)","handlingStrategy":null,"validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":["docker-cli","cli-plugins","validation","go"],"analyzedSha":"e9452d6e785f6e365712b9d71bd7517591773c86","analyzedAt":"2026-08-01T07:09:22.474Z","schemaVersion":2}