diff --git a/src/cmd/go/internal/base/tool.go b/src/cmd/go/internal/base/tool.go
index 1d864aa2cc00683b6183a851eb09fdcd53c3a7c3..f2fc0ff7435f86e6c50e6a7eed49149a726bc889 100644
--- a/src/cmd/go/internal/base/tool.go
+++ b/src/cmd/go/internal/base/tool.go
@@ -30,7 +30,7 @@ func Tool(toolName string) string {
 // ToolPath returns the path at which we expect to find the named tool
 // (for example, "vet"), and the error (if any) from statting that path.
 func ToolPath(toolName string) (string, error) {
-	if !validToolName(toolName) {
+	if !ValidToolName(toolName) {
 		return "", fmt.Errorf("bad tool name: %q", toolName)
 	}
 	toolPath := filepath.Join(build.ToolDir, toolName) + cfg.ToolExeSuffix()
@@ -41,7 +41,7 @@ func ToolPath(toolName string) (string, error) {
 	return toolPath, err
 }
 
-func validToolName(toolName string) bool {
+func ValidToolName(toolName string) bool {
 	for _, c := range toolName {
 		switch {
 		case 'a' <= c && c <= 'z', '0' <= c && c <= '9', c == '_':
diff --git a/src/cmd/go/internal/cfg/cfg.go b/src/cmd/go/internal/cfg/cfg.go
index 49d87839f4a782444ad9924cbdc41df333ebe0d9..d583447cf6e711f1baad1d3cf1730b8a663b4fc1 100644
--- a/src/cmd/go/internal/cfg/cfg.go
+++ b/src/cmd/go/internal/cfg/cfg.go
@@ -207,6 +207,32 @@ func init() {
 	SetGOROOT(Getenv("GOROOT"), false)
 }
 
+// ForceHost forces GOOS and GOARCH to runtime.GOOS and runtime.GOARCH.
+// This is used by go tool to build tools for the go command's own
+// GOOS and GOARCH.
+func ForceHost() {
+	Goos = runtime.GOOS
+	Goarch = runtime.GOARCH
+	ExeSuffix = exeSuffix()
+	GO386 = buildcfg.DefaultGO386
+	GOAMD64 = buildcfg.DefaultGOAMD64
+	GOARM = buildcfg.DefaultGOARM
+	GOARM64 = buildcfg.DefaultGOARM64
+	GOMIPS = buildcfg.DefaultGOMIPS
+	GOMIPS64 = buildcfg.DefaultGOMIPS64
+	GOPPC64 = buildcfg.DefaultGOPPC64
+	GORISCV64 = buildcfg.DefaultGORISCV64
+	GOWASM = ""
+
+	// Recompute the build context using Goos and Goarch to
+	// set the correct value for ctx.CgoEnabled.
+	BuildContext = defaultContext()
+	// Recompute experiments: the settings determined depend on GOOS and GOARCH.
+	// This will also update the BuildContext's tool tags to include the new
+	// experiment tags.
+	computeExperiment()
+}
+
 // SetGOROOT sets GOROOT and associated variables to the given values.
 //
 // If isTestGo is true, build.ToolDir is set based on the TESTGO_GOHOSTOS and
@@ -269,6 +295,10 @@ var (
 )
 
 func init() {
+	computeExperiment()
+}
+
+func computeExperiment() {
 	Experiment, ExperimentErr = buildcfg.ParseGOEXPERIMENT(Goos, Goarch, RawGOEXPERIMENT)
 	if ExperimentErr != nil {
 		return
diff --git a/src/cmd/go/internal/tool/tool.go b/src/cmd/go/internal/tool/tool.go
index 7033eb1d9c3587452bbe842bd6db492f6c52a978..16e1a4f47f49c036e449587ec32b0c41f493db9f 100644
--- a/src/cmd/go/internal/tool/tool.go
+++ b/src/cmd/go/internal/tool/tool.go
@@ -18,7 +18,7 @@ import (
 	"os"
 	"os/exec"
 	"os/signal"
-	"path/filepath"
+	"path"
 	"slices"
 	"sort"
 	"strings"
@@ -26,6 +26,7 @@ import (
 	"cmd/go/internal/base"
 	"cmd/go/internal/cfg"
 	"cmd/go/internal/load"
+	"cmd/go/internal/modindex"
 	"cmd/go/internal/modload"
 	"cmd/go/internal/str"
 	"cmd/go/internal/work"
@@ -101,9 +102,20 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) {
 			}
 		}
 
+		// See if tool can be a builtin tool. If so, try to build and run it.
+		// buildAndRunBuiltinTool will fail if the install target of the loaded package is not
+		// the tool directory.
+		if tool := loadBuiltinTool(toolName); tool != "" {
+			// Increment a counter for the tool subcommand with the tool name.
+			counter.Inc("go/subcommand:tool-" + toolName)
+			buildAndRunBuiltinTool(ctx, toolName, tool, args[1:])
+			return
+		}
+
+		// Try to build and run mod tool.
 		tool := loadModTool(ctx, toolName)
 		if tool != "" {
-			buildAndRunModtool(ctx, tool, args[1:])
+			buildAndRunModtool(ctx, toolName, tool, args[1:])
 			return
 		}
 
@@ -116,47 +128,7 @@ func runTool(ctx context.Context, cmd *base.Command, args []string) {
 		counter.Inc("go/subcommand:tool-" + toolName)
 	}
 
-	if toolN {
-		cmd := toolPath
-		if len(args) > 1 {
-			cmd += " " + strings.Join(args[1:], " ")
-		}
-		fmt.Printf("%s\n", cmd)
-		return
-	}
-	args[0] = toolPath // in case the tool wants to re-exec itself, e.g. cmd/dist
-	toolCmd := &exec.Cmd{
-		Path:   toolPath,
-		Args:   args,
-		Stdin:  os.Stdin,
-		Stdout: os.Stdout,
-		Stderr: os.Stderr,
-	}
-	err = toolCmd.Start()
-	if err == nil {
-		c := make(chan os.Signal, 100)
-		signal.Notify(c)
-		go func() {
-			for sig := range c {
-				toolCmd.Process.Signal(sig)
-			}
-		}()
-		err = toolCmd.Wait()
-		signal.Stop(c)
-		close(c)
-	}
-	if err != nil {
-		// Only print about the exit status if the command
-		// didn't even run (not an ExitError) or it didn't exit cleanly
-		// or we're printing command lines too (-x mode).
-		// Assume if command exited cleanly (even with non-zero status)
-		// it printed any messages it wanted to print.
-		if e, ok := err.(*exec.ExitError); !ok || !e.Exited() || cfg.BuildX {
-			fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
-		}
-		base.SetExitStatus(1)
-		return
-	}
+	runBuiltTool(toolName, nil, append([]string{toolPath}, args[1:]...))
 }
 
 // listTools prints a list of the available tools in the tools directory.
@@ -262,6 +234,23 @@ func defaultExecName(importPath string) string {
 	return p.DefaultExecName()
 }
 
+func loadBuiltinTool(toolName string) string {
+	if !base.ValidToolName(toolName) {
+		return ""
+	}
+	cmdTool := path.Join("cmd", toolName)
+	if !modindex.IsStandardPackage(cfg.GOROOT, cfg.BuildContext.Compiler, cmdTool) {
+		return ""
+	}
+	// Create a fake package and check to see if it would be installed to the tool directory.
+	// If not, it's not a builtin tool.
+	p := &load.Package{PackagePublic: load.PackagePublic{Name: "main", ImportPath: cmdTool, Goroot: true}}
+	if load.InstallTargetDir(p) != load.ToTool {
+		return ""
+	}
+	return cmdTool
+}
+
 func loadModTool(ctx context.Context, name string) string {
 	modload.InitWorkfile()
 	modload.LoadModFile(ctx)
@@ -288,7 +277,42 @@ func loadModTool(ctx context.Context, name string) string {
 	return ""
 }
 
-func buildAndRunModtool(ctx context.Context, tool string, args []string) {
+func buildAndRunBuiltinTool(ctx context.Context, toolName, tool string, args []string) {
+	// Override GOOS and GOARCH for the build to build the tool using
+	// the same GOOS and GOARCH as this go command.
+	cfg.ForceHost()
+
+	// Ignore go.mod and go.work: we don't need them, and we want to be able
+	// to run the tool even if there's an issue with the module or workspace the
+	// user happens to be in.
+	modload.RootMode = modload.NoRoot
+
+	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
+		cmdline := str.StringList(a.Deps[0].BuiltTarget(), a.Args)
+		return runBuiltTool(toolName, nil, cmdline)
+	}
+
+	buildAndRunTool(ctx, tool, args, runFunc)
+}
+
+func buildAndRunModtool(ctx context.Context, toolName, tool string, args []string) {
+	runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
+		// Use the ExecCmd to run the binary, as go run does. ExecCmd allows users
+		// to provide a runner to run the binary, for example a simulator for binaries
+		// that are cross-compiled to a different platform.
+		cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].BuiltTarget(), a.Args)
+		// Use same environment go run uses to start the executable:
+		// the original environment with cfg.GOROOTbin added to the path.
+		env := slices.Clip(cfg.OrigEnv)
+		env = base.AppendPATH(env)
+
+		return runBuiltTool(toolName, env, cmdline)
+	}
+
+	buildAndRunTool(ctx, tool, args, runFunc)
+}
+
+func buildAndRunTool(ctx context.Context, tool string, args []string, runTool work.ActorFunc) {
 	work.BuildInit()
 	b := work.NewBuilder("")
 	defer func() {
@@ -304,23 +328,16 @@ func buildAndRunModtool(ctx context.Context, tool string, args []string) {
 
 	a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
 	a1.CacheExecutable = true
-	a := &work.Action{Mode: "go tool", Actor: work.ActorFunc(runBuiltTool), Args: args, Deps: []*work.Action{a1}}
+	a := &work.Action{Mode: "go tool", Actor: runTool, Args: args, Deps: []*work.Action{a1}}
 	b.Do(ctx, a)
 }
 
-func runBuiltTool(b *work.Builder, ctx context.Context, a *work.Action) error {
-	cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].BuiltTarget(), a.Args)
-
+func runBuiltTool(toolName string, env, cmdline []string) error {
 	if toolN {
 		fmt.Println(strings.Join(cmdline, " "))
 		return nil
 	}
 
-	// Use same environment go run uses to start the executable:
-	// the original environment with cfg.GOROOTbin added to the path.
-	env := slices.Clip(cfg.OrigEnv)
-	env = base.AppendPATH(env)
-
 	toolCmd := &exec.Cmd{
 		Path:   cmdline[0],
 		Args:   cmdline,
@@ -344,13 +361,17 @@ func runBuiltTool(b *work.Builder, ctx context.Context, a *work.Action) error {
 	}
 	if err != nil {
 		// Only print about the exit status if the command
-		// didn't even run (not an ExitError)
+		// didn't even run (not an ExitError) or if it didn't exit cleanly
+		// or we're printing command lines too (-x mode).
 		// Assume if command exited cleanly (even with non-zero status)
 		// it printed any messages it wanted to print.
-		if e, ok := err.(*exec.ExitError); ok {
+		e, ok := err.(*exec.ExitError)
+		if !ok || !e.Exited() || cfg.BuildX {
+			fmt.Fprintf(os.Stderr, "go tool %s: %s\n", toolName, err)
+		}
+		if ok {
 			base.SetExitStatus(e.ExitCode())
 		} else {
-			fmt.Fprintf(os.Stderr, "go tool %s: %s\n", filepath.Base(a.Deps[0].Target), err)
 			base.SetExitStatus(1)
 		}
 	}
diff --git a/src/cmd/go/internal/work/exec.go b/src/cmd/go/internal/work/exec.go
index 8d47b8d5cfe3a50f0bf65400468a080496e2a47b..6fc865421d76a9124cc68616438dcee7d452d676 100644
--- a/src/cmd/go/internal/work/exec.go
+++ b/src/cmd/go/internal/work/exec.go
@@ -2758,6 +2758,7 @@ func (b *Builder) cgo(a *Action, cgoExe, objdir string, pcCFLAGS, pcLDFLAGS, cgo
 	// consists of the original $CGO_LDFLAGS (unchecked) and all the
 	// flags put together from source code (checked).
 	cgoenv := b.cCompilerEnv()
+	cgoenv = append(cgoenv, cfgChangedEnv...)
 	var ldflagsOption []string
 	if len(cgoLDFLAGS) > 0 {
 		flags := make([]string, len(cgoLDFLAGS))
diff --git a/src/cmd/go/internal/work/gc.go b/src/cmd/go/internal/work/gc.go
index 3a173efee88ae41e5ad806d66efc08efeccffd71..70d22580a3e147a6be418cd6aa6edbab1e05648e 100644
--- a/src/cmd/go/internal/work/gc.go
+++ b/src/cmd/go/internal/work/gc.go
@@ -174,8 +174,7 @@ func (gcToolchain) gc(b *Builder, a *Action, archive string, importcfg, embedcfg
 		// code that uses those values to expect absolute paths.
 		args = append(args, fsys.Actual(f))
 	}
-
-	output, err = sh.runOut(base.Cwd(), nil, args...)
+	output, err = sh.runOut(base.Cwd(), cfgChangedEnv, args...)
 	return ofile, output, err
 }
 
@@ -397,7 +396,7 @@ func (gcToolchain) asm(b *Builder, a *Action, sfiles []string) ([]string, error)
 		ofile := a.Objdir + sfile[:len(sfile)-len(".s")] + ".o"
 		ofiles = append(ofiles, ofile)
 		args1 := append(args, "-o", ofile, fsys.Actual(mkAbs(p.Dir, sfile)))
-		if err := b.Shell(a).run(p.Dir, p.ImportPath, nil, args1...); err != nil {
+		if err := b.Shell(a).run(p.Dir, p.ImportPath, cfgChangedEnv, args1...); err != nil {
 			return nil, err
 		}
 	}
@@ -424,7 +423,7 @@ func (gcToolchain) symabis(b *Builder, a *Action, sfiles []string) (string, erro
 			return err
 		}
 
-		return sh.run(p.Dir, p.ImportPath, nil, args...)
+		return sh.run(p.Dir, p.ImportPath, cfgChangedEnv, args...)
 	}
 
 	var symabis string // Only set if we actually create the file
@@ -673,7 +672,7 @@ func (gcToolchain) ld(b *Builder, root *Action, targetPath, importcfg, mainpkg s
 		dir, targetPath = filepath.Split(targetPath)
 	}
 
-	env := []string{}
+	env := cfgChangedEnv
 	// When -trimpath is used, GOROOT is cleared
 	if cfg.BuildTrimpath {
 		env = append(env, "GOROOT=")
@@ -728,7 +727,7 @@ func (gcToolchain) ldShared(b *Builder, root *Action, toplevelactions []*Action,
 	// the output file path is recorded in the .gnu.version_d section.
 	dir, targetPath := filepath.Split(targetPath)
 
-	return b.Shell(root).run(dir, targetPath, nil, cfg.BuildToolexec, base.Tool("link"), "-o", targetPath, "-importcfg", importcfg, ldflags)
+	return b.Shell(root).run(dir, targetPath, cfgChangedEnv, cfg.BuildToolexec, base.Tool("link"), "-o", targetPath, "-importcfg", importcfg, ldflags)
 }
 
 func (gcToolchain) cc(b *Builder, a *Action, ofile, cfile string) error {
diff --git a/src/cmd/go/internal/work/init.go b/src/cmd/go/internal/work/init.go
index adee7c027413cea6431a94cb79617f781041ffb5..e4e83dc8f9853e58fefbdc2b29ff975cf068df5c 100644
--- a/src/cmd/go/internal/work/init.go
+++ b/src/cmd/go/internal/work/init.go
@@ -20,12 +20,36 @@ import (
 	"path/filepath"
 	"regexp"
 	"runtime"
+	"slices"
 	"strconv"
 	"sync"
 )
 
 var buildInitStarted = false
 
+// makeCfgChangedEnv is the environment to set to
+// override the current environment for GOOS, GOARCH, and the GOARCH-specific
+// architecture environment variable to the configuration used by
+// the go command. They may be different because go tool <tool> for builtin
+// tools need to be built using the host configuration, so the configuration
+// used will be changed from that set in the environment. It is clipped
+// so its can append to it without changing it.
+var cfgChangedEnv []string
+
+func makeCfgChangedEnv() []string {
+	var env []string
+	if cfg.Getenv("GOOS") != cfg.Goos {
+		env = append(env, "GOOS="+cfg.Goos)
+	}
+	if cfg.Getenv("GOARCH") != cfg.Goarch {
+		env = append(env, "GOARCH="+cfg.Goarch)
+	}
+	if archenv, val, changed := cfg.GetArchEnv(); changed {
+		env = append(env, archenv+"="+val)
+	}
+	return slices.Clip(env)
+}
+
 func BuildInit() {
 	if buildInitStarted {
 		base.Fatalf("go: internal error: work.BuildInit called more than once")
@@ -36,6 +60,8 @@ func BuildInit() {
 	modload.Init()
 	instrumentInit()
 	buildModeInit()
+	cfgChangedEnv = makeCfgChangedEnv()
+
 	if err := fsys.Init(); err != nil {
 		base.Fatal(err)
 	}
diff --git a/src/cmd/go/testdata/script/tool_build_as_needed.txt b/src/cmd/go/testdata/script/tool_build_as_needed.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8868ed3085b490dc62aecedfd55fc034644423ce
--- /dev/null
+++ b/src/cmd/go/testdata/script/tool_build_as_needed.txt
@@ -0,0 +1,52 @@
+[short] skip 'builds and runs go programs'
+[!symlink] skip 'uses symlinks to construct a GOROOT'
+
+env NEWGOROOT=$WORK${/}goroot
+env TOOLDIR=$GOROOT/pkg/tool/${GOOS}_${GOARCH}
+# Use ${/} in paths we'll check for in stdout below, so they contain '\' on Windows
+env NEWTOOLDIR=$NEWGOROOT${/}pkg${/}tool${/}${GOOS}_${GOARCH}
+mkdir $NEWGOROOT $NEWGOROOT/bin $NEWTOOLDIR
+[symlink] symlink $NEWGOROOT/src -> $GOROOT/src
+[symlink] symlink $NEWGOROOT/pkg/include -> $GOROOT/pkg/include
+[symlink] symlink $NEWGOROOT/bin/go -> $GOROOT/bin/go
+[symlink] symlink $NEWTOOLDIR/compile$GOEXE -> $TOOLDIR/compile$GOEXE
+[symlink] symlink $NEWTOOLDIR/cgo$GOEXE -> $TOOLDIR/cgo$GOEXE
+[symlink] symlink $NEWTOOLDIR/link$GOEXE -> $TOOLDIR/link$GOEXE
+[symlink] symlink $NEWTOOLDIR/asm$GOEXE -> $TOOLDIR/asm$GOEXE
+[symlink] symlink $NEWTOOLDIR/pack$GOEXE -> $TOOLDIR/pack$GOEXE
+env GOROOT=$NEWGOROOT
+env TOOLDIR=$NEWTOOLDIR
+
+# GOROOT without test2json tool builds and runs it as needed
+go env GOROOT
+! exists $TOOLDIR/test2json
+go tool test2json
+stdout '{"Action":"start"}'
+! exists $TOOLDIR/test2json$GOEXE
+go tool -n test2json
+! stdout $NEWTOOLDIR${/}test2json$GOEXE
+
+# GOROOT with test2json uses the test2json in the GOROOT
+go install cmd/test2json
+exists $TOOLDIR/test2json$GOEXE
+go tool test2json
+stdout '{"Action":"start"}'
+go tool -n test2json
+stdout $NEWTOOLDIR${/}test2json$GOEXE
+
+# Tool still runs properly even with wrong GOOS/GOARCH
+# Remove test2json from tooldir
+rm $TOOLDIR/test2json$GOEXE
+go tool -n test2json
+! stdout $NEWTOOLDIR${/}test2json$GOEXE
+# Set GOOS/GOARCH to different values than host GOOS/GOARCH.
+env GOOS=windows
+[GOOS:windows] env GOOS=linux
+env GOARCH=arm64
+[GOARCH:arm64] env GOARCH=amd64
+# Control case: go run shouldn't work because it respects
+# GOOS/GOARCH, and we can't execute non-native binary.
+! go run cmd/test2json -exec=''
+# But go tool should because it doesn't respect GOOS/GOARCH.
+go tool test2json
+stdout '{"Action":"start"}'