diff --git a/cmd/cmd.go b/cmd/cmd.go
index 98365b32..d108e5c5 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"log"
 	"os"
+	osexec "os/exec"
 	"path/filepath"
 	"slices"
 	"strings"
@@ -14,9 +15,12 @@ import (
 
 	"asdf/internal/config"
 	"asdf/internal/exec"
+	"asdf/internal/execenv"
+	"asdf/internal/execute"
 	"asdf/internal/help"
 	"asdf/internal/info"
 	"asdf/internal/installs"
+	"asdf/internal/paths"
 	"asdf/internal/plugins"
 	"asdf/internal/resolve"
 	"asdf/internal/shims"
@@ -59,6 +63,15 @@ func Execute(version string) {
 					return currentCommand(logger, tool)
 				},
 			},
+			{
+				Name: "env",
+				Action: func(cCtx *cli.Context) error {
+					shimmedCommand := cCtx.Args().Get(0)
+					args := cCtx.Args().Slice()
+
+					return envCommand(logger, shimmedCommand, args)
+				},
+			},
 			{
 				Name: "exec",
 				Action: func(cCtx *cli.Context) error {
@@ -335,6 +348,83 @@ func formatVersions(versions []string) string {
 	}
 }
 
+func envCommand(logger *log.Logger, shimmedCommand string, args []string) error {
+	command := "env"
+
+	if shimmedCommand == "" {
+		logger.Printf("usage: asdf env <command>")
+		return fmt.Errorf("usage: asdf env <command>")
+	}
+
+	if len(args) >= 2 {
+		command = args[1]
+	}
+
+	realArgs := []string{}
+	if len(args) > 2 {
+		realArgs = args[2:]
+	}
+
+	conf, err := config.LoadConfig()
+	if err != nil {
+		logger.Printf("error loading config: %s", err)
+		return err
+	}
+
+	_, plugin, version, err := getExecutable(logger, conf, shimmedCommand)
+	if err != nil {
+		return err
+	}
+
+	parsedVersion := toolversions.Parse(version)
+	execPaths, err := shims.ExecutablePaths(conf, plugin, parsedVersion)
+	if err != nil {
+		return err
+	}
+	callbackEnv := map[string]string{
+		"ASDF_INSTALL_TYPE":    parsedVersion.Type,
+		"ASDF_INSTALL_VERSION": parsedVersion.Value,
+		"ASDF_INSTALL_PATH":    installs.InstallPath(conf, plugin, parsedVersion),
+		"PATH":                 setPath(conf, execPaths),
+	}
+
+	var env map[string]string
+	var fname string
+
+	if parsedVersion.Type == "system" {
+		env = execute.SliceToMap(os.Environ())
+		newPath := paths.RemoveFromPath(env["PATH"], shims.Directory(conf))
+		env["PATH"] = newPath
+		var found bool
+		fname, found = shims.FindSystemExecutable(conf, command)
+		if !found {
+			fmt.Println("not found")
+			return err
+		}
+	} else {
+		env, err = execenv.Generate(plugin, callbackEnv)
+		if _, ok := err.(plugins.NoCallbackError); !ok && err != nil {
+			return err
+		}
+
+		fname, err = osexec.LookPath(command)
+		if err != nil {
+			return err
+		}
+	}
+
+	err = exec.Exec(fname, realArgs, execute.MapToSlice(env))
+	if err != nil {
+		fmt.Printf("err %#+v\n", err.Error())
+	}
+	return err
+}
+
+func setPath(conf config.Config, pathes []string) string {
+	currentPath := os.Getenv("PATH")
+	return strings.Join(pathes, ":") + ":" + paths.RemoveFromPath(currentPath, shims.Directory(conf))
+}
+
 func execCommand(logger *log.Logger, command string, args []string) error {
 	if command == "" {
 		logger.Printf("usage: asdf exec <command>")
@@ -347,16 +437,51 @@ func execCommand(logger *log.Logger, command string, args []string) error {
 		return err
 	}
 
-	currentDir, err := os.Getwd()
+	executable, plugin, version, err := getExecutable(logger, conf, command)
+	fmt.Printf("version %#+v\n", version)
+	fmt.Println("here")
 	if err != nil {
-		logger.Printf("unable to get current directory: %s", err)
 		return err
 	}
 
-	executable, found, err := shims.FindExecutable(conf, command, currentDir)
+	if len(args) > 1 {
+		args = args[1:]
+	} else {
+		args = []string{}
+	}
 
+	parsedVersion := toolversions.Parse(version)
+	fmt.Printf("parsedVersion %#+v\n", parsedVersion)
+	paths, err := shims.ExecutablePaths(conf, plugin, parsedVersion)
+	if err != nil {
+		return err
+	}
+	callbackEnv := map[string]string{
+		"ASDF_INSTALL_TYPE":    parsedVersion.Type,
+		"ASDF_INSTALL_VERSION": parsedVersion.Value,
+		"ASDF_INSTALL_PATH":    installs.InstallPath(conf, plugin, parsedVersion),
+		"PATH":                 setPath(conf, paths),
+	}
+
+	env, _ := execenv.Generate(plugin, callbackEnv)
+	return exec.Exec(executable, args, execute.MapToSlice(env))
+}
+
+func getExecutable(logger *log.Logger, conf config.Config, command string) (executable string, plugin plugins.Plugin, version string, err error) {
+	currentDir, err := os.Getwd()
+	if err != nil {
+		logger.Printf("unable to get current directory: %s", err)
+		return "", plugins.Plugin{}, "", err
+	}
+
+	executable, plugin, version, found, err := shims.FindExecutable(conf, command, currentDir)
 	if err != nil {
 
+		if _, ok := err.(shims.NoExecutableForPluginError); ok {
+			logger.Printf("No executable %s found for current version. Please select a different version or install %s manually for the current version", command, command)
+			os.Exit(1)
+			return "", plugin, version, err
+		}
 		shimPath := shims.Path(conf, command)
 		toolVersions, _ := shims.GetToolsAndVersionsFromShimFile(shimPath)
 
@@ -383,21 +508,16 @@ func execCommand(logger *log.Logger, command string, args []string) error {
 		}
 
 		os.Exit(126)
-		return err
+		return executable, plugins.Plugin{}, "", err
 	}
 
 	if !found {
 		logger.Print("executable not found")
 		os.Exit(126)
-		return fmt.Errorf("executable not found")
-	}
-	if len(args) > 1 {
-		args = args[1:]
-	} else {
-		args = []string{}
+		return executable, plugins.Plugin{}, "", fmt.Errorf("executable not found")
 	}
 
-	return exec.Exec(executable, args, os.Environ())
+	return executable, plugin, version, nil
 }
 
 func anyInstalled(conf config.Config, toolVersions []toolversions.ToolVersions) bool {
@@ -842,6 +962,11 @@ func reshimCommand(logger *log.Logger, tool, version string) (err error) {
 }
 
 func shimVersionsCommand(logger *log.Logger, shimName string) error {
+	if shimName == "" {
+		logger.Printf("usage: asdf shimversions <command>")
+		return fmt.Errorf("usage: asdf shimversions <command>")
+	}
+
 	conf, err := config.LoadConfig()
 	if err != nil {
 		logger.Printf("error loading config: %s", err)
@@ -852,7 +977,7 @@ func shimVersionsCommand(logger *log.Logger, shimName string) error {
 	toolVersions, err := shims.GetToolsAndVersionsFromShimFile(shimPath)
 	for _, toolVersion := range toolVersions {
 		for _, version := range toolVersion.Versions {
-			fmt.Printf("%s %s", toolVersion.Name, version)
+			fmt.Printf("%s %s\n", toolVersion.Name, version)
 		}
 	}
 	return err
@@ -877,7 +1002,7 @@ func whichCommand(logger *log.Logger, command string) error {
 		return errors.New("must provide command")
 	}
 
-	path, _, err := shims.FindExecutable(conf, command, currentDir)
+	path, _, _, _, err := shims.FindExecutable(conf, command, currentDir)
 	if _, ok := err.(shims.UnknownCommandError); ok {
 		logger.Printf("unknown command: %s. Perhaps you have to reshim?", command)
 		return errors.New("command not found")
diff --git a/docs/guide/upgrading-from-v0-14-to-v0-15.md b/docs/guide/upgrading-from-v0-14-to-v0-15.md
index 2335147b..e7288c15 100644
--- a/docs/guide/upgrading-from-v0-14-to-v0-15.md
+++ b/docs/guide/upgrading-from-v0-14-to-v0-15.md
@@ -20,6 +20,7 @@ versions are supported. The affected commands:
 * `asdf plugin-list-all` -> `asdf plugin list all`
 * `asdf plugin-update` -> `asdf plugin update`
 * `asdf plugin-remove` -> `asdf plugin remove`
+* `asdf shim-versions` -> `asdf shimversions`
 
 ### `asdf global` and `asdf local` commands have been replaced by the `asdf set` command
 
diff --git a/internal/execenv/execenv.go b/internal/execenv/execenv.go
new file mode 100644
index 00000000..eeb8bcf2
--- /dev/null
+++ b/internal/execenv/execenv.go
@@ -0,0 +1,49 @@
+// Package execenv contains logic for generating execution environing using a plugin's
+// exec-env callback script if available.
+package execenv
+
+import (
+	"fmt"
+	"strings"
+
+	"asdf/internal/execute"
+	"asdf/internal/plugins"
+)
+
+const execEnvCallbackName = "exec-env"
+
+// Generate runs exec-env callback if available and captures the environment
+// variables it sets. It then parses them and returns them as a map.
+func Generate(plugin plugins.Plugin, callbackEnv map[string]string) (env map[string]string, err error) {
+	execEnvPath, err := plugin.CallbackPath(execEnvCallbackName)
+	if err != nil {
+		return callbackEnv, err
+	}
+
+	var stdout strings.Builder
+
+	// This is done to support the legacy behavior. exec-env is the only asdf
+	// callback that works by exporting environment variables. Because of this,
+	// executing the callback isn't enough. We actually need to source it (.) so
+	// the environment variables get set, and then run `env` so they get printed
+	// to STDOUT.
+	expression := execute.NewExpression(fmt.Sprintf(". \"%s\"; env", execEnvPath), []string{})
+	expression.Env = callbackEnv
+	expression.Stdout = &stdout
+	err = expression.Run()
+
+	return envMap(stdout.String()), err
+}
+
+func envMap(env string) map[string]string {
+	slice := map[string]string{}
+
+	for _, envVar := range strings.Split(env, "\n") {
+		varValue := strings.Split(envVar, "=")
+		if len(varValue) == 2 {
+			slice[varValue[0]] = varValue[1]
+		}
+	}
+
+	return slice
+}
diff --git a/internal/execenv/execenv_test.go b/internal/execenv/execenv_test.go
new file mode 100644
index 00000000..d705f66c
--- /dev/null
+++ b/internal/execenv/execenv_test.go
@@ -0,0 +1,43 @@
+package execenv
+
+import (
+	"testing"
+
+	"asdf/internal/config"
+	"asdf/internal/plugins"
+	"asdf/repotest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	testPluginName  = "lua"
+	testPluginName2 = "ruby"
+)
+
+func TestGenerate(t *testing.T) {
+	testDataDir := t.TempDir()
+
+	t.Run("returns map of environment variables", func(t *testing.T) {
+		conf := config.Config{DataDir: testDataDir}
+		_, err := repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName)
+		assert.Nil(t, err)
+		plugin := plugins.New(conf, testPluginName)
+		assert.Nil(t, repotest.WritePluginCallback(plugin.Dir, "exec-env", "#!/usr/bin/env bash\nexport BAZ=bar"))
+		env, err := Generate(plugin, map[string]string{"ASDF_INSTALL_VERSION": "test"})
+		assert.Nil(t, err)
+		assert.Equal(t, "bar", env["BAZ"])
+		assert.Equal(t, "test", env["ASDF_INSTALL_VERSION"])
+	})
+
+	t.Run("returns error when plugin lacks exec-env callback", func(t *testing.T) {
+		conf := config.Config{DataDir: testDataDir}
+		_, err := repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName2)
+		assert.Nil(t, err)
+		plugin := plugins.New(conf, testPluginName2)
+		env, err := Generate(plugin, map[string]string{})
+		assert.Equal(t, err.(plugins.NoCallbackError).Error(), "Plugin named ruby does not have a callback named exec-env")
+		_, found := env["FOO"]
+		assert.False(t, found)
+	})
+}
diff --git a/internal/execute/execute.go b/internal/execute/execute.go
index d6e98c4b..561c647b 100644
--- a/internal/execute/execute.go
+++ b/internal/execute/execute.go
@@ -47,7 +47,7 @@ func (c Command) Run() error {
 
 	cmd := exec.Command("bash", "-c", command)
 
-	cmd.Env = mapToSlice(c.Env)
+	cmd.Env = MapToSlice(c.Env)
 	cmd.Stdin = c.Stdin
 
 	// Capture stdout and stderr
@@ -57,6 +57,29 @@ func (c Command) Run() error {
 	return cmd.Run()
 }
 
+// MapToSlice converts an env map to env slice suitable for syscall.Exec
+func MapToSlice(env map[string]string) (slice []string) {
+	for key, value := range env {
+		slice = append(slice, fmt.Sprintf("%s=%s", key, value))
+	}
+
+	return slice
+}
+
+// SliceToMap converts an env map to env slice suitable for syscall.Exec
+func SliceToMap(env []string) map[string]string {
+	envMap := map[string]string{}
+
+	for _, envVar := range env {
+		varValue := strings.Split(envVar, "=")
+		if len(varValue) == 2 {
+			envMap[varValue[0]] = varValue[1]
+		}
+	}
+
+	return envMap
+}
+
 func formatArgString(args []string) string {
 	var newArgs []string
 	for _, str := range args {
@@ -64,11 +87,3 @@ func formatArgString(args []string) string {
 	}
 	return strings.Join(newArgs, " ")
 }
-
-func mapToSlice(env map[string]string) (slice []string) {
-	for key, value := range env {
-		slice = append(slice, fmt.Sprintf("%s=%s", key, value))
-	}
-
-	return slice
-}
diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go
index a721cb39..12ab3a97 100644
--- a/internal/plugins/plugins.go
+++ b/internal/plugins/plugins.go
@@ -154,11 +154,9 @@ func (p Plugin) Exists() error {
 
 // RunCallback invokes a callback with the given name if it exists for the plugin
 func (p Plugin) RunCallback(name string, arguments []string, environment map[string]string, stdOut io.Writer, errOut io.Writer) error {
-	callback := filepath.Join(p.Dir, "bin", name)
-
-	_, err := os.Stat(callback)
-	if errors.Is(err, os.ErrNotExist) {
-		return NoCallbackError{callback: name, plugin: p.Name}
+	callback, err := p.CallbackPath(name)
+	if err != nil {
+		return err
 	}
 
 	cmd := execute.New(fmt.Sprintf("'%s'", callback), arguments)
@@ -170,6 +168,17 @@ func (p Plugin) RunCallback(name string, arguments []string, environment map[str
 	return cmd.Run()
 }
 
+// CallbackPath returns the full file path to a callback script
+func (p Plugin) CallbackPath(name string) (string, error) {
+	path := filepath.Join(p.Dir, "bin", name)
+	_, err := os.Stat(path)
+	if errors.Is(err, os.ErrNotExist) {
+		return "", NoCallbackError{callback: name, plugin: p.Name}
+	}
+
+	return path, nil
+}
+
 // List takes config and flags for what to return and builds a list of plugins
 // representing the currently installed plugins on the system.
 func List(config config.Config, urls, refs bool) (plugins []Plugin, err error) {
diff --git a/internal/plugins/plugins_test.go b/internal/plugins/plugins_test.go
index a0d53052..98c5622e 100644
--- a/internal/plugins/plugins_test.go
+++ b/internal/plugins/plugins_test.go
@@ -183,7 +183,7 @@ func TestRemove(t *testing.T) {
 	t.Run("returns error when plugin with name does not exist", func(t *testing.T) {
 		var stdout strings.Builder
 		var stderr strings.Builder
-		err := Remove(conf, "nonexistant", &stdout, &stderr)
+		err := Remove(conf, "nonexistent", &stdout, &stderr)
 		assert.NotNil(t, err)
 		assert.ErrorContains(t, err, "No such plugin")
 	})
@@ -251,10 +251,10 @@ func TestUpdate(t *testing.T) {
 		{
 			desc:        "returns error when plugin with name does not exist",
 			givenConf:   conf,
-			givenName:   "nonexistant",
+			givenName:   "nonexistent",
 			givenRef:    "",
 			wantSomeRef: false,
-			wantErrMsg:  "no such plugin: nonexistant",
+			wantErrMsg:  "no such plugin: nonexistent",
 		},
 		{
 			desc:        "returns error when plugin repo does not exist",
@@ -352,7 +352,7 @@ func TestPluginExists(t *testing.T) {
 	})
 
 	t.Run("returns false when plugin dir does not exist", func(t *testing.T) {
-		exists, err := PluginExists(testDataDir, "non-existant")
+		exists, err := PluginExists(testDataDir, "non-existent")
 		if err != nil {
 			t.Errorf("got %v, expected nil", err)
 		}
@@ -396,9 +396,9 @@ func TestRunCallback(t *testing.T) {
 		var stdout strings.Builder
 		var stderr strings.Builder
 
-		err = plugin.RunCallback("non-existant", []string{}, emptyEnv, &stdout, &stderr)
+		err = plugin.RunCallback("non-existent", []string{}, emptyEnv, &stdout, &stderr)
 
-		assert.Equal(t, err.(NoCallbackError).Error(), "Plugin named lua does not have a callback named non-existant")
+		assert.Equal(t, err.(NoCallbackError).Error(), "Plugin named lua does not have a callback named non-existent")
 	})
 
 	t.Run("passes argument to command", func(t *testing.T) {
@@ -432,6 +432,28 @@ func TestRunCallback(t *testing.T) {
 	})
 }
 
+func TestCallbackPath(t *testing.T) {
+	testDataDir := t.TempDir()
+	conf := config.Config{DataDir: testDataDir}
+	_, err := repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName)
+	assert.Nil(t, err)
+	plugin := New(conf, testPluginName)
+
+	t.Run("returns callback path when callback exists", func(t *testing.T) {
+		path, err := plugin.CallbackPath("install")
+		assert.Nil(t, err)
+		assert.Equal(t, filepath.Base(path), "install")
+		assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(path))), plugin.Name)
+		assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(path)))), "plugins")
+	})
+
+	t.Run("returns error when callback does not exist", func(t *testing.T) {
+		path, err := plugin.CallbackPath("non-existent")
+		assert.Equal(t, err.(NoCallbackError).Error(), "Plugin named lua does not have a callback named non-existent")
+		assert.Equal(t, path, "")
+	})
+}
+
 func TestLegacyFilenames(t *testing.T) {
 	testDataDir := t.TempDir()
 	conf := config.Config{DataDir: testDataDir}
@@ -488,7 +510,7 @@ func TestParseLegacyVersionFile(t *testing.T) {
 	})
 
 	t.Run("returns error when passed file that doesn't exist", func(t *testing.T) {
-		versions, err := plugin.ParseLegacyVersionFile("non-existant-file")
+		versions, err := plugin.ParseLegacyVersionFile("non-existent-file")
 		assert.Error(t, err)
 		assert.Empty(t, versions)
 	})
diff --git a/internal/shims/shims.go b/internal/shims/shims.go
index 941986e6..9c187ed5 100644
--- a/internal/shims/shims.go
+++ b/internal/shims/shims.go
@@ -56,16 +56,16 @@ func (e NoExecutableForPluginError) Error() string {
 
 // FindExecutable takes a shim name and a current directory and returns the path
 // to the executable that the shim resolves to.
-func FindExecutable(conf config.Config, shimName, currentDirectory string) (string, bool, error) {
+func FindExecutable(conf config.Config, shimName, currentDirectory string) (string, plugins.Plugin, string, bool, error) {
 	shimPath := Path(conf, shimName)
 
 	if _, err := os.Stat(shimPath); err != nil {
-		return "", false, UnknownCommandError{shim: shimName}
+		return "", plugins.Plugin{}, "", false, UnknownCommandError{shim: shimName}
 	}
 
 	toolVersions, err := GetToolsAndVersionsFromShimFile(shimPath)
 	if err != nil {
-		return "", false, err
+		return "", plugins.Plugin{}, "", false, err
 	}
 
 	existingPluginToolVersions := make(map[plugins.Plugin]resolve.ToolVersions)
@@ -77,7 +77,7 @@ func FindExecutable(conf config.Config, shimName, currentDirectory string) (stri
 
 			versions, found, err := resolve.Version(conf, plugin, currentDirectory)
 			if err != nil {
-				return "", false, nil
+				return "", plugins.Plugin{}, "", false, nil
 			}
 
 			if found {
@@ -93,21 +93,21 @@ func FindExecutable(conf config.Config, shimName, currentDirectory string) (stri
 	}
 
 	if len(existingPluginToolVersions) == 0 {
-		return "", false, NoVersionSetError{shim: shimName}
+		return "", plugins.Plugin{}, "", false, NoVersionSetError{shim: shimName}
 	}
 
 	for plugin, toolVersions := range existingPluginToolVersions {
 		for _, version := range toolVersions.Versions {
 			if version == "system" {
 				if executablePath, found := FindSystemExecutable(conf, shimName); found {
-					return executablePath, true, nil
+					return executablePath, plugin, version, true, nil
 				}
 
 				break
 			}
 			executablePath, err := GetExecutablePath(conf, plugin, shimName, version)
 			if err == nil {
-				return executablePath, true, nil
+				return executablePath, plugin, version, true, nil
 			}
 		}
 	}
@@ -119,7 +119,7 @@ func FindExecutable(conf config.Config, shimName, currentDirectory string) (stri
 		versions = append(versions, existingPluginToolVersions[plugin].Versions...)
 	}
 
-	return "", false, NoExecutableForPluginError{shim: shimName, tools: tools, versions: versions}
+	return "", plugins.Plugin{}, "", false, NoExecutableForPluginError{shim: shimName, tools: tools, versions: versions}
 }
 
 // FindSystemExecutable returns the path to the system
@@ -127,7 +127,7 @@ func FindExecutable(conf config.Config, shimName, currentDirectory string) (stri
 func FindSystemExecutable(conf config.Config, executableName string) (string, bool) {
 	currentPath := os.Getenv("PATH")
 	defer os.Setenv("PATH", currentPath)
-	os.Setenv("PATH", paths.RemoveFromPath(currentPath, shimsDirectory(conf)))
+	os.Setenv("PATH", paths.RemoveFromPath(currentPath, Directory(conf)))
 	executablePath, err := exec.LookPath(executableName)
 	return executablePath, err == nil
 }
@@ -288,7 +288,9 @@ func Path(conf config.Config, shimName string) string {
 	return filepath.Join(conf.DataDir, shimDirName, shimName)
 }
 
-func shimsDirectory(conf config.Config) string {
+// Directory returns the path to the shims directory for the current
+// configuration.
+func Directory(conf config.Config) string {
 	return filepath.Join(conf.DataDir, shimDirName)
 }
 
@@ -298,14 +300,11 @@ func ensureShimDirExists(conf config.Config) error {
 
 // ToolExecutables returns a slice of executables for a given tool version
 func ToolExecutables(conf config.Config, plugin plugins.Plugin, versionType, version string) (executables []string, err error) {
-	dirs, err := ExecutableDirs(plugin)
+	paths, err := ExecutablePaths(conf, plugin, toolversions.Version{Type: versionType, Value: version})
 	if err != nil {
-		return executables, err
+		return []string{}, err
 	}
 
-	installPath := installs.InstallPath(conf, plugin, toolversions.Version{Type: versionType, Value: version})
-	paths := dirsToPaths(dirs, installPath)
-
 	for _, path := range paths {
 		entries, err := os.ReadDir(path)
 		if _, ok := err.(*os.PathError); err != nil && !ok {
@@ -325,6 +324,18 @@ func ToolExecutables(conf config.Config, plugin plugins.Plugin, versionType, ver
 	return executables, err
 }
 
+// ExecutablePaths returns a slice of absolute directory paths that tool
+// executables are contained in.
+func ExecutablePaths(conf config.Config, plugin plugins.Plugin, version toolversions.Version) ([]string, error) {
+	dirs, err := ExecutableDirs(plugin)
+	if err != nil {
+		return []string{}, err
+	}
+
+	installPath := installs.InstallPath(conf, plugin, version)
+	return dirsToPaths(dirs, installPath), nil
+}
+
 // ExecutableDirs returns a slice of directory names that tool executables are
 // contained in
 func ExecutableDirs(plugin plugins.Plugin) ([]string, error) {
diff --git a/internal/shims/shims_test.go b/internal/shims/shims_test.go
index be314362..ed5afbc9 100644
--- a/internal/shims/shims_test.go
+++ b/internal/shims/shims_test.go
@@ -30,16 +30,18 @@ func TestFindExecutable(t *testing.T) {
 	currentDir := t.TempDir()
 
 	t.Run("returns error when shim with name does not exist", func(t *testing.T) {
-		executable, found, err := FindExecutable(conf, "foo", currentDir)
+		executable, _, version, found, err := FindExecutable(conf, "foo", currentDir)
 		assert.Empty(t, executable)
 		assert.False(t, found)
+		assert.Empty(t, version)
 		assert.Equal(t, err.(UnknownCommandError).Error(), "unknown command: foo")
 	})
 
 	t.Run("returns error when shim is present but no version is set", func(t *testing.T) {
-		executable, found, err := FindExecutable(conf, "dummy", currentDir)
+		executable, _, version, found, err := FindExecutable(conf, "dummy", currentDir)
 		assert.Empty(t, executable)
 		assert.False(t, found)
+		assert.Empty(t, version)
 		assert.Equal(t, err.(NoVersionSetError).Error(), "no versions set for dummy")
 	})
 
@@ -48,9 +50,11 @@ func TestFindExecutable(t *testing.T) {
 		data := []byte("lua 1.1.0")
 		assert.Nil(t, os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666))
 
-		executable, found, err := FindExecutable(conf, "dummy", currentDir)
+		executable, gotPlugin, version, found, err := FindExecutable(conf, "dummy", currentDir)
 		assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(executable))), "1.1.0")
 		assert.Equal(t, filepath.Base(executable), "dummy")
+		assert.Equal(t, plugin, gotPlugin)
+		assert.Equal(t, version, "1.1.0")
 		assert.True(t, found)
 		assert.Nil(t, err)
 	})
@@ -66,7 +70,9 @@ func TestFindExecutable(t *testing.T) {
 		assert.Nil(t, os.WriteFile(toolpath, []byte("lua system\n"), 0o666))
 		assert.Nil(t, GenerateAll(conf, &stdout, &stderr))
 
-		executable, found, err := FindExecutable(conf, "ls", currentDir)
+		executable, gotPlugin, version, found, err := FindExecutable(conf, "ls", currentDir)
+		assert.Equal(t, plugin, gotPlugin)
+		assert.Equal(t, version, "system")
 		assert.True(t, found)
 		assert.Nil(t, err)
 
@@ -336,6 +342,32 @@ func TestToolExecutables(t *testing.T) {
 	})
 }
 
+func TestExecutablePaths(t *testing.T) {
+	conf, plugin := generateConfig(t)
+	installVersion(t, conf, plugin, "1.2.3")
+
+	t.Run("returns list only containing 'bin' when list-bin-paths callback missing", func(t *testing.T) {
+		executables, err := ExecutablePaths(conf, plugin, toolversions.Version{Type: "version", Value: "1.2.3"})
+		path := executables[0]
+		assert.Nil(t, err)
+		assert.Equal(t, filepath.Base(filepath.Dir(path)), "1.2.3")
+		assert.Equal(t, filepath.Base(path), "bin")
+	})
+
+	t.Run("returns list of executable paths for tool version", func(t *testing.T) {
+		data := []byte("echo 'foo bar'")
+		err := os.WriteFile(filepath.Join(plugin.Dir, "bin", "list-bin-paths"), data, 0o777)
+		assert.Nil(t, err)
+
+		executables, err := ExecutablePaths(conf, plugin, toolversions.Version{Type: "version", Value: "1.2.3"})
+		path1 := executables[0]
+		path2 := executables[1]
+		assert.Nil(t, err)
+		assert.Equal(t, filepath.Base(path1), "foo")
+		assert.Equal(t, filepath.Base(path2), "bar")
+	})
+}
+
 func TestExecutableDirs(t *testing.T) {
 	conf, plugin := generateConfig(t)
 	installVersion(t, conf, plugin, "1.2.3")
diff --git a/main_test.go b/main_test.go
index 424b4aba..d13b798d 100644
--- a/main_test.go
+++ b/main_test.go
@@ -75,9 +75,9 @@ func TestBatsTests(t *testing.T) {
 		runBatsFile(t, dir, "reshim_command.bats")
 	})
 
-	//t.Run("shim_env_command", func(t *testing.T) {
-	//  runBatsFile(t, dir, "shim_env_command.bats")
-	//})
+	t.Run("shim_env_command", func(t *testing.T) {
+		runBatsFile(t, dir, "shim_env_command.bats")
+	})
 
 	//t.Run("shim_exec", func(t *testing.T) {
 	//  runBatsFile(t, dir, "shim_exec.bats")
diff --git a/test/shim_env_command.bats b/test/shim_env_command.bats
index c97da850..a5747856 100644
--- a/test/shim_env_command.bats
+++ b/test/shim_env_command.bats
@@ -38,7 +38,8 @@ teardown() {
   echo "dummy 1.0" >"$PROJECT_DIR/.tool-versions"
   run asdf install
 
-  echo "export FOO=bar" >"$ASDF_DIR/plugins/dummy/bin/exec-env"
+  echo '#!/usr/bin/env bash
+  export FOO=bar' >"$ASDF_DIR/plugins/dummy/bin/exec-env"
   chmod +x "$ASDF_DIR/plugins/dummy/bin/exec-env"
 
   run asdf env dummy
@@ -46,13 +47,34 @@ teardown() {
   echo "$output" | grep 'FOO=bar'
 }
 
+@test "asdf env should print error when plugin version lacks the specified executable" {
+  echo "dummy 1.0" >"$PROJECT_DIR/.tool-versions"
+  run asdf install
+
+  echo '#!/usr/bin/env bash
+  export FOO=bar' >"$ASDF_DIR/plugins/dummy/bin/exec-env"
+  chmod +x "$ASDF_DIR/plugins/dummy/bin/exec-env"
+
+  echo "dummy system" >"$PROJECT_DIR/.tool-versions"
+
+  run asdf env dummy
+  [ "$status" -eq 1 ]
+  [ "$output" = "No executable dummy found for current version. Please select a different version or install dummy manually for the current version" ]
+}
+
 @test "asdf env should ignore plugin custom environment on system version" {
   echo "dummy 1.0" >"$PROJECT_DIR/.tool-versions"
   run asdf install
 
-  echo "export FOO=bar" >"$ASDF_DIR/plugins/dummy/bin/exec-env"
+  echo '#!/usr/bin/env bash
+  export FOO=bar' >"$ASDF_DIR/plugins/dummy/bin/exec-env"
   chmod +x "$ASDF_DIR/plugins/dummy/bin/exec-env"
 
+  # Create a "system" dummy executable
+  echo '#!/usr/bin/env bash
+  echo "system dummy"' >"$ASDF_BIN/dummy"
+  chmod +x "$ASDF_BIN/dummy"
+
   echo "dummy system" >"$PROJECT_DIR/.tool-versions"
 
   run asdf env dummy
@@ -63,8 +85,10 @@ teardown() {
   [ "$status" -eq 1 ]
 
   run asdf env dummy which dummy
-  [ "$output" = "$ASDF_DIR/shims/dummy" ]
+  [ "$output" = "$ASDF_BIN/dummy" ]
   [ "$status" -eq 0 ]
+  # Remove "system" dummy executable
+  rm "$ASDF_BIN/dummy"
 }
 
 @test "asdf env should set PATH correctly" {