docker/cli · error

unexpected hook response type: ${Type}

Error message

unexpected hook response type: ${Type}

What it means

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).

Source

Thrown at cli-plugins/manager/hooks.go:96

		}

		resp, err := p.RunHook(ctx, hooks.Request{
			RootCmd:      match,
			Flags:        flags,
			CommandError: cmdErrorMessage,
		})
		if err != nil {
			return nil, false, err
		}

		var message hooks.Response
		if err := json.Unmarshal(resp, &message); err != nil {
			return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
		}

		// currently the only hook type
		if message.Type != hooks.NextSteps {
			return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
		}

		messages, err = hooks.ParseTemplate(message.Template, subCmd)
		if err != nil {
			return nil, false, err
		}

		return messages, true, nil
	}

	for pluginName, pluginCfg := range pluginsCfg {
		messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
		if err != nil {
			// skip misbehaving plugins, but don't halt execution
			logrus.WithFields(logrus.Fields{
				"error":  err,
				"plugin": pluginName,
			}).Debug("Plugin hook invocation failed")

View on GitHub (pinned to 4f84911bfe)

Solutions

  1. In the plugin's hook subcommand, always emit a JSON object with Type set to 0 (hooks.NextSteps): {"Type": 0, "Template": "..."}.
  2. Use the hooks.Response struct from github.com/docker/cli/cli-plugins/hooks so the wire format is correct.
  3. Verify the plugin binary is up to date and not a stale build using an older experimental response format.
  4. If you do not need hooks, remove the plugin's entry from the "hooks"/"error-hooks" config keys to prevent invocation.

Example fix

// before — plugin emits a custom type
resp := struct{ Type int; Template string }{Type: 2, Template: "done"}
// after — use the contract type
resp := hooks.Response{Type: hooks.NextSteps, Template: "done"}
Defensive patterns

Strategy: validation

Validate before calling

// Plugin side: always construct the response from the contract type so Type is valid.
resp := hooks.Response{Type: hooks.NextSteps, Template: msg}
out, _ := json.Marshal(resp)
fmt.Println(string(out))

Type guard

// Host side: the manager already narrows by checking message.Type == hooks.NextSteps.
// Plugin authors can self-validate before printing:
func isValidResponse(r hooks.Response) bool { return r.Type == hooks.NextSteps }

Try / catch

// The manager catches this per-plugin (cli-plugins/manager/hooks.go:108-116) and logs at Debug,
// continuing with other plugins. Keep that non-fatal handling; do not rethrow.

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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