hashicorp/nomad · error

one of launch command or reattach config must be specified

Error message

one of launch command or reattach config must be specified

What it means

The complementary guard in dispensePlugin: neither a launch command nor a reattach config was provided, so there is no way to obtain a plugin client. go-plugin needs either an exec.Cmd to spawn or a ReattachConfig to connect to an already-running process.

Source

Thrown at helper/pluginutils/loader/loader.go:225

	return instance, nil
}

// Reattach reattaches to a previously launched external plugin.
func (l *PluginLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) {
	return l.dispensePlugin(pluginType, "", "", nil, config, l.logger)
}

// dispensePlugin is used to launch or reattach to an external plugin.
func (l *PluginLoader) dispensePlugin(
	pluginType, apiVersion, cmd string, args []string, reattach *plugin.ReattachConfig,
	logger log.Logger) (PluginInstance, error) {

	var pluginCmd *exec.Cmd
	if cmd != "" && reattach != nil {
		return nil, fmt.Errorf("both launch command and reattach config specified")
	} else if cmd == "" && reattach == nil {
		return nil, fmt.Errorf("one of launch command or reattach config must be specified")
	} else if cmd != "" {
		pluginCmd = exec.Command(cmd, args...)
	}

	client := plugin.NewClient(&plugin.ClientConfig{
		HandshakeConfig:  base.Handshake,
		Plugins:          getPluginMap(pluginType, logger),
		Cmd:              pluginCmd,
		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
		Logger:           logger,
		Reattach:         reattach,
	})

	// Connect via RPC
	rpcClient, err := client.Client()
	if err != nil {
		client.Kill()
		return nil, err

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Ensure the plugin's exePath is resolved (binary exists in plugin_dir) before Dispense.
  2. When reattaching, build and pass a valid *plugin.ReattachConfig (with Protocol, Addr, Pid).
  3. Re-run plugin discovery (NewPluginLoader/init) so the loader has a valid launch command.
  4. Check state/reattach persistence for fields lost or zeroed on restore.

Example fix

// before
instance, err := l.Reattach(nil) // nothing to reattach to
// after
instance, err := l.Reattach(&plugin.ReattachConfig{
  Protocol: plugin.ProtocolGRPC,
  Addr:     addr,
  Pid:      pid,
})
Defensive patterns

Strategy: validation

Validate before calling

func canDispense(cmd string, reattach *plugin.ReattachConfig) error {
    if cmd == "" && reattach == nil {
        return errors.New("need a launch command or a reattach config")
    }
    return nil
}
// plus: ensure exePath resolved before Dispense
if pinfo.exePath == "" && pinfo.factory == nil {
    return errors.New("plugin discovered without executable path")
}

Try / catch

instance, err := dispensePlugin(t, v, cmd, args, reattach, logger)
if err != nil && strings.Contains(err.Error(), "one of launch command or reattach config must be specified") {
    return fmt.Errorf("no launch info for plugin %s; re-run discovery", t)
}

Prevention

When it happens

Trigger: Calling dispensePlugin with cmd == "" and reattach == nil — e.g. Dispense on a plugin whose exePath was never resolved (plugin discovered without a valid binary path), or Reattach called with a nil/empty reattach config.

Common situations: Plugin registry entry missing exePath because discovery failed silently; state restore passing an empty reattach config; tests constructing dispensePlugin calls with placeholder empty strings.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/b775bfbe78702e16. Report an issue: GitHub.