{"record":{"id":"39bdeefcbae46dab","repo":"docker/cli","slug":"unexpected-hook-response-type-type","errorCode":null,"errorMessage":"unexpected hook response type: ${Type}","messagePattern":"unexpected hook response type: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli-plugins/manager/hooks.go","lineNumber":96,"sourceCode":"\t\t}\n\n\t\tresp, err := p.RunHook(ctx, hooks.Request{\n\t\t\tRootCmd:      match,\n\t\t\tFlags:        flags,\n\t\t\tCommandError: cmdErrorMessage,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\n\t\tvar message hooks.Response\n\t\tif err := json.Unmarshal(resp, &message); err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed to unmarshal hook response (%q): %w\", string(resp), err)\n\t\t}\n\n\t\t// currently the only hook type\n\t\tif message.Type != hooks.NextSteps {\n\t\t\treturn nil, false, errors.New(\"unexpected hook response type: \" + strconv.Itoa(int(message.Type)))\n\t\t}\n\n\t\tmessages, err = hooks.ParseTemplate(message.Template, subCmd)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\n\t\treturn messages, true, nil\n\t}\n\n\tfor pluginName, pluginCfg := range pluginsCfg {\n\t\tmessages, ok, err := tryInvokeHook(pluginName, pluginCfg)\n\t\tif err != nil {\n\t\t\t// skip misbehaving plugins, but don't halt execution\n\t\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\t\"error\":  err,\n\t\t\t\t\"plugin\": pluginName,\n\t\t\t}).Debug(\"Plugin hook invocation failed\")","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/docker/cli/blob/4f84911bfe8811e9b028e4b1fee8e7510be79387/cli-plugins/manager/hooks.go#L78-L114","documentation":"When the CLI invokes a plugin's hook subcommand, it unmarshals the JSON output into hooks.Response and then asserts (cli-plugins/manager/hooks.go:95) that Response.Type equals hooks.NextSteps (the only currently-defined ResponseType, value 0). If a plugin returns any other Type value, the manager returns errors.New(\"unexpected hook response type: \"+<int>) at line 96. The manager catches this per-plugin and logs it at Debug level (it skips misbehaving plugins rather than halting the CLI).","triggerScenarios":"A CLI plugin hook subcommand prints a JSON object whose \"Type\" field is a non-zero integer (e.g. {\"Type\": 1, \"Template\": \"...\"}), or emits malformed/legacy output that unmarshals to a non-NextSteps Type. Most commonly happens when a plugin author invents a custom response type.","commonSituations":"Third-party plugins predating the hooks contract, plugins that treat Type as a string and produce a zero-value/integer mismatch, or plugins copied from a different hook protocol.","solutions":["In the plugin's hook subcommand, always emit a JSON object with Type set to 0 (hooks.NextSteps): {\"Type\": 0, \"Template\": \"...\"}.","Use the hooks.Response struct from github.com/docker/cli/cli-plugins/hooks so the wire format is correct.","Verify the plugin binary is up to date and not a stale build using an older experimental response format.","If you do not need hooks, remove the plugin's entry from the \"hooks\"/\"error-hooks\" config keys to prevent invocation."],"exampleFix":"// before — plugin emits a custom type\nresp := struct{ Type int; Template string }{Type: 2, Template: \"done\"}\n// after — use the contract type\nresp := hooks.Response{Type: hooks.NextSteps, Template: \"done\"}","handlingStrategy":"validation","validationCode":"// Plugin side: always construct the response from the contract type so Type is valid.\nresp := hooks.Response{Type: hooks.NextSteps, Template: msg}\nout, _ := json.Marshal(resp)\nfmt.Println(string(out))","typeGuard":"// Host side: the manager already narrows by checking message.Type == hooks.NextSteps.\n// Plugin authors can self-validate before printing:\nfunc isValidResponse(r hooks.Response) bool { return r.Type == hooks.NextSteps }","tryCatchPattern":"// The manager catches this per-plugin (cli-plugins/manager/hooks.go:108-116) and logs at Debug,\n// continuing with other plugins. Keep that non-fatal handling; do not rethrow.","preventionTips":["Never invent custom ResponseType values; only hooks.NextSteps (0) is accepted.","Build responses with the hooks.Response struct from the SDK, not an anonymous struct.","Add an integration test that runs your plugin's hook subcommand and asserts Type == 0.","Forward-compatibility: emit only known fields; ignore unknown request fields gracefully."],"tags":["cli-plugins","hooks","json","plugin-protocol"],"backgroundTag":null,"analyzedSha":"4f84911bfe8811e9b028e4b1fee8e7510be79387","analyzedAt":"2026-08-07T12:15:29.814Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}