hashicorp/nomad · error · ErrPluginNotExecutable

%w: %q (wraps ErrPluginNotExecutable)

Error message

%w: %q (wraps ErrPluginNotExecutable)

What it means

After confirming the secrets plugin binary exists, `NewExternalSecretsPlugin` checks it is executable via helper.IsExecutable(f) and returns ErrPluginNotExecutable wrapped with the plugin name when it is not. Nomad refuses to launch plugins that lack the executable bit (or equivalent on Windows), so plugin registration fails fast with a typed, detectable error.

Source

Thrown at client/commonplugins/secrets_plugin.go:64

// NewExternalSecretsPlugin creates an instance of a secrets plugin by validating the plugin
// binary exists and is executable, and parsing any string key/value pairs out of the config
// which will be used as environment variables for Fetch.
func NewExternalSecretsPlugin(commonPluginDir string, name string) (*externalSecretsPlugin, error) {
	// validate plugin
	if runtime.GOOS == "windows" {
		name += ".exe"
	}
	executable := filepath.Join(commonPluginDir, SecretsPluginDir, name)
	f, err := os.Stat(executable)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("%w: %q", ErrPluginNotExists, name)
		}
		return nil, err
	}
	if !helper.IsExecutable(f) {
		return nil, fmt.Errorf("%w: %q", ErrPluginNotExecutable, name)
	}

	return &externalSecretsPlugin{pluginPath: executable}, nil
}

func (e *externalSecretsPlugin) Fingerprint(ctx context.Context) (*PluginFingerprint, error) {
	plugCtx, cancel := context.WithTimeout(ctx, SecretsCmdTimeout)
	defer cancel()

	cmd := exec.CommandContext(plugCtx, e.pluginPath, "fingerprint")
	cmd.Env = []string{
		"CPI_OPERATION=fingerprint",
	}

	stdout, stderr, err := runPlugin(cmd, SecretsKillTimeout)
	if err != nil {
		return nil, err
	}

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. chmod +x the plugin binary in the plugin directory
  2. Ensure deployment tooling preserves file modes (e.g. `unzip`, `tar -p`, Docker COPY of a pre-built executable)
  3. Verify the user Nomad runs as has execute permission on the file
  4. Optionally skip optional plugins by checking errors.Is(err, commonplugins.ErrPluginNotExecutable)

Example fix

// host fix
// before: -rw-r--r-- plugins/secrets/secrets-plugin
chmod 0755 /opt/nomad/plugins/secrets/secrets-plugin
// after: -rwxr-xr-x plugins/secrets/secrets-plugin
Defensive patterns

Strategy: type-guard

Validate before calling

info, err := os.Stat(pluginPath)
if err != nil {
    log.Fatal(err)
}
if info.Mode()&0o111 == 0 {
    log.Fatalf("plugin %s is not executable: chmod +x %s", pluginPath, pluginPath)
}

Type guard

func isPluginNotExecutable(err error) bool {
    return errors.Is(err, commonplugins.ErrPluginNotExecutable)
}

Try / catch

p, err := commonplugins.NewExternalSecretsPlugin(logger, name, dir)
switch {
case errors.Is(err, commonplugins.ErrPluginNotExecutable):
    return fmt.Errorf("fix permissions on %s (chmod +x)", name)
case err != nil:
    return err
}

Prevention

When it happens

Trigger: Deploying the secrets plugin binary without +x permissions, e.g. extracting an archive that did not preserve modes, or copying via a tool that resets permissions; running Nomad as a user without execute permission on the file.

Common situations: Ansible/Docker COPY of plugin binaries losing the executable bit; binaries mounted from volumes with noexec or restrictive perms; CI artifacts uploaded without mode preservation then deployed to Nomad plugin dirs.

Related errors


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