Repository: lima-vm/lima

Issue:
`limactl disk` should not depend on qemu-img for raw disks


Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml
index 0551d9b7..560bd240 100644
--- a/.github/workflows/codeql.yaml
+++ b/.github/workflows/codeql.yaml
@@ -32,7 +32,7 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d  # v3.28.10
+      uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5  # v3.28.11
       with:
         languages: ${{ matrix.language }}
         build-mode: ${{ matrix.build-mode }}
@@ -48,6 +48,6 @@ jobs:
         exit 1
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d  # v3.28.10
+      uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5  # v3.28.11
       with:
         category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 32bd197b..9fcf1e79 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -49,7 +49,7 @@ jobs:
         path: _artifacts/
   release:
     # An old release of Ubuntu is chosen for glibc compatibility
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     needs: artifacts-darwin
     timeout-minutes: 20
     # The maximum access is "read" for PRs from public forked repos
@@ -110,7 +110,7 @@ jobs:
         - - -
         Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE])
         EOF
-    - uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4  # v2.2.0
+    - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2  # v2.2.3
       if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
       with:
         subject-path: _artifacts/*
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 31ed2b5c..bbb9f09b 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -60,6 +60,6 @@ jobs:
     # Upload the results to GitHub's code scanning dashboard (optional).
     # Commenting out will disable upload of results to your repo's Code Scanning dashboard
     - name: "Upload to code-scanning"
-      uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d  # v3.28.10
+      uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5  # v3.28.11
       with:
         sarif_file: results.sarif
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 156c4d8f..992816e4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -39,7 +39,7 @@ jobs:
     - name: Verify generated files
       run: make install-tools generate check-generated
     - name: Run golangci-lint
-      uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837  # v6.5.0
+      uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea  # v6.5.1
       with:
         version: v1.64.2
         args: --verbose --timeout=10m
@@ -66,6 +66,12 @@ jobs:
     - name: Check licenses
       # the allow list corresponds to https://github.com/cncf/foundation/blob/e5db022a0009f4db52b89d9875640cf3137153fe/allowed-third-party-license-policy.md
       run: go-licenses check --include_tests  ./... --allowed_licenses=Apache-2.0,BSD-2-Clause,BSD-2-Clause-FreeBSD,BSD-3-Clause,MIT,ISC,Python-2.0,PostgreSQL,X11,Zlib
+    - name: Install ltag
+      # The GitHub repo has been moved from kunalkushwaha/ltag to containerd/ltag,
+      # but the Go module name is not changed yet: https://github.com/containerd/ltag/issues/17
+      run: go install github.com/kunalkushwaha/ltag@v0.2.5
+    - name: Check license boilerplates
+      run: ltag -t ./hack/ltag --check -v
 
   security:
     name: "Vulncheck"
@@ -129,12 +135,10 @@ jobs:
     - name: Enable WSL2
       run: |
         wsl --set-default-version 2
-        # Manually install the latest kernel from MSI
-        Invoke-WebRequest -Uri "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi" -OutFile "wsl_update_x64.msi"
-        $pwd = (pwd).Path
-        Start-Process msiexec.exe -Wait -ArgumentList "/I $pwd\wsl_update_x64.msi /quiet"
+        wsl --shutdown
         wsl --update
         wsl --status
+        wsl --version
         wsl --list --online
     - name: Install WSL2 distro
       timeout-minutes: 1
@@ -160,22 +164,15 @@ jobs:
       run: go test -v ./...
     - name: Make
       run: make
-# FIXME: Windows CI began to fail on Oct 21, 2024.
-# Something seems to have changed between win22/20241006.1 and win22/20241015.1.
-# https://github.com/lima-vm/lima/issues/2775
-#    - name: Smoke test
-#      # Make sure the path is set properly and then run limactl
-#      run: |
-#        $env:Path = 'C:\Program Files\Git\usr\bin;' + $env:Path
-#        Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $env:Path
-#        .\_output\bin\limactl.exe start template://experimental/wsl2
-#      # TODO: run the full integration tests
-#    - name: Debug
-#      if: always()
-#      run: type C:\Users\runneradmin\.lima\wsl2\ha.stdout.log
-#    - name: Debug
-#      if: always()
-#      run: type C:\Users\runneradmin\.lima\wsl2\ha.stderr.log
+    - name: Integration tests (WSL2, Windows host)
+      run: |
+        $env:Path = "$pwd\_output\bin;" + 'C:\Program Files\Git\usr\bin;' + $env:Path
+        Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $env:Path
+        $env:MSYS2_ENV_CONV_EXCL='HOME_HOST;HOME_GUEST'
+        $env:HOME_HOST=$(cygpath.exe "$env:USERPROFILE")
+        $env:HOME_GUEST="/mnt$env:HOME_HOST"
+        $env:LIMACTL_CREATE_ARGS='--vm-type=wsl2 --mount-type=wsl2 --containerd=system'
+        bash.exe -c "./hack/test-templates.sh templates/experimental/wsl2.yaml"
 
   qemu:
     name: "Integration tests (QEMU, macOS host)"
@@ -256,7 +253,8 @@ jobs:
     steps:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
       with:
-        fetch-depth: 1
+        # To avoid "failed to load YAML file \"templates/experimental/riscv64.yaml\": can't parse builtin Lima version \"3f3a6f6\": 3f3a6f6 is not in dotted-tri format"
+        fetch-depth: 0
     - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34  # v5.3.0
       with:
         go-version: 1.24.x
@@ -311,7 +309,7 @@ jobs:
     - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34  # v5.3.0
       with:
         go-version: 1.24.x
-    - uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f  # v4.2.1
+    - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf  # v4.2.2
       with:
         path: ~/.cache/lima/download
         key: ${{ runner.os }}-colima-${{ matrix.colima-version }}
diff --git a/Makefile b/Makefile
index be201c71..ef472651 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
 # Files are installed under $(DESTDIR)/$(PREFIX)
 PREFIX ?= /usr/local
 DEST := $(shell echo "$(DESTDIR)/$(PREFIX)" | sed 's:///*:/:g; s://*$$::')
diff --git a/cmd/lima b/cmd/lima
index f26202c9..f5695b69 100755
--- a/cmd/lima
+++ b/cmd/lima
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu
 
 # Environment Variables
diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go
index 391ae667..c3395aed 100644
--- a/cmd/lima-guestagent/daemon_linux.go
+++ b/cmd/lima-guestagent/daemon_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/lima-guestagent/install_systemd_linux.go b/cmd/lima-guestagent/install_systemd_linux.go
index 41ec69c1..df44791f 100644
--- a/cmd/lima-guestagent/install_systemd_linux.go
+++ b/cmd/lima-guestagent/install_systemd_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/lima-guestagent/main_linux.go b/cmd/lima-guestagent/main_linux.go
index 26884d3c..ad7c9157 100644
--- a/cmd/lima-guestagent/main_linux.go
+++ b/cmd/lima-guestagent/main_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/completion.go b/cmd/limactl/completion.go
index 54d42ddf..06c1af40 100644
--- a/cmd/limactl/completion.go
+++ b/cmd/limactl/completion.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go
index 4b5fc3e3..44263e20 100644
--- a/cmd/limactl/copy.go
+++ b/cmd/limactl/copy.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
@@ -5,9 +8,12 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
+	"path/filepath"
+	"runtime"
 	"strings"
 
 	"github.com/coreos/go-semver/semver"
+	"github.com/lima-vm/lima/pkg/ioutilx"
 	"github.com/lima-vm/lima/pkg/sshutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/sirupsen/logrus"
@@ -77,6 +83,16 @@ func copyAction(cmd *cobra.Command, args []string) error {
 	// this assumes that ssh and scp come from the same place, but scp has no -V
 	legacySSH := sshutil.DetectOpenSSHVersion("ssh").LessThan(*semver.New("8.0.0"))
 	for _, arg := range args {
+		if runtime.GOOS == "windows" {
+			if filepath.IsAbs(arg) {
+				arg, err = ioutilx.WindowsSubsystemPath(arg)
+				if err != nil {
+					return err
+				}
+			} else {
+				arg = filepath.ToSlash(arg)
+			}
+		}
 		path := strings.Split(arg, ":")
 		switch len(path) {
 		case 1:
diff --git a/cmd/limactl/debug.go b/cmd/limactl/debug.go
index cfbd4d52..ad91220c 100644
--- a/cmd/limactl/debug.go
+++ b/cmd/limactl/debug.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/delete.go b/cmd/limactl/delete.go
index e69e9563..ee7dba36 100644
--- a/cmd/limactl/delete.go
+++ b/cmd/limactl/delete.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/disk.go b/cmd/limactl/disk.go
index dafd601a..a8299a79 100644
--- a/cmd/limactl/disk.go
+++ b/cmd/limactl/disk.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
@@ -9,6 +12,7 @@ import (
 	"text/tabwriter"
 
 	"github.com/docker/go-units"
+	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/qemu"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/sirupsen/logrus"
@@ -101,7 +105,13 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	if err := qemu.CreateDataDisk(diskDir, format, int(diskSize)); err != nil {
+	// qemu may not be available, use it only if needed.
+	if format == "raw" {
+		err = nativeimgutil.CreateRawDataDisk(diskDir, int(diskSize))
+	} else {
+		err = qemu.CreateDataDisk(diskDir, format, int(diskSize))
+	}
+	if err != nil {
 		rerr := os.RemoveAll(diskDir)
 		if rerr != nil {
 			err = errors.Join(err, fmt.Errorf("failed to remove a directory %q: %w", diskDir, rerr))
@@ -390,9 +400,17 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
 			}
 		}
 	}
-	if err := qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize)); err != nil {
+
+	// qemu may not be available, use it only if needed.
+	if disk.Format == "raw" {
+		err = nativeimgutil.ResizeRawDataDisk(disk.Dir, int(diskSize))
+	} else {
+		err = qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize))
+	}
+	if err != nil {
 		return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
 	}
+
 	logrus.Infof("Resized disk %q (%q)", diskName, disk.Dir)
 	return nil
 }
diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index 47e37086..e9d33311 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go
index fc509548..e5b9a584 100644
--- a/cmd/limactl/editflags/editflags.go
+++ b/cmd/limactl/editflags/editflags.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package editflags
 
 import (
diff --git a/cmd/limactl/factory-reset.go b/cmd/limactl/factory-reset.go
index c13a450f..a5303098 100644
--- a/cmd/limactl/factory-reset.go
+++ b/cmd/limactl/factory-reset.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/gendoc.go b/cmd/limactl/gendoc.go
index f1d3935d..7a8b7c95 100644
--- a/cmd/limactl/gendoc.go
+++ b/cmd/limactl/gendoc.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/genschema.go b/cmd/limactl/genschema.go
index 1ad574bb..8de619fd 100644
--- a/cmd/limactl/genschema.go
+++ b/cmd/limactl/genschema.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/hostagent.go b/cmd/limactl/hostagent.go
index 45de7441..75a838cb 100644
--- a/cmd/limactl/hostagent.go
+++ b/cmd/limactl/hostagent.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/info.go b/cmd/limactl/info.go
index 8dc3a788..8aafa714 100644
--- a/cmd/limactl/info.go
+++ b/cmd/limactl/info.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/list.go b/cmd/limactl/list.go
index f7d5c928..f10064d4 100644
--- a/cmd/limactl/list.go
+++ b/cmd/limactl/list.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go
index 259f622a..268fad0d 100644
--- a/cmd/limactl/main.go
+++ b/cmd/limactl/main.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/protect.go b/cmd/limactl/protect.go
index d094eec5..36140609 100644
--- a/cmd/limactl/protect.go
+++ b/cmd/limactl/protect.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/prune.go b/cmd/limactl/prune.go
index 5d377ec3..7dc38a5c 100644
--- a/cmd/limactl/prune.go
+++ b/cmd/limactl/prune.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go
index c4611a0b..36454010 100644
--- a/cmd/limactl/shell.go
+++ b/cmd/limactl/shell.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
@@ -5,13 +8,19 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
+	"path"
+	"runtime"
 	"strconv"
 	"strings"
 
 	"al.essio.dev/pkg/shellescape"
 	"github.com/coreos/go-semver/semver"
+	"github.com/lima-vm/lima/pkg/instance"
+	"github.com/lima-vm/lima/pkg/ioutilx"
+	networks "github.com/lima-vm/lima/pkg/networks/reconcile"
 	"github.com/lima-vm/lima/pkg/sshutil"
 	"github.com/lima-vm/lima/pkg/store"
+	"github.com/lima-vm/sshocker/pkg/ssh"
 	"github.com/mattn/go-isatty"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -43,6 +52,7 @@ func newShellCommand() *cobra.Command {
 
 	shellCmd.Flags().String("shell", "", "shell interpreter, e.g. /bin/bash")
 	shellCmd.Flags().String("workdir", "", "working directory")
+	shellCmd.Flags().Bool("reconnect", false, "reconnect to the SSH session")
 	return shellCmd
 }
 
@@ -72,7 +82,49 @@ func shellAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 	if inst.Status == store.StatusStopped {
-		return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
+		startNow, err := askWhetherToStart()
+		if err != nil {
+			return err
+		}
+
+		if !startNow {
+			return nil
+		}
+
+		ctx := cmd.Context()
+
+		err = networks.Reconcile(ctx, inst.Name)
+		if err != nil {
+			return err
+		}
+
+		err = instance.Start(ctx, inst, "", false)
+		if err != nil {
+			return err
+		}
+
+		inst, err = store.Inspect(instName)
+		if err != nil {
+			return err
+		}
+	}
+
+	restart, err := cmd.Flags().GetBool("reconnect")
+	if err != nil {
+		return err
+	}
+	if restart {
+		logrus.Infof("Exiting ssh session for the instance %q", instName)
+
+		sshConfig := &ssh.SSHConfig{
+			ConfigFile:     inst.SSHConfigFile,
+			Persist:        false,
+			AdditionalArgs: []string{},
+		}
+
+		if err := ssh.ExitMaster(inst.Hostname, inst.SSHLocalPort, sshConfig); err != nil {
+			return err
+		}
 	}
 
 	// When workDir is explicitly set, the shell MUST have workDir as the cwd, or exit with an error.
@@ -89,6 +141,9 @@ func shellAction(cmd *cobra.Command, args []string) error {
 		// FIXME: check whether y.Mounts contains the home, not just len > 0
 	} else if len(inst.Config.Mounts) > 0 {
 		hostCurrentDir, err := os.Getwd()
+		if err == nil && runtime.GOOS == "windows" {
+			hostCurrentDir, err = mountDirFromWindowsDir(hostCurrentDir)
+		}
 		if err == nil {
 			changeDirCmd = fmt.Sprintf("cd %s", shellescape.Quote(hostCurrentDir))
 		} else {
@@ -96,6 +151,9 @@ func shellAction(cmd *cobra.Command, args []string) error {
 			logrus.WithError(err).Warn("failed to get the current directory")
 		}
 		hostHomeDir, err := os.UserHomeDir()
+		if err == nil && runtime.GOOS == "windows" {
+			hostHomeDir, err = mountDirFromWindowsDir(hostHomeDir)
+		}
 		if err == nil {
 			changeDirCmd = fmt.Sprintf("%s || cd %s", changeDirCmd, shellescape.Quote(hostHomeDir))
 		} else {
@@ -186,6 +244,14 @@ func shellAction(cmd *cobra.Command, args []string) error {
 	return sshCmd.Run()
 }
 
+func mountDirFromWindowsDir(dir string) (string, error) {
+	dir, err := ioutilx.WindowsSubsystemPath(dir)
+	if err == nil && !strings.HasPrefix(dir, "/mnt/") {
+		dir = path.Join("/mnt", dir)
+	}
+	return dir, err
+}
+
 func shellBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
 	return bashCompleteInstanceNames(cmd)
 }
diff --git a/cmd/limactl/show-ssh.go b/cmd/limactl/show-ssh.go
index f3867b68..b469c297 100644
--- a/cmd/limactl/show-ssh.go
+++ b/cmd/limactl/show-ssh.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/snapshot.go b/cmd/limactl/snapshot.go
index f6736837..76e55df5 100644
--- a/cmd/limactl/snapshot.go
+++ b/cmd/limactl/snapshot.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/start-at-login.go b/cmd/limactl/start-at-login.go
index 312d1a46..39db8bbb 100644
--- a/cmd/limactl/start-at-login.go
+++ b/cmd/limactl/start-at-login.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index b0f08d31..d6ea4251 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
@@ -202,7 +205,9 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
 			return nil, err
 		}
 	}
-
+	if err := tmpl.Embed(cmd.Context(), true, true); err != nil {
+		return nil, err
+	}
 	yqExprs, err := editflags.YQExpressions(flags, true)
 	if err != nil {
 		return nil, err
diff --git a/cmd/limactl/stop.go b/cmd/limactl/stop.go
index b4bff7d6..554547fe 100644
--- a/cmd/limactl/stop.go
+++ b/cmd/limactl/stop.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/sudoers.go b/cmd/limactl/sudoers.go
index eb8fd1f4..4d6dc7b3 100644
--- a/cmd/limactl/sudoers.go
+++ b/cmd/limactl/sudoers.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/template.go b/cmd/limactl/template.go
index 5cfc0a29..1444ac2f 100644
--- a/cmd/limactl/template.go
+++ b/cmd/limactl/template.go
@@ -1,6 +1,10 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
+	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -8,6 +12,7 @@ import (
 	"github.com/lima-vm/lima/pkg/limatmpl"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/store/dirnames"
+	"github.com/lima-vm/lima/pkg/yqutil"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
@@ -23,10 +28,14 @@ func newTemplateCommand() *cobra.Command {
 		// The template command is still hidden because the subcommands and options are still under development
 		// and subject to change at any time.
 		Hidden: true,
+		PreRun: func(*cobra.Command, []string) {
+			logrus.Warn("`limactl template` is experimental")
+		},
 	}
 	templateCommand.AddCommand(
 		newTemplateCopyCommand(),
 		newTemplateValidateCommand(),
+		newTemplateYQCommand(),
 	)
 	return templateCommand
 }
@@ -43,32 +52,95 @@ var templateCopyExample = `  Template locators are local files, file://, https:/
   # Copy default template to STDOUT
   limactl template copy template://default -
 
-  # Copy template from web location to local file
-  limactl template copy https://example.com/lima.yaml mighty-machine.yaml
+  # Copy template from web location to local file and embed all external references
+  # (this does not embed template:// references)
+  limactl template copy --embed https://example.com/lima.yaml mighty-machine.yaml
 `
 
 func newTemplateCopyCommand() *cobra.Command {
 	templateCopyCommand := &cobra.Command{
-		Use:     "copy TEMPLATE DEST",
+		Use:     "copy [OPTIONS] TEMPLATE DEST",
 		Short:   "Copy template",
 		Long:    "Copy a template via locator to a local file",
 		Example: templateCopyExample,
 		Args:    WrapArgsError(cobra.ExactArgs(2)),
 		RunE:    templateCopyAction,
 	}
+	templateCopyCommand.Flags().Bool("embed", false, "embed external dependencies into template")
+	templateCopyCommand.Flags().Bool("embed-all", false, "embed all dependencies into template")
+	templateCopyCommand.Flags().Bool("fill", false, "fill defaults")
+	templateCopyCommand.Flags().Bool("verbatim", false, "don't make locators absolute")
 	return templateCopyCommand
 }
 
+func fillDefaults(tmpl *limatmpl.Template) error {
+	limaDir, err := dirnames.LimaDir()
+	if err != nil {
+		return err
+	}
+	// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
+	// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
+	filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
+	tmpl.Config, err = limayaml.Load(tmpl.Bytes, filePath)
+	if err == nil {
+		tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)
+	}
+	return err
+}
+
 func templateCopyAction(cmd *cobra.Command, args []string) error {
-	tmpl, err := limatmpl.Read(cmd.Context(), "", args[0])
+	source := args[0]
+	target := args[1]
+	embed, err := cmd.Flags().GetBool("embed")
+	if err != nil {
+		return err
+	}
+	embedAll, err := cmd.Flags().GetBool("embed-all")
+	if err != nil {
+		return err
+	}
+	fill, err := cmd.Flags().GetBool("fill")
+	if err != nil {
+		return err
+	}
+	verbatim, err := cmd.Flags().GetBool("verbatim")
+	if err != nil {
+		return err
+	}
+	if fill {
+		embedAll = true
+	}
+	if embedAll {
+		embed = true
+	}
+	if embed && verbatim {
+		return errors.New("--verbatim cannot be used with any of --embed, --embed-all, or --fill")
+	}
+	tmpl, err := limatmpl.Read(cmd.Context(), "", source)
 	if err != nil {
 		return err
 	}
 	if len(tmpl.Bytes) == 0 {
-		return fmt.Errorf("don't know how to interpret %q as a template locator", args[0])
+		return fmt.Errorf("don't know how to interpret %q as a template locator", source)
+	}
+	if !verbatim {
+		if embed {
+			// Embed default base.yaml only when fill is true.
+			if err := tmpl.Embed(cmd.Context(), embedAll, fill); err != nil {
+				return err
+			}
+		} else {
+			if err := tmpl.UseAbsLocators(); err != nil {
+				return err
+			}
+		}
+	}
+	if fill {
+		if err := fillDefaults(tmpl); err != nil {
+			return err
+		}
 	}
 	writer := cmd.OutOrStdout()
-	target := args[1]
 	if target != "-" {
 		file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
 		if err != nil {
@@ -81,6 +153,51 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
 	return err
 }
 
+const templateYQHelp = `Use the builtin YQ evaluator to extract information from a template.
+External references are embedded and default values are filled in
+before the YQ expression is evaluated.
+
+Example:
+  limactl template yq template://default '.images[].location'
+
+The example command is equivalent to using an external yq command like this:
+  limactl template copy --fill template://default - | yq '.images[].location'
+`
+
+func newTemplateYQCommand() *cobra.Command {
+	templateYQCommand := &cobra.Command{
+		Use:   "yq TEMPLATE EXPR",
+		Short: "Query template expressions",
+		Long:  templateYQHelp,
+		Args:  WrapArgsError(cobra.ExactArgs(2)),
+		RunE:  templateYQAction,
+	}
+	return templateYQCommand
+}
+
+func templateYQAction(cmd *cobra.Command, args []string) error {
+	locator := args[0]
+	expr := args[1]
+	tmpl, err := limatmpl.Read(cmd.Context(), "", locator)
+	if err != nil {
+		return err
+	}
+	if len(tmpl.Bytes) == 0 {
+		return fmt.Errorf("don't know how to interpret %q as a template locator", locator)
+	}
+	if err := tmpl.Embed(cmd.Context(), true, true); err != nil {
+		return err
+	}
+	if err := fillDefaults(tmpl); err != nil {
+		return err
+	}
+	out, err := yqutil.EvaluateExpressionPlain(expr, string(tmpl.Bytes))
+	if err == nil {
+		_, err = fmt.Fprint(cmd.OutOrStdout(), out)
+	}
+	return err
+}
+
 func newTemplateValidateCommand() *cobra.Command {
 	templateValidateCommand := &cobra.Command{
 		Use:   "validate TEMPLATE [TEMPLATE, ...]",
@@ -113,10 +230,14 @@ func templateValidateAction(cmd *cobra.Command, args []string) error {
 		if tmpl.Name == "" {
 			return fmt.Errorf("can't determine instance name from template locator %q", arg)
 		}
+		// Embed default base.yaml only when fill is true.
+		if err := tmpl.Embed(cmd.Context(), true, fill); err != nil {
+			return err
+		}
 		// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
 		// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
-		instDir := filepath.Join(limaDir, tmpl.Name)
-		y, err := limayaml.Load(tmpl.Bytes, instDir)
+		filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
+		y, err := limayaml.Load(tmpl.Bytes, filePath)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/limactl/tunnel.go b/cmd/limactl/tunnel.go
index f9d615c7..9e690281 100644
--- a/cmd/limactl/tunnel.go
+++ b/cmd/limactl/tunnel.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/unprotect.go b/cmd/limactl/unprotect.go
index 2e95ce33..5cb9b303 100644
--- a/cmd/limactl/unprotect.go
+++ b/cmd/limactl/unprotect.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/cmd/limactl/usernet.go b/cmd/limactl/usernet.go
index 5ad356f4..dac355c7 100644
--- a/cmd/limactl/usernet.go
+++ b/cmd/limactl/usernet.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package main
 
 import (
diff --git a/go.mod b/go.mod
index 3bce68c6..7efcb3fa 100644
--- a/go.mod
+++ b/go.mod
@@ -4,21 +4,21 @@ module github.com/lima-vm/lima
 go 1.23.0
 
 require (
-	al.essio.dev/pkg/shellescape v1.5.1
+	al.essio.dev/pkg/shellescape v1.6.0
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/Code-Hex/vz/v3 v3.6.0 // gomodjail:unconfined
 	github.com/Microsoft/go-winio v0.6.2 // gomodjail:unconfined
 	github.com/apparentlymart/go-cidr v1.1.0
 	github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e
-	github.com/cheggaaa/pb/v3 v3.1.6 // gomodjail:unconfined
-	github.com/containerd/containerd v1.7.25
+	github.com/cheggaaa/pb/v3 v3.1.7 // gomodjail:unconfined
+	github.com/containerd/containerd v1.7.26
 	github.com/containerd/continuity v0.4.5
-	github.com/containers/gvisor-tap-vsock v0.8.3 // gomodjail:unconfined
+	github.com/containers/gvisor-tap-vsock v0.8.4 // gomodjail:unconfined
 	github.com/coreos/go-semver v0.3.1
 	github.com/cpuguy83/go-md2man/v2 v2.0.6
 	github.com/cyphar/filepath-securejoin v0.4.1
 	github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7
-	github.com/diskfs/go-diskfs v1.5.1 // gomodjail:unconfined
+	github.com/diskfs/go-diskfs v1.5.2 // gomodjail:unconfined
 	github.com/docker/go-units v0.5.0
 	github.com/elastic/go-libaudit/v2 v2.6.1
 	github.com/foxcpp/go-mockdns v1.1.0
@@ -27,7 +27,7 @@ require (
 	github.com/google/yamlfmt v0.16.0
 	github.com/invopop/jsonschema v0.13.0
 	github.com/lima-vm/go-qcow2reader v0.6.0
-	github.com/lima-vm/sshocker v0.3.5 // gomodjail:unconfined
+	github.com/lima-vm/sshocker v0.3.6 // gomodjail:unconfined
 	github.com/mattn/go-isatty v0.0.20
 	github.com/mattn/go-shellwords v1.0.12
 	github.com/mdlayher/vsock v1.2.1 // gomodjail:unconfined
@@ -43,17 +43,17 @@ require (
 	github.com/spf13/cobra v1.9.1 // gomodjail:unconfined
 	github.com/spf13/pflag v1.0.6
 	github.com/wk8/go-ordered-map/v2 v2.1.8
-	golang.org/x/net v0.35.0
-	golang.org/x/sync v0.11.0
-	golang.org/x/sys v0.30.0 // gomodjail:unconfined
-	golang.org/x/text v0.22.0
-	google.golang.org/grpc v1.70.0
+	golang.org/x/net v0.37.0
+	golang.org/x/sync v0.12.0
+	golang.org/x/sys v0.31.0 // gomodjail:unconfined
+	golang.org/x/text v0.23.0
+	google.golang.org/grpc v1.71.0
 	google.golang.org/protobuf v1.36.5 // gomodjail:unconfined
 	gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
 	gotest.tools/v3 v3.5.2
-	k8s.io/api v0.32.2
-	k8s.io/apimachinery v0.32.2
-	k8s.io/client-go v0.32.2
+	k8s.io/api v0.32.3
+	k8s.io/apimachinery v0.32.3
+	k8s.io/client-go v0.32.3
 )
 
 require (
@@ -112,20 +112,20 @@ require (
 	github.com/pierrec/lz4/v4 v4.1.22 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pkg/sftp v1.13.7 // indirect
-	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
 	// gomodjail:unconfined
 	github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/yuin/gopher-lua v1.1.1 // indirect
-	golang.org/x/crypto v0.35.0 // indirect
+	golang.org/x/crypto v0.36.0 // indirect
 	golang.org/x/mod v0.22.0 // indirect
-	golang.org/x/oauth2 v0.24.0 // indirect
-	golang.org/x/term v0.29.0 // indirect
+	golang.org/x/oauth2 v0.25.0 // indirect
+	golang.org/x/term v0.30.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
 	golang.org/x/tools v0.28.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
diff --git a/go.sum b/go.sum
index 2dea7589..b7cdd229 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
-al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
+al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
+al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
 github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
 github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
 github.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1pFAqvnP87Zh0Nw=
@@ -32,18 +32,18 @@ github.com/braydonk/yaml v0.9.0 h1:ewGMrVmEVpsm3VwXQDR388sLg5+aQ8Yihp6/hc4m+h4=
 github.com/braydonk/yaml v0.9.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4=
 github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
-github.com/cheggaaa/pb/v3 v3.1.6 h1:h0x+vd7EiUohAJ29DJtJy+SNAc55t/elW3jCD086EXk=
-github.com/cheggaaa/pb/v3 v3.1.6/go.mod h1:urxmfVtaxT+9aWk92DbsvXFZtNSWQSO5TRAp+MJ3l1s=
-github.com/containerd/containerd v1.7.25 h1:khEQOAXOEJalRO228yzVsuASLH42vT7DIo9Ss+9SMFQ=
-github.com/containerd/containerd v1.7.25/go.mod h1:tWfHzVI0azhw4CT2vaIjsb2CoV4LJ9PrMPaULAr21Ok=
+github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
+github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=
+github.com/containerd/containerd v1.7.26 h1:3cs8K2RHlMQaPifLqgRyI4VBkoldNdEw62cb7qQga7k=
+github.com/containerd/containerd v1.7.26/go.mod h1:m4JU0E+h0ebbo9yXD7Hyt+sWnc8tChm7MudCjj4jRvQ=
 github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
 github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
-github.com/containers/gvisor-tap-vsock v0.8.3 h1:Am3VdjXTn8Mn+dNhgkiRcCFOTSM8u9aWKLW3KTHOGjk=
-github.com/containers/gvisor-tap-vsock v0.8.3/go.mod h1:46MvrqNuRNbjV4ZsZ3mHVJjR2Eh+fpyRh72EvWWFFjU=
+github.com/containers/gvisor-tap-vsock v0.8.4 h1:z7MqcldnXYGaU6uTaKVl7RFxTmbhNsd2UL0CyM3fdBs=
+github.com/containers/gvisor-tap-vsock v0.8.4/go.mod h1:Guh8d/SiuJ9jlnuIyUjcKkFKQ2qpLhNKPGD1jMoIt2Q=
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
@@ -62,8 +62,8 @@ github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 h1:3OVJAbR131
 github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7/go.mod h1:K4+o74YGNjOb9N6yyG+LPj1NjHtk+Qz0IYQPvirbaLs=
 github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
-github.com/diskfs/go-diskfs v1.5.1 h1:Mnzgx3ERI+lzGzDAV+ejFLj/Uz9Cs/7wDkaAXwH1RMk=
-github.com/diskfs/go-diskfs v1.5.1/go.mod h1:bRFumZeGFCO8C2KNswrQeuj2m1WCVr4Ms5IjWMczMDk=
+github.com/diskfs/go-diskfs v1.5.2 h1:Aj+f4sYlu3seXJe5KwyOWlol0eRBG9EKGYYYm37DO9s=
+github.com/diskfs/go-diskfs v1.5.2/go.mod h1:bRFumZeGFCO8C2KNswrQeuj2m1WCVr4Ms5IjWMczMDk=
 github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
 github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
@@ -163,8 +163,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lima-vm/go-qcow2reader v0.6.0 h1:dNstUGQxEUPbmiiVnu/cek2x7scrHe2VJy5JseLLflo=
 github.com/lima-vm/go-qcow2reader v0.6.0/go.mod h1:ay45SlGOzU+2Vc21g5/lmQgPn7Hmf0JpPhm8cuOK1FI=
-github.com/lima-vm/sshocker v0.3.5 h1:1QNs6hVT4ZzZpifaWwe8WrDfi3zmdpHS6rA61Ao6/k4=
-github.com/lima-vm/sshocker v0.3.5/go.mod h1:FOw9S/jgmasbcTUhreVS3fAZB9LEo7CZhYZ0+y34bEQ=
+github.com/lima-vm/sshocker v0.3.6 h1:mJvUxUY/uA942GT2JkEbUuWxK4Pj/pBxO9iUB1c3TE8=
+github.com/lima-vm/sshocker v0.3.6/go.mod h1:UFcM3Up8YOU6vGWpdKwG5VUflng0PEC/l3SID+xYX1A=
 github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y=
 github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM=
 github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
@@ -230,8 +230,9 @@ github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6kt
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
 github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@@ -273,16 +274,18 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
 github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
-go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
-go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
-go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
-go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
-go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
-go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
-go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -291,8 +294,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -316,10 +319,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
-golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
-golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
+golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -329,8 +332,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -352,8 +355,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -362,8 +365,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -372,8 +375,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
 golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -392,10 +395,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
 google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -416,12 +419,12 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
-k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
-k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
-k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
-k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
-k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
+k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
+k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
+k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
+k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
+k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
diff --git a/hack/brew-install-version.sh b/hack/brew-install-version.sh
index fbd6028e..eaca43a7 100755
--- a/hack/brew-install-version.sh
+++ b/hack/brew-install-version.sh
@@ -1,4 +1,8 @@
 #!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # This script only works for formulas in the homebrew-core.
 # It assumes the homebrew-core has been checked out into ./homebrew-core.
 # It only needs commit messages, so the checkout can be filtered with tree:0.
diff --git a/hack/cache-common-inc.sh b/hack/cache-common-inc.sh
index d3ce456a..bb1cb6b2 100755
--- a/hack/cache-common-inc.sh
+++ b/hack/cache-common-inc.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # print the error message and exit with status 1
 function error_exit() {
 	echo "Error: $*" >&2
@@ -15,17 +18,18 @@ function error_exit() {
 # ```
 function download_template_if_needed() {
 	local template="$1"
-	case "${template}" in
-	https://*)
-		tmp_yaml=$(mktemp -d)/template.yaml
-		curl -sSLf "${template}" >"${tmp_yaml}" || return
-		echo "${tmp_yaml}"
-		;;
-	*)
-		test -f "${template}" || return
-		echo "${template}"
-		;;
-	esac
+	tmp_yaml="$(mktemp -d)/template.yaml"
+	# The upgrade test doesn't have limactl installed first. The old version wouldn't support `limactl tmpl` anyways.
+	if command -v limactl >/dev/null; then
+		limactl tmpl copy --embed-all "${template}" "${tmp_yaml}" || return
+	else
+		if [[ $template == https://* ]]; then
+			curl -sSLf "${template}" >"${tmp_yaml}" || return
+		else
+			cp "${template}" "${tmp_yaml}"
+		fi
+	fi
+	echo "${tmp_yaml}"
 }
 
 # e.g.
diff --git a/hack/calculate-cache.sh b/hack/calculate-cache.sh
index a87af6bc..26a9a0bf 100755
--- a/hack/calculate-cache.sh
+++ b/hack/calculate-cache.sh
@@ -1,4 +1,8 @@
 #!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # This script calculates the expected content size, actual cached size, and cache-keys used in caching method before and after
 # implementation in https://github.com/lima-vm/lima/pull/2508
 #
diff --git a/hack/common.inc.sh b/hack/common.inc.sh
index 1e90ff8e..95e429fc 100644
--- a/hack/common.inc.sh
+++ b/hack/common.inc.sh
@@ -23,7 +23,7 @@ if [[ ${BASH_VERSINFO:-0} -lt 4 ]]; then
 	exit 1
 fi
 
-: "${LIMA_HOME:=$HOME/.lima}"
+: "${LIMA_HOME:=${HOME_HOST:-$HOME}/.lima}"
 _IPERF3=iperf3
 # iperf3-darwin does some magic on macOS to avoid "No route on host" on macOS 15
 # https://github.com/lima-vm/socket_vmnet/issues/85
diff --git a/hack/debug-cache.sh b/hack/debug-cache.sh
index db04cf1b..a9510351 100755
--- a/hack/debug-cache.sh
+++ b/hack/debug-cache.sh
@@ -1,4 +1,8 @@
 #!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 cache_dir="${HOME}/Library/Caches"
 if [ "$(uname -s)" != "Darwin" ]; then
diff --git a/hack/fix-qemu-signature.sh b/hack/fix-qemu-signature.sh
index b0203484..f571c329 100755
--- a/hack/fix-qemu-signature.sh
+++ b/hack/fix-qemu-signature.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # This script fixes the signature of QEMU binary with the "com.apple.security.hypervisor" entitlement.
 #
 # A workaround for "QEMU (homebrew) is broken on Intel: `[hostagent] Driver stopped due to error: "signal: abort trap"` ..."
diff --git a/hack/inject-cmdline-to-template.sh b/hack/inject-cmdline-to-template.sh
index 13dec85c..08b92899 100755
--- a/hack/inject-cmdline-to-template.sh
+++ b/hack/inject-cmdline-to-template.sh
@@ -1,4 +1,8 @@
 #!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 #
 # This script does
 # 1. detect arch from template if not provided
diff --git a/hack/ltag/bash.txt b/hack/ltag/bash.txt
new file mode 100644
index 00000000..10b096c9
--- /dev/null
+++ b/hack/ltag/bash.txt
@@ -0,0 +1,2 @@
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
diff --git a/hack/ltag/dockerfile.txt b/hack/ltag/dockerfile.txt
new file mode 100644
index 00000000..10b096c9
--- /dev/null
+++ b/hack/ltag/dockerfile.txt
@@ -0,0 +1,2 @@
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
diff --git a/hack/ltag/go.txt b/hack/ltag/go.txt
new file mode 100644
index 00000000..661d3adb
--- /dev/null
+++ b/hack/ltag/go.txt
@@ -0,0 +1,3 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
diff --git a/hack/ltag/makefile.txt b/hack/ltag/makefile.txt
new file mode 100644
index 00000000..10b096c9
--- /dev/null
+++ b/hack/ltag/makefile.txt
@@ -0,0 +1,2 @@
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
diff --git a/hack/oss-fuzz-build.sh b/hack/oss-fuzz-build.sh
index 0b1e757b..1334554d 100755
--- a/hack/oss-fuzz-build.sh
+++ b/hack/oss-fuzz-build.sh
@@ -1,4 +1,8 @@
 #!/bin/bash -eu
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # This script is used by OSS-Fuzz to build and run Limas fuzz tests continuously.
 # Limas OSS-Fuzz integration can be found here: https://github.com/google/oss-fuzz/tree/master/projects/lima
 # Modify https://github.com/google/oss-fuzz/blob/master/projects/lima/project.yaml for access management to Limas OSS-Fuzz crashes.
diff --git a/hack/test-colima.sh b/hack/test-colima.sh
index d543f383..893cb330 100755
--- a/hack/test-colima.sh
+++ b/hack/test-colima.sh
@@ -1,4 +1,8 @@
 #!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux -o pipefail
 
 colima start
diff --git a/hack/test-mount-home.sh b/hack/test-mount-home.sh
index dd8e1afa..dd551627 100755
--- a/hack/test-mount-home.sh
+++ b/hack/test-mount-home.sh
@@ -1,4 +1,8 @@
 #!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -11,14 +15,15 @@ if [ "$#" -ne 1 ]; then
 fi
 
 NAME="$1"
-hometmp="$HOME/lima-test-tmp"
+hometmp="${HOME_HOST:-$HOME}/lima-test-tmp"
+hometmpguest="${HOME_GUEST:-$HOME}/lima-test-tmp"
 INFO "Testing home access (\"$hometmp\")"
 rm -rf "$hometmp"
 mkdir -p "$hometmp"
 defer "rm -rf \"$hometmp\""
 echo "random-content-${RANDOM}" >"$hometmp/random"
 expected="$(cat "$hometmp/random")"
-got="$(limactl shell "$NAME" cat "$hometmp/random")"
+got="$(limactl shell "$NAME" cat "$hometmpguest/random")"
 INFO "$hometmp/random: expected=${expected}, got=${got}"
 if [ "$got" != "$expected" ]; then
 	ERROR "Home directory is not shared?"
diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl
index 3a41ce1e..b812060c 100755
--- a/hack/test-port-forwarding.pl
+++ b/hack/test-port-forwarding.pl
@@ -129,7 +129,8 @@ EOF
 sleep 5;
 
 # Record current log size, so we can skip prior output
-$ENV{LIMA_HOME} ||= "$ENV{HOME}/.lima";
+$ENV{HOME_HOST} ||= "$ENV{HOME}";
+$ENV{LIMA_HOME} ||= "$ENV{HOME_HOST}/.lima";
 my $ha_log = "$ENV{LIMA_HOME}/$instance/ha.stderr.log";
 my $ha_log_size = -s $ha_log or die;
 
@@ -211,11 +212,11 @@ sub JoinHostPort {
 # "ipv4" and "ipv6" will be replaced by the actual host ipv4 and ipv6 addresses.
 __DATA__
 portForwards:
-  # We can't test that port 22 will be blocked because the guestagent has
-  # been ignoring it since startup, so the log message is in the part of
-  # the log we skipped.
-  # skip: 127.0.0.1 22 → 127.0.0.1 2222
-  # ignore: 127.0.0.1 sshLocalPort
+# We can't test that port 22 will be blocked because the guestagent has
+# been ignoring it since startup, so the log message is in the part of
+# the log we skipped.
+# skip: 127.0.0.1 22 → 127.0.0.1 2222
+# ignore: 127.0.0.1 sshLocalPort
 
 - guestIP: 127.0.0.2
   guestPortRange: [3000, 3009]
diff --git a/hack/test-selinux.sh b/hack/test-selinux.sh
index 8ec318c2..817a6726 100755
--- a/hack/test-selinux.sh
+++ b/hack/test-selinux.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
diff --git a/hack/test-templates.sh b/hack/test-templates.sh
index 26e064b5..4b38ac0f 100755
--- a/hack/test-templates.sh
+++ b/hack/test-templates.sh
@@ -1,6 +1,13 @@
 #!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
+# will prevent msys2 converting Linux path arguments into Windows paths before passing to limactl
+export MSYS2_ARG_CONV_EXCL='*'
+
 scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 # shellcheck source=common.inc.sh
 source "${scriptdir}/common.inc.sh"
@@ -10,11 +17,22 @@ if [ "$#" -ne 1 ]; then
 	exit 1
 fi
 
-FILE="$1"
+# Resolve any ../ fragments in the filename because they are invalid in relative template locators
+FILE="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
 NAME="$(basename -s .yaml "$FILE")"
+OS_HOST="$(uname -o)"
+
+# On Windows $HOME of the bash runner, %USERPROFILE% of the host machine and mpunting point in the guest machine
+# are all different folders. This will handle path differences, when values are expilictly set.
+HOME_HOST=${HOME_HOST:-$HOME}
+HOME_GUEST=${HOME_GUEST:-$HOME}
+FILE_HOST=$FILE
+if [ "${OS_HOST}" = "Msys" ]; then
+	FILE_HOST="$(cygpath -w "$FILE")"
+fi
 
-INFO "Validating \"$FILE\""
-limactl validate "$FILE"
+INFO "Validating \"$FILE_HOST\""
+limactl validate "$FILE_HOST"
 
 # --cpus=1 is needed for running vz on GHA: https://github.com/lima-vm/lima/pull/1511#issuecomment-1574937888
 LIMACTL_CREATE=(limactl --tty=false create --cpus=1 --memory=1)
@@ -22,6 +40,7 @@ LIMACTL_CREATE=(limactl --tty=false create --cpus=1 --memory=1)
 CONTAINER_ENGINE="nerdctl"
 
 declare -A CHECKS=(
+	["proxy-settings"]="1"
 	["systemd"]="1"
 	["systemd-strict"]="1"
 	["mount-home"]="1"
@@ -36,6 +55,7 @@ declare -A CHECKS=(
 	["user-v2"]=""
 	["mount-path-with-spaces"]=""
 	["provision-ansible"]=""
+	["provision-data"]=""
 	["param-env-variables"]=""
 	["set-user"]=""
 )
@@ -63,12 +83,18 @@ case "$NAME" in
 	CHECKS["snapshot-offline"]="1"
 	CHECKS["mount-path-with-spaces"]="1"
 	CHECKS["provision-ansible"]="1"
+	CHECKS["provision-data"]="1"
 	CHECKS["param-env-variables"]="1"
 	CHECKS["set-user"]="1"
 	;;
 "docker")
 	CONTAINER_ENGINE="docker"
 	;;
+"wsl2")
+	# TODO https://github.com/lima-vm/lima/issues/3268
+	CHECKS["proxy-settings"]=
+	CHECKS["port-forwards"]=
+	;;
 esac
 
 if limactl ls -q | grep -q "$NAME"; then
@@ -76,12 +102,7 @@ if limactl ls -q | grep -q "$NAME"; then
 	exit 1
 fi
 
-# Create ${NAME}-tmp to inspect the enabled features.
-# TODO: skip downloading and converting the image here.
-# Probably `limactl create` should have "dry run" mode that just generates `lima.yaml`.
-# shellcheck disable=SC2086
-"${LIMACTL_CREATE[@]}" ${LIMACTL_CREATE_ARGS} --set ".additionalDisks=null" --name="${NAME}-tmp" "$FILE"
-case "$(yq '.networks[].lima' "${LIMA_HOME}/${NAME}-tmp/lima.yaml")" in
+case "$(limactl tmpl yq "$FILE_HOST" '.networks[].lima')" in
 "shared")
 	CHECKS["vmnet"]=1
 	;;
@@ -90,35 +111,43 @@ case "$(yq '.networks[].lima' "${LIMA_HOME}/${NAME}-tmp/lima.yaml")" in
 	CHECKS["user-v2"]=1
 	;;
 esac
-limactl rm -f "${NAME}-tmp"
 
 if [[ -n ${CHECKS["port-forwards"]} ]]; then
-	tmpconfig="$HOME/lima-config-tmp"
+	tmpconfig="$HOME_HOST/lima-config-tmp"
 	mkdir -p "${tmpconfig}"
 	defer "rm -rf \"$tmpconfig\""
 	tmpfile="${tmpconfig}/${NAME}.yaml"
 	cp "$FILE" "${tmpfile}"
 	FILE="${tmpfile}"
+	FILE_HOST=$FILE
+	if [ "${OS_HOST}" = "Msys" ]; then
+		FILE_HOST="$(cygpath -w "$FILE")"
+	fi
+
 	INFO "Setup port forwarding rules for testing in \"${FILE}\""
 	"${scriptdir}/test-port-forwarding.pl" "${FILE}"
-	limactl validate "$FILE"
+	INFO "Validating \"$FILE_HOST\""
+	limactl validate "$FILE_HOST"
 fi
 
+INFO "Make sure template embedding copies \"$FILE_HOST\" exactly"
+diff -u <(echo -n "base: $FILE_HOST" | limactl tmpl copy --embed - -) "$FILE_HOST"
+
 function diagnose() {
 	NAME="$1"
 	set -x +e
-	tail "$HOME/.lima/${NAME}"/*.log
+	tail "$HOME_HOST/.lima/${NAME}"/*.log
 	limactl shell "$NAME" systemctl --no-pager status
 	limactl shell "$NAME" systemctl --no-pager
 	mkdir -p failure-logs
-	cp -pf "$HOME/.lima/${NAME}"/*.log failure-logs/
+	cp -pf "$HOME_HOST/.lima/${NAME}"/*.log failure-logs/
 	limactl shell "$NAME" sudo cat /var/log/cloud-init-output.log | tee failure-logs/cloud-init-output.log
 	set +x -e
 }
 
 export ftp_proxy=http://localhost:2121
 
-INFO "Creating \"$NAME\" from \"$FILE\""
+INFO "Creating \"$NAME\" from \"$FILE_HOST\""
 defer "limactl delete -f \"$NAME\""
 
 if [[ -n ${CHECKS["disk"]} ]]; then
@@ -130,7 +159,7 @@ fi
 
 set -x
 # shellcheck disable=SC2086
-"${LIMACTL_CREATE[@]}" ${LIMACTL_CREATE_ARGS} "$FILE"
+"${LIMACTL_CREATE[@]}" ${LIMACTL_CREATE_ARGS} "$FILE_HOST"
 set +x
 
 if [[ -n ${CHECKS["mount-path-with-spaces"]} ]]; then
@@ -152,7 +181,7 @@ limactl shell "$NAME" cat /etc/os-release
 set +x
 
 INFO "Testing that host home is not wiped out"
-[ -e "$HOME/.lima" ]
+[ -e "$HOME_HOST/.lima" ]
 
 if [[ -n ${CHECKS["mount-path-with-spaces"]} ]]; then
 	INFO 'Testing that "/tmp/lima test dir with spaces" is not wiped out'
@@ -165,30 +194,38 @@ if [[ -n ${CHECKS["provision-ansible"]} ]]; then
 	limactl shell "$NAME" test -e /tmp/ansible
 fi
 
+if [[ -n ${CHECKS["provision-data"]} ]]; then
+	INFO 'Testing that /etc/sysctl.d/99-inotify.conf was created successfully on provision'
+	limactl shell "$NAME" grep -q fs.inotify.max_user_watches /etc/sysctl.d/99-inotify.conf
+fi
+
 if [[ -n ${CHECKS["param-env-variables"]} ]]; then
 	INFO 'Testing that PARAM env variables are exported to all types of provisioning scripts and probes'
 	limactl shell "$NAME" test -e /tmp/param-boot
 	limactl shell "$NAME" test -e /tmp/param-dependency
 	limactl shell "$NAME" test -e /tmp/param-probe
 	limactl shell "$NAME" test -e /tmp/param-system
-	limactl shell "$NAME" test -e /tmp/param-user
+	# TODO re-enable once https://github.com/lima-vm/lima/issues/3308 is fixed
+	# limactl shell "$NAME" test -e /tmp/param-user
 fi
 
 if [[ -n ${CHECKS["set-user"]} ]]; then
 	INFO 'Testing that user settings can be provided by lima.yaml'
-	limactl shell "$NAME" grep "^john:x:4711:4711:John Doe:/home/john-john" /etc/passwd
+	limactl shell "$NAME" grep "^john:x:4711:4711:John Doe:/home/john-john:/usr/bin/bash" /etc/passwd
 fi
 
-INFO "Testing proxy settings are imported"
-got=$(limactl shell "$NAME" env | grep FTP_PROXY)
-# Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced
-# by the gateway address, and the value is set immediately without a restart
-gatewayIp=$(limactl shell "$NAME" ip route show 0.0.0.0/0 dev eth0 | cut -d\  -f3)
-expected="FTP_PROXY=http://${gatewayIp}:2121"
-INFO "FTP_PROXY: expected=${expected} got=${got}"
-if [ "$got" != "$expected" ]; then
-	ERROR "proxy environment variable not set to correct value"
-	exit 1
+if [[ -n ${CHECKS["proxy-settings"]} ]]; then
+	INFO "Testing proxy settings are imported"
+	got=$(limactl shell "$NAME" env | grep FTP_PROXY)
+	# Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced
+	# by the gateway address, and the value is set immediately without a restart
+	gatewayIp=$(limactl shell "$NAME" ip route show 0.0.0.0/0 dev eth0 | cut -d\  -f3)
+	expected="FTP_PROXY=http://${gatewayIp}:2121"
+	INFO "FTP_PROXY: expected=${expected} got=${got}"
+	if [ "$got" != "$expected" ]; then
+		ERROR "proxy environment variable not set to correct value"
+		exit 1
+	fi
 fi
 
 INFO "Testing limactl copy command"
@@ -196,7 +233,11 @@ tmpdir="$(mktemp -d "${TMPDIR:-/tmp}"/lima-test-templates.XXXXXX)"
 defer "rm -rf \"$tmpdir\""
 tmpfile="$tmpdir/lima-hostname"
 rm -f "$tmpfile"
-limactl cp "$NAME":/etc/hostname "$tmpfile"
+tmpfile_host=$tmpfile
+if [ "${OS_HOST}" = "Msys" ]; then
+	tmpfile_host="$(cygpath -w "$tmpfile")"
+fi
+limactl cp "$NAME":/etc/hostname "$tmpfile_host"
 expected="$(limactl shell "$NAME" cat /etc/hostname)"
 got="$(cat "$tmpfile")"
 INFO "/etc/hostname: expected=${expected}, got=${got}"
@@ -234,32 +275,38 @@ nginx_image="ghcr.io/stargz-containers/nginx:1.19-alpine-org"
 alpine_image="ghcr.io/containerd/alpine:3.14.0"
 
 if [[ -n ${CHECKS["container-engine"]} ]]; then
+	sudo=""
+	# Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future.
+	if [[ "$(limactl ls --json "${NAME}" | jq -r .vmType)" == "wsl2" ]]; then
+		sudo="sudo"
+	fi
 	INFO "Run a nginx container with port forwarding 127.0.0.1:8080"
 	set -x
-	if ! limactl shell "$NAME" $CONTAINER_ENGINE info; then
-		limactl shell "$NAME" sudo cat /var/log/cloud-init-output.log
+	if ! limactl shell "$NAME" $sudo $CONTAINER_ENGINE info; then
+		limactl shell "$NAME" cat /var/log/cloud-init-output.log
 		ERROR "\"${CONTAINER_ENGINE} info\" failed"
 		exit 1
 	fi
-	limactl shell "$NAME" $CONTAINER_ENGINE pull --quiet ${nginx_image}
-	limactl shell "$NAME" $CONTAINER_ENGINE run -d --name nginx -p 127.0.0.1:8080:80 ${nginx_image}
+	limactl shell "$NAME" $sudo $CONTAINER_ENGINE pull --quiet ${nginx_image}
+	limactl shell "$NAME" $sudo $CONTAINER_ENGINE run -d --name nginx -p 127.0.0.1:8080:80 ${nginx_image}
 
 	timeout 3m bash -euxc "until curl -f --retry 30 --retry-connrefused http://127.0.0.1:8080; do sleep 3; done"
 
-	limactl shell "$NAME" $CONTAINER_ENGINE rm -f nginx
+	limactl shell "$NAME" $sudo $CONTAINER_ENGINE rm -f nginx
 	set +x
 	if [[ -n ${CHECKS["mount-home"]} ]]; then
-		hometmp="$HOME/lima-container-engine-test-tmp"
+		hometmp="$HOME_HOST/lima-container-engine-test-tmp"
+		hometmpguest="$HOME_GUEST/lima-container-engine-test-tmp"
 		# test for https://github.com/lima-vm/lima/issues/187
 		INFO "Testing home bind mount (\"$hometmp\")"
 		rm -rf "$hometmp"
 		mkdir -p "$hometmp"
 		defer "rm -rf \"$hometmp\""
 		set -x
-		limactl shell "$NAME" $CONTAINER_ENGINE pull --quiet ${alpine_image}
+		limactl shell "$NAME" $sudo $CONTAINER_ENGINE pull --quiet ${alpine_image}
 		echo "random-content-${RANDOM}" >"$hometmp/random"
 		expected="$(cat "$hometmp/random")"
-		got="$(limactl shell "$NAME" $CONTAINER_ENGINE run --rm -v "$hometmp/random":/mnt/foo ${alpine_image} cat /mnt/foo)"
+		got="$(limactl shell "$NAME" $sudo $CONTAINER_ENGINE run --rm -v "$hometmpguest/random":/mnt/foo ${alpine_image} cat /mnt/foo)"
 		INFO "$hometmp/random: expected=${expected}, got=${got}"
 		if [ "$got" != "$expected" ]; then
 			ERROR "Home directory is not shared?"
@@ -284,6 +331,9 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then
 	if [ "${NAME}" = "opensuse" ]; then
 		limactl shell "$NAME" sudo zypper in -y netcat-openbsd
 	fi
+	if limactl shell "$NAME" command -v dnf; then
+		limactl shell "$NAME" sudo dnf install -y nc
+	fi
 	"${scriptdir}/test-port-forwarding.pl" "${NAME}"
 
 	if [[ -n ${CHECKS["container-engine"]} || ${NAME} == "alpine"* ]]; then
@@ -306,6 +356,10 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then
 				rm nerdctl-full.tgz
 				sudo="sudo"
 			fi
+			# Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future.
+			if [[ "$(limactl ls --json "${NAME}" | jq -r .vmType)" == "wsl2" ]]; then
+				sudo="sudo"
+			fi
 			limactl shell "$NAME" $sudo $CONTAINER_ENGINE info
 			limactl shell "$NAME" $sudo $CONTAINER_ENGINE pull --quiet ${nginx_image}
 
@@ -360,7 +414,8 @@ if [[ -n ${CHECKS["restart"]} ]]; then
 	fi
 
 	INFO "Stopping \"$NAME\""
-	limactl stop "$NAME"
+	# TODO https://github.com/lima-vm/lima/issues/3221
+	limactl stop "$NAME" || [ "${OS_HOST}" = "Msys" ]
 	sleep 3
 
 	if [[ -n ${CHECKS["disk"]} ]]; then
@@ -406,7 +461,7 @@ fi
 if [[ -n ${CHECKS["user-v2"]} ]]; then
 	INFO "Testing user-v2 network"
 	secondvm="$NAME-1"
-	"${LIMACTL_CREATE[@]}" --set ".additionalDisks=null" "$FILE" --name "$secondvm"
+	"${LIMACTL_CREATE[@]}" --set ".additionalDisks=null" "$FILE_HOST" --name "$secondvm"
 	if ! limactl start "$secondvm"; then
 		ERROR "Failed to start \"$secondvm\""
 		diagnose "$secondvm"
@@ -474,7 +529,8 @@ if [[ $NAME == "fedora" && "$(limactl ls --json "$NAME" | jq -r .vmType)" == "vz
 fi
 
 INFO "Stopping \"$NAME\""
-limactl stop "$NAME"
+# TODO https://github.com/lima-vm/lima/issues/3221
+limactl stop "$NAME" || [ "${OS_HOST}" = "Msys" ]
 sleep 3
 
 INFO "Deleting \"$NAME\""
diff --git a/hack/test-templates/test-misc.yaml b/hack/test-templates/test-misc.yaml
index 2137c5f3..1c660fff 100644
--- a/hack/test-templates/test-misc.yaml
+++ b/hack/test-templates/test-misc.yaml
@@ -3,21 +3,7 @@
 # - snapshots
 # - (More to come)
 #
-# This template requires Lima v1.0.0-alpha.0 or later.
-images:
-# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
-- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20220902/ubuntu-22.04-server-cloudimg-amd64.img"
-  arch: "x86_64"
-  digest: "sha256:c777670007cc5f132417b9e0bc01367ccfc2a989951ffa225bb1952917c3aa81"
-- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20220902/ubuntu-22.04-server-cloudimg-arm64.img"
-  arch: "aarch64"
-  digest: "sha256:9620f479bd5a6cbf1e805654d41b27f4fc56ef20f916c8331558241734de81ae"
-# Fallback to the latest release image.
-# Hint: run `limactl prune` to invalidate the cache
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
-  arch: "x86_64"
-- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
-  arch: "aarch64"
+base: template://ubuntu-22.04
 
 # 9p is not compatible with `limactl snapshot`
 mountTypesUnsupported: ["9p"]
@@ -26,15 +12,14 @@ mounts:
   writable: true
 - location: "/tmp/lima test dir with spaces"
   writable: true
-- location: "/tmp/lima"
-  writable: true
 
 param:
   BOOT: boot
   DEPENDENCY: dependency
   PROBE: probe
   SYSTEM: system
-  USER: user
+# TODO re-enable once https://github.com/lima-vm/lima/issues/3308 is fixed
+# USER: user
 
 provision:
 - mode: ansible
@@ -45,8 +30,14 @@ provision:
   script: "touch /tmp/param-$PARAM_DEPENDENCY"
 - mode: system
   script: "touch /tmp/param-$PARAM_SYSTEM"
-- mode: user
-  script: "touch /tmp/param-$PARAM_USER"
+# TODO re-enable once https://github.com/lima-vm/lima/issues/3308 is fixed
+# - mode: user
+#   script: "touch /tmp/param-$PARAM_USER"
+- mode: data
+  path: /etc/sysctl.d/99-inotify.conf
+  content: |
+    fs.inotify.max_user_watches = 524288
+    fs.inotify.max_user_instances = 512
 
 probes:
 - mode: readiness
@@ -64,3 +55,5 @@ user:
   comment: John Doe
   home: "/home/{{.User}}-{{.User}}"
   uid: 4711
+  # Ubuntu has identical /bin/bash and /usr/bin/bash
+  shell: /usr/bin/bash
diff --git a/hack/test-upgrade.sh b/hack/test-upgrade.sh
index 8b79bafb..696295f6 100755
--- a/hack/test-upgrade.sh
+++ b/hack/test-upgrade.sh
@@ -1,4 +1,8 @@
 #!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
diff --git a/hack/toolexec-for-codesign.sh b/hack/toolexec-for-codesign.sh
index 1b58d1be..2943385f 100755
--- a/hack/toolexec-for-codesign.sh
+++ b/hack/toolexec-for-codesign.sh
@@ -1,5 +1,8 @@
 #!/bin/sh
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # This script is used to wrap the compiler and linker commands in the build
 # process. It captures the output of the command and logs it to a file.
 # The script's primary purpose is codesigning the output of the linker command
diff --git a/hack/update-template-almalinux.sh b/hack/update-template-almalinux.sh
index 6613f7f8..8f3076aa 100755
--- a/hack/update-template-almalinux.sh
+++ b/hack/update-template-almalinux.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-alpine.sh b/hack/update-template-alpine.sh
index 1c745572..c2dbce82 100755
--- a/hack/update-template-alpine.sh
+++ b/hack/update-template-alpine.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-archlinux.sh b/hack/update-template-archlinux.sh
index 6a2a53db..a3fd6c58 100755
--- a/hack/update-template-archlinux.sh
+++ b/hack/update-template-archlinux.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-centos-stream.sh b/hack/update-template-centos-stream.sh
index 01ebe327..b6142484 100755
--- a/hack/update-template-centos-stream.sh
+++ b/hack/update-template-centos-stream.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-debian.sh b/hack/update-template-debian.sh
index 044ebc81..d7feee70 100755
--- a/hack/update-template-debian.sh
+++ b/hack/update-template-debian.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-fedora.sh b/hack/update-template-fedora.sh
index 39f5bb4d..0386babe 100755
--- a/hack/update-template-fedora.sh
+++ b/hack/update-template-fedora.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-opensuse.sh b/hack/update-template-opensuse.sh
index 02196c42..541e530d 100755
--- a/hack/update-template-opensuse.sh
+++ b/hack/update-template-opensuse.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-oraclelinux.sh b/hack/update-template-oraclelinux.sh
index e487e914..065311e6 100755
--- a/hack/update-template-oraclelinux.sh
+++ b/hack/update-template-oraclelinux.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-rocky.sh b/hack/update-template-rocky.sh
index 989cb318..a83779ec 100755
--- a/hack/update-template-rocky.sh
+++ b/hack/update-template-rocky.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template-ubuntu.sh b/hack/update-template-ubuntu.sh
index 9ac8b4a9..32b373a5 100755
--- a/hack/update-template-ubuntu.sh
+++ b/hack/update-template-ubuntu.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/hack/update-template.sh b/hack/update-template.sh
index 5d965ed7..9fc4b9bf 100755
--- a/hack/update-template.sh
+++ b/hack/update-template.sh
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu -o pipefail
 
 # Functions in this script assume error handling with 'set -e'.
diff --git a/pkg/autostart/autostart.go b/pkg/autostart/autostart.go
index f95872dd..038b2035 100644
--- a/pkg/autostart/autostart.go
+++ b/pkg/autostart/autostart.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // Package autostart manage start at login unit files for darwin/linux
 package autostart
 
diff --git a/pkg/bicopy/bicopy.go b/pkg/bicopy/bicopy.go
index 3db68521..db6d44c0 100644
--- a/pkg/bicopy/bicopy.go
+++ b/pkg/bicopy/bicopy.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // From https://raw.githubusercontent.com/norouter/norouter/v0.6.5/pkg/agent/bicopy/bicopy.go
 /*
    Copyright (C) NoRouter authors.
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot.sh b/pkg/cidata/cidata.TEMPLATE.d/boot.sh
index ab65c719..35c3faa6 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu
 
 INFO() {
@@ -48,6 +52,31 @@ else
 	done
 fi
 
+# indirect variable lookup, like ${!var} in bash
+deref() {
+	eval echo \$"$1"
+}
+
+if [ -d "${LIMA_CIDATA_MNT}"/provision.data ]; then
+	for f in "${LIMA_CIDATA_MNT}"/provision.data/*; do
+		filename=$(basename "$f")
+		overwrite=$(deref "LIMA_CIDATA_DATAFILE_${filename}_OVERWRITE")
+		owner=$(deref "LIMA_CIDATA_DATAFILE_${filename}_OWNER")
+		path=$(deref "LIMA_CIDATA_DATAFILE_${filename}_PATH")
+		permissions=$(deref "LIMA_CIDATA_DATAFILE_${filename}_PERMISSIONS")
+		if [ -e "$path" ] && [ "$overwrite" = "false" ]; then
+			INFO "Not overwriting $path"
+		else
+			INFO "Copying $f to $path"
+			# intermediate directories will be owned by root, regardless of OWNER setting
+			mkdir -p "$(dirname "$path")"
+			cp "$f" "$path"
+			chown "$owner" "$path"
+			chmod "$permissions" "$path"
+		fi
+	done
+fi
+
 if [ -d "${LIMA_CIDATA_MNT}"/provision.system ]; then
 	for f in "${LIMA_CIDATA_MNT}"/provision.system/*; do
 		INFO "Executing $f"
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/00-alpine-user-group.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/00-alpine-user-group.sh
index cf2ff5f2..e6d25175 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/00-alpine-user-group.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/00-alpine-user-group.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 test -f /etc/alpine-release || exit 0
 
 # Make sure that root is in the sudoers file.
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/00-check-rtc-and-wait-ntp.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/00-check-rtc-and-wait-ntp.sh
index c799e9c1..b8a93333 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/00-check-rtc-and-wait-ntp.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/00-check-rtc-and-wait-ntp.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu
 
 # In vz, the VM lacks an RTC when booting with a kernel image (see: https://developer.apple.com/forums/thread/760344).
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/00-modprobe.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/00-modprobe.sh
index 6dea53fd..599dae52 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/00-modprobe.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/00-modprobe.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # Load modules as soon as the cloud-init starts up.
 # Because Arch Linux removes kernel module files when the kernel package was updated during running cloud-init :(
 
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/00-reboot-if-required.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/00-reboot-if-required.sh
index 968181a1..fe75c1ae 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/00-reboot-if-required.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/00-reboot-if-required.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 # Check if cloud-init forgot to reboot_if_required
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/01-alpine-ash-as-bash.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/01-alpine-ash-as-bash.sh
index f7e54019..a06e4770 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/01-alpine-ash-as-bash.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/01-alpine-ash-as-bash.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # This script pretends that /bin/ash can be used as /bin/bash, so all following
 # cloud-init scripts can use `#!/bin/bash` and `set -o pipefail`.
 test -f /etc/alpine-release || exit 0
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh
index 68391712..ec187e3c 100755
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # This script replaces the cloud-init functionality of creating a user and setting its SSH keys
 # when using a WSL2 VM.
 [ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/04-persistent-data-volume.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/04-persistent-data-volume.sh
index 423538f3..9fb14246 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/04-persistent-data-volume.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/04-persistent-data-volume.sh
@@ -1,4 +1,8 @@
 #!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 # bash is used for enabling `set -o pipefail`.
 # NOTE: On Alpine, /bin/bash is ash with ASH_BASH_COMPAT, not GNU bash
 set -eux -o pipefail
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh
index a8a7726d..d4816f9b 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh
@@ -1,5 +1,8 @@
 #!/bin/bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux -o pipefail
 
 test "$LIMA_CIDATA_DISKS" -gt 0 || exit 0
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-mounts.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-mounts.sh
index 47298c75..adc7d2f7 100755
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-mounts.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-mounts.sh
@@ -1,5 +1,8 @@
 #!/bin/bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux -o pipefail
 
 # Check if mount type is virtiofs and vm type as vz
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/05-rosetta-volume.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/05-rosetta-volume.sh
index 42bc069b..4a43ff3f 100755
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/05-rosetta-volume.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/05-rosetta-volume.sh
@@ -1,5 +1,8 @@
 #!/bin/bash
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux -o pipefail
 
 if [ "$LIMA_CIDATA_ROSETTA_ENABLED" != "true" ]; then
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh
index 97b5cdfd..683397ad 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh
@@ -1,4 +1,8 @@
 #!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux -o pipefail
 
 # Define host.lima.internal in case the hostResolver is disabled. When using
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/07-etc-environment.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/07-etc-environment.sh
index 245678de..2083adcb 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/07-etc-environment.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/07-etc-environment.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 # /etc/environment must be written after 04-persistent-data-volume.sh has run to
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/08-shell-prompt.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/08-shell-prompt.sh
index 9edb33a4..3430a2df 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/08-shell-prompt.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/08-shell-prompt.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 # This script is only intended for the default.yaml image, which is based on Ubuntu LTS
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/09-host-dns-setup.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/09-host-dns-setup.sh
index ee14366f..7bd1aac4 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/09-host-dns-setup.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/09-host-dns-setup.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 readonly chain=LIMADNS
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/10-alpine-prep.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/10-alpine-prep.sh
index 606f5d6c..d1057821 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/10-alpine-prep.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/10-alpine-prep.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 # This script prepares Alpine for lima; there is nothing in here for other distros
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/11-colorterm-environment.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/11-colorterm-environment.sh
index 1142fac3..5a09fa92 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/11-colorterm-environment.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/11-colorterm-environment.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 if [ -d /etc/ssh/sshd_config.d ]; then
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/20-rootless-base.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/20-rootless-base.sh
index c5907a92..0af775b0 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/20-rootless-base.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/20-rootless-base.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 # This script does not work unless systemd is available
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh
index 893b92eb..d48b2846 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh
@@ -1,5 +1,8 @@
 #!/bin/sh
 
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
index 8ede9790..b1d3a6ca 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 INSTALL_IPTABLES=0
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/35-setup-packages.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/35-setup-packages.sh
index 8084da70..3c26a9b0 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/35-setup-packages.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/35-setup-packages.sh
@@ -1,4 +1,8 @@
 #!/bin/sh
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 
 update_fuse_conf() {
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/40-install-containerd.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/40-install-containerd.sh
index 3ab3df4e..aa7843b2 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/40-install-containerd.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/40-install-containerd.sh
@@ -1,4 +1,8 @@
 #!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eux
 : "${CONTAINERD_NAMESPACE:=default}"
 # Overridable in .bashrc
diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env
index b1720f0d..89d2d34f 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/lima.env
+++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env
@@ -19,6 +19,12 @@ LIMA_CIDATA_DISK_{{$i}}_FORMAT={{$disk.Format}}
 LIMA_CIDATA_DISK_{{$i}}_FSTYPE={{$disk.FSType}}
 LIMA_CIDATA_DISK_{{$i}}_FSARGS={{range $j, $arg := $disk.FSArgs}}{{if $j}} {{end}}{{$arg}}{{end}}
 {{- end}}
+{{- range $dataFile := .DataFiles}}
+LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_OVERWRITE={{$dataFile.Overwrite}}
+LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_OWNER={{$dataFile.Owner}}
+LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_PATH={{$dataFile.Path}}
+LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_PERMISSIONS={{$dataFile.Permissions}}
+{{- end}}
 LIMA_CIDATA_GUEST_INSTALL_PREFIX={{ .GuestInstallPrefix }}
 {{- if .Containerd.User}}
 LIMA_CIDATA_CONTAINERD_USER=1
diff --git a/pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh b/pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh
index b1d63a9c..e3cf3085 100755
--- a/pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh
@@ -1,4 +1,8 @@
 #!/bin/bash
+
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
+
 set -eu
 
 : "${SELFTEST:=}"
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index abd2ccf4..6028a63f 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package cidata
 
 import (
@@ -11,6 +14,7 @@ import (
 	"path"
 	"path/filepath"
 	"slices"
+	"strconv"
 	"strings"
 	"time"
 
@@ -197,14 +201,6 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 	}
 	for i, f := range instConfig.Mounts {
 		tag := fmt.Sprintf("mount%d", i)
-		location, err := localpathutil.Expand(f.Location)
-		if err != nil {
-			return nil, err
-		}
-		mountPoint, err := localpathutil.Expand(*f.MountPoint)
-		if err != nil {
-			return nil, err
-		}
 		options := "defaults"
 		switch fstype {
 		case "9p", "virtiofs":
@@ -217,7 +213,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 				options += fmt.Sprintf(",version=%s", *f.NineP.ProtocolVersion)
 				msize, err := units.RAMInBytes(*f.NineP.Msize)
 				if err != nil {
-					return nil, fmt.Errorf("failed to parse msize for %q: %w", location, err)
+					return nil, fmt.Errorf("failed to parse msize for %q: %w", f.Location, err)
 				}
 				options += fmt.Sprintf(",msize=%d", msize)
 				options += fmt.Sprintf(",cache=%s", *f.NineP.Cache)
@@ -225,9 +221,9 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 			// don't fail the boot, if virtfs is not available
 			options += ",nofail"
 		}
-		args.Mounts = append(args.Mounts, Mount{Tag: tag, MountPoint: mountPoint, Type: fstype, Options: options})
-		if location == hostHome {
-			args.HostHomeMountPoint = mountPoint
+		args.Mounts = append(args.Mounts, Mount{Tag: tag, MountPoint: *f.MountPoint, Type: fstype, Options: options})
+		if f.Location == hostHome {
+			args.HostHomeMountPoint = *f.MountPoint
 		}
 	}
 
@@ -319,10 +315,19 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L
 
 	args.BootCmds = getBootCmds(instConfig.Provision)
 
-	for _, f := range instConfig.Provision {
+	for i, f := range instConfig.Provision {
 		if f.Mode == limayaml.ProvisionModeDependency && *f.SkipDefaultDependencyResolution {
 			args.SkipDefaultDependencyResolution = true
 		}
+		if f.Mode == limayaml.ProvisionModeData {
+			args.DataFiles = append(args.DataFiles, DataFile{
+				FileName:    fmt.Sprintf("%08d", i),
+				Overwrite:   strconv.FormatBool(*f.Overwrite),
+				Owner:       *f.Owner,
+				Path:        *f.Path,
+				Permissions: *f.Permissions,
+			})
+		}
 	}
 
 	return &args, nil
@@ -373,6 +378,11 @@ func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNS
 				Path:   fmt.Sprintf("provision.%s/%08d", f.Mode, i),
 				Reader: strings.NewReader(f.Script),
 			})
+		case limayaml.ProvisionModeData:
+			layout = append(layout, iso9660util.Entry{
+				Path:   fmt.Sprintf("provision.%s/%08d", f.Mode, i),
+				Reader: strings.NewReader(*f.Content),
+			})
 		case limayaml.ProvisionModeBoot:
 			continue
 		case limayaml.ProvisionModeAnsible:
diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go
index b2287f6e..70cba120 100644
--- a/pkg/cidata/template.go
+++ b/pkg/cidata/template.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package cidata
 
 import (
@@ -47,6 +50,15 @@ type Mount struct {
 type BootCmds struct {
 	Lines []string
 }
+
+type DataFile struct {
+	FileName    string
+	Overwrite   string
+	Owner       string
+	Path        string
+	Permissions string
+}
+
 type Disk struct {
 	Name   string
 	Device string
@@ -81,6 +93,7 @@ type TemplateArgs struct {
 	Env                             map[string]string
 	Param                           map[string]string
 	BootScripts                     bool
+	DataFiles                       []DataFile
 	DNSAddresses                    []string
 	CACerts                         CACerts
 	HostHomeMountPoint              string
diff --git a/pkg/debugutil/debugutil.go b/pkg/debugutil/debugutil.go
index 241a2069..3586d001 100644
--- a/pkg/debugutil/debugutil.go
+++ b/pkg/debugutil/debugutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package debugutil
 
 // Debug is set via main.
diff --git a/pkg/downloader/downloader.go b/pkg/downloader/downloader.go
index dd25f17d..785261ea 100644
--- a/pkg/downloader/downloader.go
+++ b/pkg/downloader/downloader.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package downloader
 
 import (
diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go
index 1d44e32e..457265ae 100644
--- a/pkg/driver/driver.go
+++ b/pkg/driver/driver.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package driver
 
 import (
diff --git a/pkg/driverutil/driverutil.go b/pkg/driverutil/driverutil.go
index a0d12787..eb27833e 100644
--- a/pkg/driverutil/driverutil.go
+++ b/pkg/driverutil/driverutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package driverutil
 
 import (
diff --git a/pkg/driverutil/instance.go b/pkg/driverutil/instance.go
index f93b3854..d7c443ff 100644
--- a/pkg/driverutil/instance.go
+++ b/pkg/driverutil/instance.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package driverutil
 
 import (
diff --git a/pkg/editutil/editorcmd/editorcmd.go b/pkg/editutil/editorcmd/editorcmd.go
index 3488079d..796f38dd 100644
--- a/pkg/editutil/editorcmd/editorcmd.go
+++ b/pkg/editutil/editorcmd/editorcmd.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // From https://raw.githubusercontent.com/norouter/norouter/v0.6.5/cmd/norouter/editorcmd/editorcmd.go
 /*
    Copyright (C) NoRouter authors.
diff --git a/pkg/editutil/editutil.go b/pkg/editutil/editutil.go
index 298beffa..a8a172cd 100644
--- a/pkg/editutil/editutil.go
+++ b/pkg/editutil/editutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package editutil
 
 import (
diff --git a/pkg/executil/command.go b/pkg/executil/command.go
index 8290f8a9..22f374d0 100644
--- a/pkg/executil/command.go
+++ b/pkg/executil/command.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package executil
 
 import (
diff --git a/pkg/executil/opts_others.go b/pkg/executil/opts_others.go
index 41c5479a..64adc0e9 100644
--- a/pkg/executil/opts_others.go
+++ b/pkg/executil/opts_others.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package executil
 
 import (
diff --git a/pkg/executil/opts_windows.go b/pkg/executil/opts_windows.go
index 99f62901..5d31801e 100644
--- a/pkg/executil/opts_windows.go
+++ b/pkg/executil/opts_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package executil
 
 import (
diff --git a/pkg/fileutils/download.go b/pkg/fileutils/download.go
index 97bf6d30..bb54604e 100644
--- a/pkg/fileutils/download.go
+++ b/pkg/fileutils/download.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package fileutils
 
 import (
diff --git a/pkg/freeport/freeport.go b/pkg/freeport/freeport.go
index d4e1a71a..75f7aa94 100644
--- a/pkg/freeport/freeport.go
+++ b/pkg/freeport/freeport.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // Package freeport provides functions to find free localhost ports.
 package freeport
 
diff --git a/pkg/freeport/freeport_unix.go b/pkg/freeport/freeport_unix.go
index 180e0362..bc4c0e73 100644
--- a/pkg/freeport/freeport_unix.go
+++ b/pkg/freeport/freeport_unix.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package freeport
 
 import "errors"
diff --git a/pkg/freeport/freeport_windows.go b/pkg/freeport/freeport_windows.go
index 48bef99d..6fdbf8f0 100644
--- a/pkg/freeport/freeport_windows.go
+++ b/pkg/freeport/freeport_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package freeport
 
 import "github.com/lima-vm/lima/pkg/windows"
diff --git a/pkg/fsutil/fsutil_linux.go b/pkg/fsutil/fsutil_linux.go
index 101f8d65..cd87951e 100644
--- a/pkg/fsutil/fsutil_linux.go
+++ b/pkg/fsutil/fsutil_linux.go
@@ -1,4 +1,5 @@
-//go:build linux
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
 
 package fsutil
 
diff --git a/pkg/fsutil/fsutil_others.go b/pkg/fsutil/fsutil_others.go
index c85cd346..868e242b 100644
--- a/pkg/fsutil/fsutil_others.go
+++ b/pkg/fsutil/fsutil_others.go
@@ -1,5 +1,8 @@
 //go:build !linux
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package fsutil
 
 func IsNFS(string) (bool, error) {
diff --git a/pkg/guestagent/api/client/client.go b/pkg/guestagent/api/client/client.go
index d4bdf27e..934d1534 100644
--- a/pkg/guestagent/api/client/client.go
+++ b/pkg/guestagent/api/client/client.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package client
 
 import (
diff --git a/pkg/guestagent/api/client/credentials.go b/pkg/guestagent/api/client/credentials.go
index 76470f14..a64f8ae7 100644
--- a/pkg/guestagent/api/client/credentials.go
+++ b/pkg/guestagent/api/client/credentials.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package client
 
 import (
diff --git a/pkg/guestagent/api/gen.go b/pkg/guestagent/api/gen.go
index 9822947f..d0980afc 100644
--- a/pkg/guestagent/api/gen.go
+++ b/pkg/guestagent/api/gen.go
@@ -1,3 +1,6 @@
 //go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative guestservice.proto --descriptor_set_out=guestservice.pb.desc
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package api
diff --git a/pkg/guestagent/api/ipport.go b/pkg/guestagent/api/ipport.go
index 504d8a03..adf2a66d 100644
--- a/pkg/guestagent/api/ipport.go
+++ b/pkg/guestagent/api/ipport.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package api
 
 import (
diff --git a/pkg/guestagent/api/server/server.go b/pkg/guestagent/api/server/server.go
index 8c4fcde2..84018613 100644
--- a/pkg/guestagent/api/server/server.go
+++ b/pkg/guestagent/api/server/server.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package server
 
 import (
diff --git a/pkg/guestagent/guestagent.go b/pkg/guestagent/guestagent.go
index b84e29fe..b63f6e00 100644
--- a/pkg/guestagent/guestagent.go
+++ b/pkg/guestagent/guestagent.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package guestagent
 
 import (
diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go
index 69fc3baf..91d1c4d5 100644
--- a/pkg/guestagent/guestagent_linux.go
+++ b/pkg/guestagent/guestagent_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package guestagent
 
 import (
diff --git a/pkg/guestagent/iptables/iptables.go b/pkg/guestagent/iptables/iptables.go
index a997a80d..294c2e58 100644
--- a/pkg/guestagent/iptables/iptables.go
+++ b/pkg/guestagent/iptables/iptables.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package iptables
 
 import (
diff --git a/pkg/guestagent/kubernetesservice/kubernetesservice.go b/pkg/guestagent/kubernetesservice/kubernetesservice.go
index 45d21d46..62668e31 100644
--- a/pkg/guestagent/kubernetesservice/kubernetesservice.go
+++ b/pkg/guestagent/kubernetesservice/kubernetesservice.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package kubernetesservice
 
 import (
diff --git a/pkg/guestagent/procnettcp/procnettcp.go b/pkg/guestagent/procnettcp/procnettcp.go
index 3018a0fa..a1145705 100644
--- a/pkg/guestagent/procnettcp/procnettcp.go
+++ b/pkg/guestagent/procnettcp/procnettcp.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package procnettcp
 
 import (
diff --git a/pkg/guestagent/procnettcp/procnettcp_linux.go b/pkg/guestagent/procnettcp/procnettcp_linux.go
index d1c632ee..ae39224f 100644
--- a/pkg/guestagent/procnettcp/procnettcp_linux.go
+++ b/pkg/guestagent/procnettcp/procnettcp_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package procnettcp
 
 import (
diff --git a/pkg/guestagent/serialport/serialconn_linux.go b/pkg/guestagent/serialport/serialconn_linux.go
index 73fffceb..dc3211ec 100644
--- a/pkg/guestagent/serialport/serialconn_linux.go
+++ b/pkg/guestagent/serialport/serialconn_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package serialport
 
 import (
diff --git a/pkg/guestagent/serialport/seriallistener_linux.go b/pkg/guestagent/serialport/seriallistener_linux.go
index 1a2ec617..ad33482f 100644
--- a/pkg/guestagent/serialport/seriallistener_linux.go
+++ b/pkg/guestagent/serialport/seriallistener_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package serialport
 
 import (
diff --git a/pkg/guestagent/serialport/serialport_linux.go b/pkg/guestagent/serialport/serialport_linux.go
index 63b4c303..53fb28b4 100644
--- a/pkg/guestagent/serialport/serialport_linux.go
+++ b/pkg/guestagent/serialport/serialport_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package serialport
 
 import (
diff --git a/pkg/guestagent/timesync/timesync_linux.go b/pkg/guestagent/timesync/timesync_linux.go
index 38f88fc8..af613006 100644
--- a/pkg/guestagent/timesync/timesync_linux.go
+++ b/pkg/guestagent/timesync/timesync_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package timesync
 
 import (
diff --git a/pkg/hostagent/api/api.go b/pkg/hostagent/api/api.go
index 09b07915..f86de58e 100644
--- a/pkg/hostagent/api/api.go
+++ b/pkg/hostagent/api/api.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package api
 
 type Info struct {
diff --git a/pkg/hostagent/api/client/client.go b/pkg/hostagent/api/client/client.go
index 5fbf9d3d..118c5290 100644
--- a/pkg/hostagent/api/client/client.go
+++ b/pkg/hostagent/api/client/client.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package client
 
 // Forked from https://github.com/rootless-containers/rootlesskit/blob/v0.14.2/pkg/api/client/client.go
diff --git a/pkg/hostagent/api/server/server.go b/pkg/hostagent/api/server/server.go
index bf3c5e97..b75c527f 100644
--- a/pkg/hostagent/api/server/server.go
+++ b/pkg/hostagent/api/server/server.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package server
 
 import (
diff --git a/pkg/hostagent/dns/dns.go b/pkg/hostagent/dns/dns.go
index 10454905..e6d310a4 100644
--- a/pkg/hostagent/dns/dns.go
+++ b/pkg/hostagent/dns/dns.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // This file has been adapted from https://github.com/norouter/norouter/blob/v0.6.4/pkg/agent/dns/dns.go
 
 package dns
diff --git a/pkg/hostagent/events/events.go b/pkg/hostagent/events/events.go
index f7d6b4a1..4752d980 100644
--- a/pkg/hostagent/events/events.go
+++ b/pkg/hostagent/events/events.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package events
 
 import (
diff --git a/pkg/hostagent/events/watcher.go b/pkg/hostagent/events/watcher.go
index 0ff8f2d7..8d1fca47 100644
--- a/pkg/hostagent/events/watcher.go
+++ b/pkg/hostagent/events/watcher.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package events
 
 import (
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 396401a4..248e76bd 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
diff --git a/pkg/hostagent/hostagent_unix.go b/pkg/hostagent/hostagent_unix.go
index 68d8dd66..dfc5ee31 100644
--- a/pkg/hostagent/hostagent_unix.go
+++ b/pkg/hostagent/hostagent_unix.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
diff --git a/pkg/hostagent/hostagent_windows.go b/pkg/hostagent/hostagent_windows.go
index c5bec8b3..5b91c2cc 100644
--- a/pkg/hostagent/hostagent_windows.go
+++ b/pkg/hostagent/hostagent_windows.go
@@ -1,4 +1,5 @@
-//go:build windows
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
 
 package hostagent
 
diff --git a/pkg/hostagent/inotify.go b/pkg/hostagent/inotify.go
index e24f1a9b..374e3408 100644
--- a/pkg/hostagent/inotify.go
+++ b/pkg/hostagent/inotify.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
@@ -8,7 +11,6 @@ import (
 	"strings"
 
 	guestagentapi "github.com/lima-vm/lima/pkg/guestagent/api"
-	"github.com/lima-vm/lima/pkg/localpathutil"
 	"github.com/rjeczalik/notify"
 	"github.com/sirupsen/logrus"
 	"google.golang.org/protobuf/types/known/timestamppb"
@@ -71,20 +73,16 @@ func (a *HostAgent) setupWatchers(events chan notify.EventInfo) error {
 		if !*m.Writable {
 			continue
 		}
-		location, err := localpathutil.Expand(m.Location)
-		if err != nil {
-			return err
-		}
-		symlink, err := filepath.EvalSymlinks(location)
+		symlink, err := filepath.EvalSymlinks(m.Location)
 		if err != nil {
 			return err
 		}
-		if location != symlink {
-			mountSymlinks[symlink] = location
+		if m.Location != symlink {
+			mountSymlinks[symlink] = m.Location
 		}
 
-		logrus.Infof("enable inotify for writable mount: %s", location)
-		err = notify.Watch(path.Join(location, "..."), events, GetNotifyEvent())
+		logrus.Infof("enable inotify for writable mount: %s", m.Location)
+		err = notify.Watch(path.Join(m.Location, "..."), events, GetNotifyEvent())
 		if err != nil {
 			return err
 		}
diff --git a/pkg/hostagent/inotify_darwin.go b/pkg/hostagent/inotify_darwin.go
index 77a1a675..821cb805 100644
--- a/pkg/hostagent/inotify_darwin.go
+++ b/pkg/hostagent/inotify_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import "github.com/rjeczalik/notify"
diff --git a/pkg/hostagent/inotify_linux.go b/pkg/hostagent/inotify_linux.go
index dca99f4f..ff22e4d1 100644
--- a/pkg/hostagent/inotify_linux.go
+++ b/pkg/hostagent/inotify_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import "github.com/rjeczalik/notify"
diff --git a/pkg/hostagent/inotify_others.go b/pkg/hostagent/inotify_others.go
index c954b328..e3911b06 100644
--- a/pkg/hostagent/inotify_others.go
+++ b/pkg/hostagent/inotify_others.go
@@ -1,5 +1,8 @@
 //go:build !darwin && !linux
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import "github.com/rjeczalik/notify"
diff --git a/pkg/hostagent/mount.go b/pkg/hostagent/mount.go
index 646dc5d4..609fa5f6 100644
--- a/pkg/hostagent/mount.go
+++ b/pkg/hostagent/mount.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
@@ -6,7 +9,6 @@ import (
 	"os"
 
 	"github.com/lima-vm/lima/pkg/limayaml"
-	"github.com/lima-vm/lima/pkg/localpathutil"
 	"github.com/lima-vm/sshocker/pkg/reversesshfs"
 	"github.com/sirupsen/logrus"
 )
@@ -32,16 +34,7 @@ func (a *HostAgent) setupMounts() ([]*mount, error) {
 }
 
 func (a *HostAgent) setupMount(m limayaml.Mount) (*mount, error) {
-	location, err := localpathutil.Expand(m.Location)
-	if err != nil {
-		return nil, err
-	}
-
-	mountPoint, err := localpathutil.Expand(*m.MountPoint)
-	if err != nil {
-		return nil, err
-	}
-	if err := os.MkdirAll(location, 0o755); err != nil {
+	if err := os.MkdirAll(m.Location, 0o755); err != nil {
 		return nil, err
 	}
 	// NOTE: allow_other requires "user_allow_other" in /etc/fuse.conf
@@ -52,35 +45,35 @@ func (a *HostAgent) setupMount(m limayaml.Mount) (*mount, error) {
 	if *m.SSHFS.FollowSymlinks {
 		sshfsOptions += ",follow_symlinks"
 	}
-	logrus.Infof("Mounting %q on %q", location, mountPoint)
+	logrus.Infof("Mounting %q on %q", m.Location, *m.MountPoint)
 
 	rsf := &reversesshfs.ReverseSSHFS{
 		Driver:              *m.SSHFS.SFTPDriver,
 		SSHConfig:           a.sshConfig,
-		LocalPath:           location,
+		LocalPath:           m.Location,
 		Host:                "127.0.0.1",
 		Port:                a.sshLocalPort,
-		RemotePath:          mountPoint,
+		RemotePath:          *m.MountPoint,
 		Readonly:            !(*m.Writable),
 		SSHFSAdditionalArgs: []string{"-o", sshfsOptions},
 	}
 	if err := rsf.Prepare(); err != nil {
-		return nil, fmt.Errorf("failed to prepare reverse sshfs for %q on %q: %w", location, mountPoint, err)
+		return nil, fmt.Errorf("failed to prepare reverse sshfs for %q on %q: %w", m.Location, *m.MountPoint, err)
 	}
 	if err := rsf.Start(); err != nil {
-		logrus.WithError(err).Warnf("failed to mount reverse sshfs for %q on %q, retrying with `-o nonempty`", location, mountPoint)
+		logrus.WithError(err).Warnf("failed to mount reverse sshfs for %q on %q, retrying with `-o nonempty`", m.Location, *m.MountPoint)
 		// NOTE: nonempty is not supported for libfuse3: https://github.com/canonical/multipass/issues/1381
 		rsf.SSHFSAdditionalArgs = []string{"-o", "nonempty"}
 		if err := rsf.Start(); err != nil {
-			return nil, fmt.Errorf("failed to mount reverse sshfs for %q on %q: %w", location, mountPoint, err)
+			return nil, fmt.Errorf("failed to mount reverse sshfs for %q on %q: %w", m.Location, *m.MountPoint, err)
 		}
 	}
 
 	res := &mount{
 		close: func() error {
-			logrus.Infof("Unmounting %q", location)
-			if closeErr := rsf.Close(); closeErr != nil {
-				return fmt.Errorf("failed to unmount reverse sshfs for %q on %q: %w", location, mountPoint, err)
+			logrus.Infof("Unmounting %q", m.Location)
+			if err := rsf.Close(); err != nil {
+				return fmt.Errorf("failed to unmount reverse sshfs for %q on %q: %w", m.Location, *m.MountPoint, err)
 			}
 			return nil
 		},
diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go
index bfb86d1c..040d7594 100644
--- a/pkg/hostagent/port.go
+++ b/pkg/hostagent/port.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
diff --git a/pkg/hostagent/port_darwin.go b/pkg/hostagent/port_darwin.go
index a99897b6..5ac63b41 100644
--- a/pkg/hostagent/port_darwin.go
+++ b/pkg/hostagent/port_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
diff --git a/pkg/hostagent/port_others.go b/pkg/hostagent/port_others.go
index c038e6c3..8d218c25 100644
--- a/pkg/hostagent/port_others.go
+++ b/pkg/hostagent/port_others.go
@@ -1,5 +1,8 @@
 //go:build !darwin && !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
diff --git a/pkg/hostagent/port_windows.go b/pkg/hostagent/port_windows.go
index 6a3d74e1..d8d19f0c 100644
--- a/pkg/hostagent/port_windows.go
+++ b/pkg/hostagent/port_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
diff --git a/pkg/hostagent/requirements.go b/pkg/hostagent/requirements.go
index 00908655..42db08fe 100644
--- a/pkg/hostagent/requirements.go
+++ b/pkg/hostagent/requirements.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package hostagent
 
 import (
diff --git a/pkg/httpclientutil/httpclientutil.go b/pkg/httpclientutil/httpclientutil.go
index 59854545..f2092d91 100644
--- a/pkg/httpclientutil/httpclientutil.go
+++ b/pkg/httpclientutil/httpclientutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package httpclientutil
 
 // Forked from https://github.com/rootless-containers/rootlesskit/blob/v0.14.2/pkg/api/client/client.go
diff --git a/pkg/httpclientutil/httpclientutil_others.go b/pkg/httpclientutil/httpclientutil_others.go
index e3b4315f..3baf2c59 100644
--- a/pkg/httpclientutil/httpclientutil_others.go
+++ b/pkg/httpclientutil/httpclientutil_others.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package httpclientutil
 
 import (
diff --git a/pkg/httpclientutil/httpclientutil_windows.go b/pkg/httpclientutil/httpclientutil_windows.go
index c6959744..cdd23415 100644
--- a/pkg/httpclientutil/httpclientutil_windows.go
+++ b/pkg/httpclientutil/httpclientutil_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package httpclientutil
 
 import (
diff --git a/pkg/httputil/httputil.go b/pkg/httputil/httputil.go
index 32f2cdb1..b5d073d6 100644
--- a/pkg/httputil/httputil.go
+++ b/pkg/httputil/httputil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package httputil
 
 // ErrorJSON is returned with "application/json" content type and non-2XX status code.
diff --git a/pkg/identifierutil/identifierutil.go b/pkg/identifierutil/identifierutil.go
index e9a23f0d..8f950fce 100644
--- a/pkg/identifierutil/identifierutil.go
+++ b/pkg/identifierutil/identifierutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package identifierutil
 
 import "strings"
diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go
index 19884e3b..c3c2469b 100644
--- a/pkg/infoutil/infoutil.go
+++ b/pkg/infoutil/infoutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package infoutil
 
 import (
diff --git a/pkg/instance/ansible.go b/pkg/instance/ansible.go
index 28d40085..ad424415 100644
--- a/pkg/instance/ansible.go
+++ b/pkg/instance/ansible.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package instance
 
 import (
diff --git a/pkg/instance/create.go b/pkg/instance/create.go
index 4941efbc..b9088785 100644
--- a/pkg/instance/create.go
+++ b/pkg/instance/create.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package instance
 
 import (
diff --git a/pkg/instance/delete.go b/pkg/instance/delete.go
index 41593626..c4078d48 100644
--- a/pkg/instance/delete.go
+++ b/pkg/instance/delete.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package instance
 
 import (
@@ -21,10 +24,11 @@ func Delete(ctx context.Context, inst *store.Instance, force bool) error {
 
 	StopForcibly(inst)
 
-	if err := unregister(ctx, inst); err != nil {
-		return fmt.Errorf("failed to unregister %q: %w", inst.Dir, err)
+	if len(inst.Errors) == 0 {
+		if err := unregister(ctx, inst); err != nil {
+			return fmt.Errorf("failed to unregister %q: %w", inst.Dir, err)
+		}
 	}
-
 	if err := os.RemoveAll(inst.Dir); err != nil {
 		return fmt.Errorf("failed to remove %q: %w", inst.Dir, err)
 	}
diff --git a/pkg/instance/start.go b/pkg/instance/start.go
index 80b4dc44..b53de081 100644
--- a/pkg/instance/start.go
+++ b/pkg/instance/start.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package instance
 
 import (
diff --git a/pkg/instance/stop.go b/pkg/instance/stop.go
index ba42aa88..b98eb806 100644
--- a/pkg/instance/stop.go
+++ b/pkg/instance/stop.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package instance
 
 import (
diff --git a/pkg/ioutilx/ioutilx.go b/pkg/ioutilx/ioutilx.go
index efee8203..cd91e40c 100644
--- a/pkg/ioutilx/ioutilx.go
+++ b/pkg/ioutilx/ioutilx.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package ioutilx
 
 import (
@@ -5,6 +8,7 @@ import (
 	"fmt"
 	"io"
 	"os/exec"
+	"path/filepath"
 	"strings"
 
 	"github.com/sirupsen/logrus"
@@ -45,13 +49,11 @@ func FromUTF16leToString(r io.Reader) (string, error) {
 	return string(out), nil
 }
 
-func CanonicalWindowsPath(orig string) string {
-	newPath := orig
-	out, err := exec.Command("cygpath", "-m", orig).CombinedOutput()
+func WindowsSubsystemPath(orig string) (string, error) {
+	out, err := exec.Command("cygpath", filepath.ToSlash(orig)).CombinedOutput()
 	if err != nil {
 		logrus.WithError(err).Errorf("failed to convert path to mingw, maybe not using Git ssh?")
-	} else {
-		newPath = strings.TrimSpace(string(out))
+		return orig, err
 	}
-	return newPath
+	return strings.TrimSpace(string(out)), nil
 }
diff --git a/pkg/iso9660util/iso9660util.go b/pkg/iso9660util/iso9660util.go
index 149b3d41..45b82982 100644
--- a/pkg/iso9660util/iso9660util.go
+++ b/pkg/iso9660util/iso9660util.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package iso9660util
 
 import (
diff --git a/pkg/limatmpl/abs.go b/pkg/limatmpl/abs.go
new file mode 100644
index 00000000..2598b377
--- /dev/null
+++ b/pkg/limatmpl/abs.go
@@ -0,0 +1,131 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package limatmpl
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+	"path"
+	"path/filepath"
+	"runtime"
+	"strings"
+)
+
+// UseAbsLocators will replace all relative template locators with absolute ones, so this template
+// can be stored anywhere and still reference the same base templates and files.
+func (tmpl *Template) UseAbsLocators() error {
+	err := tmpl.useAbsLocators()
+	return tmpl.ClearOnError(err)
+}
+
+func (tmpl *Template) useAbsLocators() error {
+	if err := tmpl.Unmarshal(); err != nil {
+		return err
+	}
+	basePath, err := basePath(tmpl.Locator)
+	if err != nil {
+		return err
+	}
+	for i, baseLocator := range tmpl.Config.Base {
+		absLocator, err := absPath(baseLocator.URL, basePath)
+		if err != nil {
+			return err
+		}
+		if i == 0 {
+			// base can be either a single string (URL), or a single locator object, or a list whose first element can be either a string or an object
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.base | select(type == \"!!str\")) |= %q\n", absLocator))
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.base | select(type == \"!!map\") | .url) |= %q\n", absLocator))
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.base | select(type == \"!!seq\" and (.[0] | type) == \"!!str\") | .[0]) |= %q\n", absLocator))
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.base | select(type == \"!!seq\" and (.[0] | type) == \"!!map\") | .[0].url) |= %q\n", absLocator))
+		} else {
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.base[%d] | select(type == \"!!str\")) |= %q\n", i, absLocator))
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.base[%d] | select(type == \"!!map\") | .url) |= %q\n", i, absLocator))
+		}
+	}
+	for i, p := range tmpl.Config.Probes {
+		if p.File != nil {
+			absLocator, err := absPath(p.File.URL, basePath)
+			if err != nil {
+				return err
+			}
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.probes[%d].file | select(type == \"!!str\")) = %q\n", i, absLocator))
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.probes[%d].file | select(type == \"!!map\") | .url) = %q\n", i, absLocator))
+		}
+	}
+	for i, p := range tmpl.Config.Provision {
+		if p.File != nil {
+			absLocator, err := absPath(p.File.URL, basePath)
+			if err != nil {
+				return err
+			}
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.provision[%d].file | select(type == \"!!str\")) = %q\n", i, absLocator))
+			tmpl.expr.WriteString(fmt.Sprintf("| ($a.provision[%d].file | select(type == \"!!map\") | .url) = %q\n", i, absLocator))
+		}
+	}
+	return tmpl.evalExpr()
+}
+
+// withVolume adds the volume name of the current working directory to a path without volume name.
+// On Windows filepath.Abs() only returns a "rooted" name, but does not add the volume name.
+// withVolume also normalizes all path separators to the platform native one.
+func withVolume(path string) (string, error) {
+	if runtime.GOOS == "windows" && filepath.VolumeName(path) == "" {
+		root, err := filepath.Abs("/")
+		if err != nil {
+			return "", err
+		}
+		path = filepath.VolumeName(root) + path
+	}
+	return filepath.Clean(path), nil
+}
+
+// basePath returns the locator in absolute format, but without the filename part.
+func basePath(locator string) (string, error) {
+	u, err := url.Parse(locator)
+	// Single-letter schemes will be drive names on Windows, e.g. "c:/foo"
+	if err == nil && len(u.Scheme) > 1 {
+		// path.Dir("") returns ".", which must be removed for url.JoinPath() to do the right thing later
+		return u.Scheme + "://" + strings.TrimSuffix(path.Dir(path.Join(u.Host, u.Path)), "."), nil
+	}
+	base, err := filepath.Abs(filepath.Dir(locator))
+	if err != nil {
+		return "", err
+	}
+	return withVolume(base)
+}
+
+// absPath either returns the locator directly, or combines it with the basePath if the locator is a relative path.
+func absPath(locator, basePath string) (string, error) {
+	if locator == "" {
+		return "", errors.New("locator is empty")
+	}
+	u, err := url.Parse(locator)
+	if err == nil && len(u.Scheme) > 1 {
+		return locator, nil
+	}
+	// Check for rooted locator; filepath.IsAbs() returns false on Windows when the volume name is missing
+	volumeLen := len(filepath.VolumeName(locator))
+	if locator[volumeLen] != '/' && locator[volumeLen] != filepath.Separator {
+		switch {
+		case basePath == "":
+			return "", errors.New("basePath is empty")
+		case basePath == "-":
+			return "", errors.New("can't use relative paths when reading template from STDIN")
+		case strings.Contains(locator, "../"):
+			return "", fmt.Errorf("relative locator path %q must not contain '../' segments", locator)
+		case volumeLen != 0:
+			return "", fmt.Errorf("relative locator path %q must not include a volume name", locator)
+		}
+		u, err = url.Parse(basePath)
+		if err != nil {
+			return "", err
+		}
+		if len(u.Scheme) > 1 {
+			return u.JoinPath(locator).String(), nil
+		}
+		locator = filepath.Join(basePath, locator)
+	}
+	return withVolume(locator)
+}
diff --git a/pkg/limatmpl/embed.go b/pkg/limatmpl/embed.go
new file mode 100644
index 00000000..e1437b18
--- /dev/null
+++ b/pkg/limatmpl/embed.go
@@ -0,0 +1,612 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package limatmpl
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"slices"
+	"sync"
+
+	"github.com/coreos/go-semver/semver"
+	"github.com/lima-vm/lima/pkg/limayaml"
+	"github.com/lima-vm/lima/pkg/store/dirnames"
+	"github.com/lima-vm/lima/pkg/store/filenames"
+	"github.com/lima-vm/lima/pkg/version/versionutil"
+	"github.com/lima-vm/lima/pkg/yqutil"
+	"github.com/sirupsen/logrus"
+)
+
+var warnBaseIsExperimental = sync.OnceFunc(func() {
+	logrus.Warn("`base` is experimental")
+})
+
+var warnFileIsExperimental = sync.OnceFunc(func() {
+	logrus.Warn("`provision[*].file` and `probes[*].file` are experimental")
+})
+
+// Embed will recursively resolve all "base" dependencies and update the
+// template with the merged result. It also inlines all external provisioning
+// and probe scripts.
+func (tmpl *Template) Embed(ctx context.Context, embedAll, defaultBase bool) error {
+	if err := tmpl.UseAbsLocators(); err != nil {
+		return err
+	}
+	seen := make(map[string]bool)
+	err := tmpl.embedAllBases(ctx, embedAll, defaultBase, seen)
+	// additionalDisks, mounts, and networks may combine entries based on a shared key
+	// This must be done after **all** base templates have been merged, so that wildcard keys can match
+	// against all earlier list entries, and not just against the direct parent template.
+	if err == nil {
+		err = tmpl.combineListEntries()
+	}
+	return tmpl.ClearOnError(err)
+}
+
+func (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase bool, seen map[string]bool) error {
+	logrus.Debugf("Embedding templates into %q", tmpl.Locator)
+	if defaultBase {
+		configDir, err := dirnames.LimaConfigDir()
+		if err != nil {
+			return err
+		}
+		defaultBaseFilename := filepath.Join(configDir, filenames.Base)
+		if _, err := os.Stat(defaultBaseFilename); err == nil {
+			// turn string into single element list
+			// empty concatenation works around bug https://github.com/mikefarah/yq/issues/2269
+			tmpl.expr.WriteString("| ($a.base | select(type == \"!!str\")) |= [\"\" + .]\n")
+			tmpl.expr.WriteString("| ($a.base | select(type == \"!!map\")) |= [[] + .]\n")
+			// prepend base template at the beginning of the list
+			tmpl.expr.WriteString(fmt.Sprintf("| $a.base = [%q, $a.base[]]\n", defaultBaseFilename))
+			if err := tmpl.evalExpr(); err != nil {
+				return err
+			}
+		}
+	}
+	for {
+		if err := tmpl.Unmarshal(); err != nil {
+			return err
+		}
+		if len(tmpl.Config.Base) == 0 {
+			break
+		}
+		baseLocator := tmpl.Config.Base[0]
+		if baseLocator.Digest != nil {
+			return fmt.Errorf("base %q in %q has specified a digest; digest support is not yet implemented", baseLocator.URL, tmpl.Locator)
+		}
+		isTemplate, _ := SeemsTemplateURL(baseLocator.URL)
+		if isTemplate && !embedAll {
+			// Once we skip a template:// URL we can no longer embed any other base template
+			for i := 1; i < len(tmpl.Config.Base); i++ {
+				isTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i].URL)
+				if !isTemplate {
+					return fmt.Errorf("cannot embed template %q after not embedding %q", tmpl.Config.Base[i].URL, baseLocator.URL)
+				}
+			}
+			break
+			// TODO should we track embedding of template:// URLs so we can warn if we embed a non-template:// URL afterwards?
+		}
+
+		if seen[baseLocator.URL] {
+			return fmt.Errorf("base template loop detected: template %q already included", baseLocator.URL)
+		}
+		seen[baseLocator.URL] = true
+
+		// remove base[0] from template before merging
+		if err := tmpl.embedBase(ctx, baseLocator, embedAll, seen); err != nil {
+			return err
+		}
+	}
+	if err := tmpl.embedAllScripts(ctx, embedAll); err != nil {
+		return err
+	}
+	if len(tmpl.Bytes) > yBytesLimit {
+		return fmt.Errorf("template %q embedding exceeded the size limit (%d bytes)", tmpl.Locator, yBytesLimit)
+	}
+	return nil
+}
+
+func (tmpl *Template) embedBase(ctx context.Context, baseLocator limayaml.LocatorWithDigest, embedAll bool, seen map[string]bool) error {
+	warnBaseIsExperimental()
+	logrus.Debugf("Embedding base %q in template %q", baseLocator.URL, tmpl.Locator)
+	if err := tmpl.Unmarshal(); err != nil {
+		return err
+	}
+	base, err := Read(ctx, "", baseLocator.URL)
+	if err != nil {
+		return err
+	}
+	if err := base.UseAbsLocators(); err != nil {
+		return err
+	}
+	if err := base.embedAllBases(ctx, embedAll, false, seen); err != nil {
+		return err
+	}
+	if err := tmpl.merge(base); err != nil {
+		return err
+	}
+	if len(tmpl.Bytes) > yBytesLimit {
+		return fmt.Errorf("template %q embedding exceeded the size limit (%d bytes)", tmpl.Locator, yBytesLimit)
+	}
+	return nil
+}
+
+// evalExprImpl evaluates tmpl.expr against one or more documents.
+// Called by evalExpr() and embedAllScripts() for single documents and merge() for 2 documents.
+func (tmpl *Template) evalExprImpl(prefix string, b []byte) error {
+	var err error
+	expr := prefix + tmpl.expr.String() + "| $a"
+	tmpl.Bytes, err = yqutil.EvaluateExpression(expr, b)
+	// Make sure the YAML ends with just a single newline
+	tmpl.Bytes = append(bytes.TrimRight(tmpl.Bytes, "\n"), '\n')
+	tmpl.Config = nil
+	tmpl.expr.Reset()
+	return tmpl.ClearOnError(err)
+}
+
+// evalExpr evaluates tmpl.expr against the tmpl.Bytes document.
+func (tmpl *Template) evalExpr() error {
+	var err error
+	if tmpl.expr.Len() > 0 {
+		// There is just a single document; $a and $b are the same
+		singleDocument := "select(document_index == 0) as $a | $a as $b\n"
+		err = tmpl.evalExprImpl(singleDocument, tmpl.Bytes)
+	}
+	return err
+}
+
+// merge merges the base template into tmpl.
+func (tmpl *Template) merge(base *Template) error {
+	if err := tmpl.mergeBase(base); err != nil {
+		return tmpl.ClearOnError(err)
+	}
+	documents := fmt.Sprintf("%s\n---\n%s", string(tmpl.Bytes), string(base.Bytes))
+	return tmpl.evalExprImpl(mergeDocuments, []byte(documents))
+}
+
+// mergeBase generates a yq script to merge the template with a base.
+// Most of the merging is done generically by the mergeDocuments script below.
+// Only thing left is to compare minimum version numbers and keep the highest version.
+func (tmpl *Template) mergeBase(base *Template) error {
+	if err := tmpl.Unmarshal(); err != nil {
+		return err
+	}
+	if err := base.Unmarshal(); err != nil {
+		return err
+	}
+	if tmpl.Config.MinimumLimaVersion != nil && base.Config.MinimumLimaVersion != nil {
+		if versionutil.GreaterThan(*base.Config.MinimumLimaVersion, *tmpl.Config.MinimumLimaVersion) {
+			const minimumLimaVersion = "minimumLimaVersion"
+			tmpl.copyField(minimumLimaVersion, minimumLimaVersion)
+		}
+	}
+	if tmpl.Config.VMOpts.QEMU.MinimumVersion != nil && base.Config.VMOpts.QEMU.MinimumVersion != nil {
+		tmplVersion := *semver.New(*tmpl.Config.VMOpts.QEMU.MinimumVersion)
+		baseVersion := *semver.New(*base.Config.VMOpts.QEMU.MinimumVersion)
+		if tmplVersion.LessThan(baseVersion) {
+			const minimumQEMUVersion = "vmOpts.qemu.minimumVersion"
+			tmpl.copyField(minimumQEMUVersion, minimumQEMUVersion)
+		}
+	}
+	return nil
+}
+
+// mergeDocuments copies over settings from the base that don't yet exist
+// in the template, and to append lists from the base to template lists.
+// Both head and line comments are copied over as well.
+//
+// It also handles these special cases:
+// * dns lists are not merged and only copied when the template doesn't have any dns entries at all.
+// * probes and provision scripts are appended in reverse order.
+// * mountTypesUnsupported have duplicate values removed.
+// * base is removed from the template.
+const mergeDocuments = `
+  select(document_index == 0) as $a
+| select(document_index == 1) as $b
+
+# $c will be mutilated to implement our own "merge only new fields" logic.
+| $b as $c
+
+# Delete the base that is being merged right now
+| $a | select(.base | tag == "!!seq") | del(.base[0])
+| $a | select(.base | (tag == "!!seq" and length == 0)) | del(.base)
+| $a | select(.base | tag == "!!str") | del(.base)
+
+# If $a.base is a list, then $b.base must be a list as well
+# (note $b, not $c, because we merge lists from $b)
+| $b | select((.base | tag == "!!str") and ($a.base | tag == "!!seq")) | .base = [ "" + .base ]
+
+# Delete base DNS entries if the template list is not empty.
+| $a | select(.dns) | del($b.dns, $c.dns)
+
+# Mark all new list fields with a custom tag. This is needed to avoid appending
+# newly copied lists to themselves again when we merge lists.
+| $c | .. | select(tag == "!!seq") tag = "!!tag"
+
+# Delete all nodes in $c that are in $a and not a map. This is necessary because
+# the yq "*n" operator (merge only new fields) does not copy all comments across.
+| $c | delpaths([$a | .. | select(tag != "!!map") | path])
+
+# Merging with null returns null; use an empty map if $c has only comments
+| $a * ($c // {}) as $a
+
+# Find all elements that are existing lists. This will not match newly
+# copied lists because they have a custom !!tag instead of !!seq.
+# Append the elements from the same path in $b.
+# Exception: base templates, provision scripts and probes are prepended instead.
+| $a | (.. | select(tag == "!!seq" and (path[0] | test("^(base|provision|probes)$") | not))) |=
+   (. + (path[] as $p ireduce ($b; .[$p])))
+| $a | (.. | select(tag == "!!seq" and (path[0] | test("^(base|provision|probes)$")))) |=
+   ((path[] as $p ireduce ($b; .[$p])) + .)
+
+# Copy head and line comments for existing lists that do not already have comments.
+# New lists and existing maps already have their comments updated by the $a * $c merge.
+| $a | (.. | select(tag == "!!seq" and (key | head_comment == "")) | key) head_comment |=
+   (((path[] as $p ireduce ($b; .[$p])) | key | head_comment) // "")
+| $a | (.. | select(tag == "!!seq" and (key | line_comment == "")) | key) line_comment |=
+   (((path[] as $p ireduce ($b; .[$p])) | key | line_comment) // "")
+
+# Make sure mountTypesUnsupported elements are unique.
+| $a | (select(.mountTypesUnsupported) | .mountTypesUnsupported) |= unique
+
+# Remove the custom tags again so they do not clutter up the YAML output.
+| $a | .. tag = ""
+`
+
+// listFields returns dst and src fields like "list[idx].field".
+func listFields(list string, dstIdx, srcIdx int, field string) (dst, src string) {
+	dst = fmt.Sprintf("%s[%d]", list, dstIdx)
+	src = fmt.Sprintf("%s[%d]", list, srcIdx)
+	if field != "" {
+		dst += "." + field
+		src += "." + field
+	}
+	return dst, src
+}
+
+// copyField copies value and comments from $b.src to $a.dst.
+func (tmpl *Template) copyField(dst, src string) {
+	tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s) = $b.%s\n", dst, src))
+	// The head_comment is on the key and not the value, so needs to be copied explicitly.
+	// Surprisingly the line_comment seems to be copied with the value already even though it is also on the key.
+	tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s | key) head_comment = ($b.%s | key | head_comment)\n", dst, src))
+}
+
+// copyListEntryField copies $b.list[srcIdx].field to $a.list[dstIdx].field (including comments).
+// Note: field must not be "" and must not be a list field itself either.
+func (tmpl *Template) copyListEntryField(list string, dstIdx, srcIdx int, field string) {
+	tmpl.copyField(listFields(list, dstIdx, srcIdx, field))
+}
+
+type commentType string
+
+const (
+	headComment commentType = "head"
+	lineComment commentType = "line"
+)
+
+// copyComment copies a non-empty head or line comment from $b.src to $a.dst, but only if $a.dst already exists.
+func (tmpl *Template) copyComment(dst, src string, commentType commentType, isMapElement bool) {
+	onKey := ""
+	if isMapElement {
+		onKey = " | key" // For map elements the comments are on the keys and not the values.
+	}
+	// The expression is careful not to create a null $a.dst entry if $b.src has no comments and $a.dst didn't already exist.
+	// e.g.: `| $a | (select(.foo) | .foo | key | select(head_comment == "" and ($b.bar | key | head_comment != ""))) head_comment |= ($b.bar | key | head_comment)`
+	tmpl.expr.WriteString(fmt.Sprintf("| $a | (select(.%s) | .%s%s | select(%s_comment == \"\" and ($b.%s%s | %s_comment != \"\"))) %s_comment |= ($b.%s%s | %s_comment)\n",
+		dst, dst, onKey, commentType, src, onKey, commentType, commentType, src, onKey, commentType))
+}
+
+// copyComments copies all non-empty comments from $b.src to $a.dst.
+func (tmpl *Template) copyComments(dst, src string, isMapElement bool) {
+	for _, commentType := range []commentType{headComment, lineComment} {
+		tmpl.copyComment(dst, src, commentType, isMapElement)
+	}
+}
+
+// copyListEntryComments copies all non-empty comments from $b.list[srcIdx].field to $a.list[dstIdx].field.
+func (tmpl *Template) copyListEntryComments(list string, dstIdx, srcIdx int, field string) {
+	dst, src := listFields(list, dstIdx, srcIdx, field)
+	isMapElement := field != ""
+	tmpl.copyComments(dst, src, isMapElement)
+}
+
+func (tmpl *Template) deleteListEntry(list string, idx int) {
+	tmpl.expr.WriteString(fmt.Sprintf("| del($a.%s[%d], $b.%s[%d])\n", list, idx, list, idx))
+}
+
+// upgradeListEntryStringToMapField turns list[idx] from a string to a {field: list[idx]} map.
+func (tmpl *Template) upgradeListEntryStringToMapField(list string, idx int, field string) {
+	// TODO the head_comment on the string becomes duplicated as a foot_comment on the new field; could be a yq bug?
+	tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s[%d] | select(type == \"!!str\")) |= {\"%s\": .}\n", list, idx, field))
+}
+
+// combineListEntries combines entries based on a shared unique key.
+// If two entries share the same key, then any missing fields in the earlier entry are
+// filled in from the latter one. The latter one is then deleted.
+//
+// Notes:
+// * The field order is not maintained when entries with a matching key are merged.
+// * The unique keys (and mount locations) are assumed to not be subject to Go templating.
+// * A wildcard key '*' matches all prior list entries.
+func (tmpl *Template) combineListEntries() error {
+	if err := tmpl.Unmarshal(); err != nil {
+		return err
+	}
+
+	tmpl.combineAdditionalDisks()
+	tmpl.combineMounts()
+	tmpl.combineNetworks()
+
+	return tmpl.evalExpr()
+}
+
+// TODO: Maybe instead of hard-coding all the yaml names of LimaYAML struct fields we should
+// TODO: retrieve them using reflection from the Go type tags to avoid possible typos.
+
+// combineAdditionalDisks combines additionalDisks entries. The shared key is the disk name.
+func (tmpl *Template) combineAdditionalDisks() {
+	const additionalDisks = "additionalDisks"
+
+	diskIdx := make(map[string]int, len(tmpl.Config.AdditionalDisks))
+	for src := 0; src < len(tmpl.Config.AdditionalDisks); {
+		disk := tmpl.Config.AdditionalDisks[src]
+		var from, to int
+		if disk.Name == "*" {
+			// copy to **all** previous entries
+			from = 0
+			to = src - 1
+		} else {
+			if i, ok := diskIdx[disk.Name]; ok {
+				// copy to previous disk with the same diskIdx
+				from = i
+				to = i
+			} else {
+				// record disk index and continue with the next entry
+				if disk.Name != "" {
+					diskIdx[disk.Name] = src
+				}
+				src++
+				continue
+			}
+		}
+		for dst := from; dst <= to; dst++ {
+			// upgrade additionalDisks[dst] from "disk" name string to {"name": "disk"} map so we can add fields
+			upgradeDiskToMap := sync.OnceFunc(func() {
+				tmpl.upgradeListEntryStringToMapField(additionalDisks, dst, "name")
+			})
+
+			dest := &tmpl.Config.AdditionalDisks[dst]
+			if dest.Format == nil && disk.Format != nil {
+				upgradeDiskToMap()
+				tmpl.copyListEntryField(additionalDisks, dst, src, "format")
+				dest.Format = disk.Format
+			}
+			// TODO: Does it make sense to merge "fsType" and "fsArgs" independently of each other?
+			if dest.FSType == nil && disk.FSType != nil {
+				upgradeDiskToMap()
+				tmpl.copyListEntryField(additionalDisks, dst, src, "fsType")
+				dest.FSType = disk.FSType
+			}
+			// "fsArgs" are inherited all-or-nothing; they are not appended
+			if len(dest.FSArgs) == 0 && len(disk.FSArgs) != 0 {
+				upgradeDiskToMap()
+				tmpl.copyListEntryField(additionalDisks, dst, src, "fsArgs")
+				dest.FSArgs = disk.FSArgs
+			}
+			// TODO: Is there a good reason not to copy comments from wildcard entries?
+			if disk.Name != "*" {
+				tmpl.copyListEntryComments(additionalDisks, dst, src, "")
+			}
+		}
+		tmpl.Config.AdditionalDisks = slices.Delete(tmpl.Config.AdditionalDisks, src, src+1)
+		tmpl.deleteListEntry(additionalDisks, src)
+	}
+}
+
+// combineMounts combines mounts entries. The shared key is the mount point.
+func (tmpl *Template) combineMounts() {
+	const mounts = "mounts"
+
+	mountPointIdx := make(map[string]int, len(tmpl.Config.Mounts))
+	for src := 0; src < len(tmpl.Config.Mounts); {
+		mount := tmpl.Config.Mounts[src]
+		// mountPoint (an optional field) defaults to location (a required field)
+		mountPoint := mount.Location
+		if mount.MountPoint != nil {
+			mountPoint = *mount.MountPoint
+		}
+		var from, to int
+		if mountPoint == "*" {
+			from = 0
+			to = src - 1
+		} else {
+			if i, ok := mountPointIdx[mountPoint]; ok {
+				from = i
+				to = i
+			} else {
+				if mountPoint != "" {
+					mountPointIdx[mountPoint] = src
+				}
+				src++
+				continue
+			}
+		}
+		for dst := from; dst <= to; dst++ {
+			dest := &tmpl.Config.Mounts[dst]
+			// MountPoint
+			if dest.MountPoint == nil && mount.MountPoint != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "mountPoint")
+				dest.MountPoint = mount.MountPoint
+			}
+			// Writable
+			if dest.Writable == nil && mount.Writable != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "writable")
+				dest.Writable = mount.Writable
+			}
+			// SSHFS
+			if dest.SSHFS.Cache == nil && mount.SSHFS.Cache != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "sshfs.cache")
+				dest.SSHFS.Cache = mount.SSHFS.Cache
+			}
+			if dest.SSHFS.FollowSymlinks == nil && mount.SSHFS.FollowSymlinks != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "sshfs.followSymlinks")
+				dest.SSHFS.FollowSymlinks = mount.SSHFS.FollowSymlinks
+			}
+			if dest.SSHFS.SFTPDriver == nil && mount.SSHFS.SFTPDriver != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "sshfs.sftpDriver")
+				dest.SSHFS.SFTPDriver = mount.SSHFS.SFTPDriver
+			}
+			// NineP
+			if dest.NineP.SecurityModel == nil && mount.NineP.SecurityModel != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "9p.securityModel")
+				dest.NineP.SecurityModel = mount.NineP.SecurityModel
+			}
+			if dest.NineP.ProtocolVersion == nil && mount.NineP.ProtocolVersion != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "9p.protocolVersion")
+				dest.NineP.ProtocolVersion = mount.NineP.ProtocolVersion
+			}
+			if dest.NineP.Msize == nil && mount.NineP.Msize != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "9p.msize")
+				dest.NineP.Msize = mount.NineP.Msize
+			}
+			if dest.NineP.Cache == nil && mount.NineP.Cache != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "9p.cache")
+				dest.NineP.Cache = mount.NineP.Cache
+			}
+			// Virtiofs
+			if dest.Virtiofs.QueueSize == nil && mount.Virtiofs.QueueSize != nil {
+				tmpl.copyListEntryField(mounts, dst, src, "virtiofs.queueSize")
+				dest.Virtiofs.QueueSize = mount.Virtiofs.QueueSize
+			}
+			if mountPoint != "*" {
+				tmpl.copyListEntryComments(mounts, dst, src, "")
+				tmpl.copyListEntryComments(mounts, dst, src, "sshfs")
+				tmpl.copyListEntryComments(mounts, dst, src, "9p")
+				tmpl.copyListEntryComments(mounts, dst, src, "virtiofs")
+			}
+		}
+		tmpl.Config.Mounts = slices.Delete(tmpl.Config.Mounts, src, src+1)
+		tmpl.deleteListEntry(mounts, src)
+	}
+}
+
+// combineNetworks combines networks entries. The shared key is the interface name.
+func (tmpl *Template) combineNetworks() {
+	const networks = "networks"
+
+	interfaceIdx := make(map[string]int, len(tmpl.Config.Networks))
+	for src := 0; src < len(tmpl.Config.Networks); {
+		nw := tmpl.Config.Networks[src]
+		var from, to int
+		if nw.Interface == "*" {
+			from = 0
+			to = src - 1
+		} else {
+			if i, ok := interfaceIdx[nw.Interface]; ok {
+				from = i
+				to = i
+			} else {
+				if nw.Interface != "" {
+					interfaceIdx[nw.Interface] = src
+				}
+				src++
+				continue
+			}
+		}
+		for dst := from; dst <= to; dst++ {
+			dest := &tmpl.Config.Networks[dst]
+			// Lima and Socket are mutually exclusive. Only copy base values if both are still unset.
+			if dest.Lima == "" && dest.Socket == "" {
+				if nw.Lima != "" {
+					tmpl.copyListEntryField(networks, dst, src, "lima")
+					dest.Lima = nw.Lima
+				}
+				if nw.Socket != "" {
+					tmpl.copyListEntryField(networks, dst, src, "socket")
+					dest.Socket = nw.Socket
+				}
+			}
+			if dest.MACAddress == "" && nw.MACAddress != "" {
+				tmpl.copyListEntryField(networks, dst, src, "macAddress")
+				dest.MACAddress = nw.MACAddress
+			}
+			if dest.VZNAT == nil && nw.VZNAT != nil {
+				tmpl.copyListEntryField(networks, dst, src, "vzNAT")
+				dest.VZNAT = nw.VZNAT
+			}
+			if dest.Metric == nil && nw.Metric != nil {
+				tmpl.copyListEntryField(networks, dst, src, "metric")
+				dest.Metric = nw.Metric
+			}
+			if nw.Interface != "*" {
+				tmpl.copyListEntryComments(networks, dst, src, "")
+			}
+		}
+		tmpl.Config.Networks = slices.Delete(tmpl.Config.Networks, src, src+1)
+		tmpl.deleteListEntry(networks, src)
+	}
+}
+
+// updateScript replaces a "file" property with the actual script and then renames the field to newName ("script" or "content").
+func (tmpl *Template) updateScript(field string, idx int, newName, script string) {
+	entry := fmt.Sprintf("$a.%s[%d].file", field, idx)
+	// Assign script to the "file" field and then rename it to "script".
+	tmpl.expr.WriteString(fmt.Sprintf("| (%s) = %q | (%s | key) = %q\n", entry, script, entry, newName))
+}
+
+// embedAllScripts replaces all "provision" and "probes" file references with the actual script.
+func (tmpl *Template) embedAllScripts(ctx context.Context, embedAll bool) error {
+	if err := tmpl.Unmarshal(); err != nil {
+		return err
+	}
+	for i, p := range tmpl.Config.Probes {
+		if p.File == nil {
+			continue
+		}
+		warnFileIsExperimental()
+		// Don't overwrite existing script. This should throw an error during validation.
+		if p.Script != "" {
+			continue
+		}
+		isTemplate, _ := SeemsTemplateURL(p.File.URL)
+		if embedAll || !isTemplate {
+			scriptTmpl, err := Read(ctx, "", p.File.URL)
+			if err != nil {
+				return err
+			}
+			tmpl.updateScript("probes", i, "script", string(scriptTmpl.Bytes))
+		}
+	}
+	for i, p := range tmpl.Config.Provision {
+		if p.File == nil {
+			continue
+		}
+		warnFileIsExperimental()
+		newName := "script"
+		switch p.Mode {
+		case limayaml.ProvisionModeData:
+			newName = "content"
+			if p.Content != nil {
+				continue
+			}
+		default:
+			if p.Script != "" {
+				continue
+			}
+		}
+		isTemplate, _ := SeemsTemplateURL(p.File.URL)
+		if embedAll || !isTemplate {
+			scriptTmpl, err := Read(ctx, "", p.File.URL)
+			if err != nil {
+				return err
+			}
+			tmpl.updateScript("provision", i, newName, string(scriptTmpl.Bytes))
+		}
+	}
+	return tmpl.evalExpr()
+}
diff --git a/pkg/limatmpl/locator.go b/pkg/limatmpl/locator.go
index ddb9b61b..5afb7cb6 100644
--- a/pkg/limatmpl/locator.go
+++ b/pkg/limatmpl/locator.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limatmpl
 
 import (
@@ -9,30 +12,31 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"regexp"
+	"runtime"
 	"strings"
+	"unicode"
 
 	"github.com/containerd/containerd/identifiers"
 	"github.com/lima-vm/lima/pkg/ioutilx"
+	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/templatestore"
 	"github.com/sirupsen/logrus"
 )
 
-type Template struct {
-	Name    string
-	Locator string
-	Bytes   []byte
-}
-
 const yBytesLimit = 4 * 1024 * 1024 // 4MiB
 
 func Read(ctx context.Context, name, locator string) (*Template, error) {
 	var err error
-
 	tmpl := &Template{
 		Name:    name,
 		Locator: locator,
 	}
 
+	if imageTemplate(tmpl, locator) {
+		return tmpl, nil
+	}
+
 	isTemplateURL, templateURL := SeemsTemplateURL(locator)
 	switch {
 	case isTemplateURL:
@@ -40,8 +44,11 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 		templateName := filepath.Join(templateURL.Host, templateURL.Path)
 		logrus.Debugf("interpreting argument %q as a template name %q", locator, templateName)
 		if tmpl.Name == "" {
-			// e.g., templateName = "deprecated/centos-7" , tmpl.Name = "centos-7"
-			tmpl.Name = filepath.Base(templateName)
+			// e.g., templateName = "deprecated/centos-7.yaml" , tmpl.Name = "centos-7"
+			tmpl.Name, err = InstNameFromYAMLPath(templateName)
+			if err != nil {
+				return nil, err
+			}
 		}
 		tmpl.Bytes, err = templatestore.Read(templateName)
 		if err != nil {
@@ -75,8 +82,12 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 				return nil, err
 			}
 		}
-		logrus.Debugf("interpreting argument %q as a file url for instance %q", locator, tmpl.Name)
-		r, err := os.Open(strings.TrimPrefix(locator, "file://"))
+		logrus.Debugf("interpreting argument %q as a file URL for instance %q", locator, tmpl.Name)
+		filePath := strings.TrimPrefix(locator, "file://")
+		if !filepath.IsAbs(filePath) {
+			return nil, fmt.Errorf("file URL %q is not an absolute path", locator)
+		}
+		r, err := os.Open(filePath)
 		if err != nil {
 			return nil, err
 		}
@@ -85,7 +96,7 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 		if err != nil {
 			return nil, err
 		}
-	case SeemsYAMLPath(locator):
+	case SeemsFilePath(locator):
 		if tmpl.Name == "" {
 			tmpl.Name, err = InstNameFromYAMLPath(locator)
 			if err != nil {
@@ -93,6 +104,10 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 			}
 		}
 		logrus.Debugf("interpreting argument %q as a file path for instance %q", locator, tmpl.Name)
+		if locator, err = filepath.Abs(locator); err != nil {
+			return nil, err
+		}
+		tmpl.Locator = locator
 		r, err := os.Open(locator)
 		if err != nil {
 			return nil, err
@@ -108,9 +123,102 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
 			return nil, fmt.Errorf("unexpected error reading stdin: %w", err)
 		}
 	}
+	// The only reason not to call tmpl.UseAbsLocators() here is that `limactl tmpl copy --verbatim …`
+	// should create an unmodified copy of the template.
 	return tmpl, nil
 }
 
+// Locators with an image file format extension, optionally followed by a compression method.
+// This regex is also used to remove the file format suffix from the instance name.
+var imageURLRegex = regexp.MustCompile(`\.(img|qcow2|raw|iso)(\.(gz|xz|bz2|zstd))?$`)
+
+// Image architecture will be guessed based on the presence of arch keywords.
+var archKeywords = map[string]limayaml.Arch{
+	"aarch64": limayaml.AARCH64,
+	"amd64":   limayaml.X8664,
+	"arm64":   limayaml.AARCH64,
+	"armhf":   limayaml.ARMV7L,
+	"armv7l":  limayaml.ARMV7L,
+	"riscv64": limayaml.RISCV64,
+	"x86_64":  limayaml.X8664,
+}
+
+// These generic tags will be stripped from an image name before turning it into an instance name.
+var genericTags = []string{
+	"base",         // Fedora, Rocky
+	"cloud",        // Fedora, openSUSE
+	"cloudimg",     // Ubuntu, Arch
+	"cloudinit",    // Alpine
+	"daily",        // Debian
+	"default",      // Gentoo
+	"generic",      // Fedora
+	"genericcloud", // CentOS, Debian, Rocky, Alma
+	"kvm",          // Oracle
+	"latest",       // Gentoo, CentOS, Rocky, Alma
+	"linux",        // Arch
+	"minimal",      // openSUSE
+	"openstack",    // Gentoo
+	"server",       // Ubuntu
+	"std",          // Alpine-Lima
+	"stream",       // CentOS
+	"uefi",         // Alpine
+	"vm",           // openSUSE
+}
+
+// imageTemplate checks if the locator specifies an image URL.
+// It will create a minimal template with the image URL and arch derived from the image name
+// and also set the default instance name to the image name, but stripped of generic tags.
+func imageTemplate(tmpl *Template, locator string) bool {
+	if !imageURLRegex.MatchString(locator) {
+		return false
+	}
+
+	var imageArch limayaml.Arch
+	for keyword, arch := range archKeywords {
+		pattern := fmt.Sprintf(`\b%s\b`, keyword)
+		if regexp.MustCompile(pattern).MatchString(locator) {
+			imageArch = arch
+			break
+		}
+	}
+	if imageArch == "" {
+		imageArch = limayaml.NewArch(runtime.GOARCH)
+		logrus.Warnf("cannot determine image arch from URL %q; assuming %q", locator, imageArch)
+	}
+	template := `arch: %q
+images:
+- location: %q
+  arch: %q
+`
+	tmpl.Bytes = []byte(fmt.Sprintf(template, imageArch, locator, imageArch))
+	tmpl.Name = InstNameFromImageURL(locator, imageArch)
+	return true
+}
+
+func InstNameFromImageURL(locator, imageArch string) string {
+	// We intentionally call both path.Base and filepath.Base in case we are running on Windows.
+	name := strings.ToLower(filepath.Base(path.Base(locator)))
+	// Remove file format and compression file types
+	name = imageURLRegex.ReplaceAllString(name, "")
+	// The Alpine "nocloud_" prefix does not fit the genericTags pattern
+	name = strings.TrimPrefix(name, "nocloud_")
+	for _, tag := range genericTags {
+		re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, tag))
+		name = re.ReplaceAllString(name, "")
+	}
+	// Remove imageArch as well if it is the native arch
+	if limayaml.IsNativeArch(imageArch) {
+		re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, imageArch))
+		name = re.ReplaceAllString(name, "")
+	}
+	// Remove timestamps from name: 8 digit date, optionally followed by
+	// a delimiter and one or more digits before a word boundary
+	name = regexp.MustCompile(`[-_.]20\d{6}([-_.]\d+)?\b`).ReplaceAllString(name, "")
+	// Normalize archlinux name
+	name = regexp.MustCompile(`^arch\b`).ReplaceAllString(name, "archlinux")
+	return name
+}
+
 func SeemsTemplateURL(arg string) (bool, *url.URL) {
 	u, err := url.Parse(arg)
 	if err != nil {
@@ -146,6 +254,20 @@ func SeemsYAMLPath(arg string) bool {
 	return strings.HasSuffix(lower, ".yml") || strings.HasSuffix(lower, ".yaml")
 }
 
+// SeemsFilePath returns true if arg either contains a path separator or has a file extension that
+// does not start with a digit. `my.yaml` is a file path, `ubuntu-20.10` is not.
+func SeemsFilePath(arg string) bool {
+	// Single-letter schemes will be drive names on Windows, e.g. "c:/foo.yaml"
+	if u, err := url.Parse(arg); err == nil && len(u.Scheme) > 1 {
+		return false
+	}
+	if strings.ContainsRune(arg, '/') || strings.ContainsRune(arg, filepath.Separator) {
+		return true
+	}
+	ext := filepath.Ext(arg)
+	return len(ext) > 1 && !unicode.IsDigit(rune(ext[1]))
+}
+
 func InstNameFromURL(urlStr string) (string, error) {
 	u, err := url.Parse(urlStr)
 	if err != nil {
diff --git a/pkg/limatmpl/template.go b/pkg/limatmpl/template.go
new file mode 100644
index 00000000..b6398a50
--- /dev/null
+++ b/pkg/limatmpl/template.go
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package limatmpl
+
+import (
+	"strings"
+
+	"github.com/lima-vm/lima/pkg/limayaml"
+)
+
+type Template struct {
+	Locator string // template locator (absolute path or URL)
+	Bytes   []byte // file contents
+
+	// The following fields are only used when the template represents a YAML config file.
+	Name   string // instance name, may be inferred from locator
+	Config *limayaml.LimaYAML
+
+	expr strings.Builder // yq expression to update template
+}
+
+func (tmpl *Template) ClearOnError(err error) error {
+	if err != nil {
+		tmpl.Bytes = nil
+		tmpl.Config = nil
+		tmpl.expr.Reset()
+	}
+	return err
+}
+
+// Unmarshal makes sure the tmpl.Config field is set. Any operation that modified
+// tmpl.Bytes is expected to set tmpl.Config back to nil.
+func (tmpl *Template) Unmarshal() error {
+	if tmpl.Config == nil {
+		tmpl.Config = &limayaml.LimaYAML{}
+		if err := limayaml.Unmarshal(tmpl.Bytes, tmpl.Config, tmpl.Locator); err != nil {
+			tmpl.Config = nil
+			return err
+		}
+	}
+	return nil
+}
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 1f1f2a86..177f80a7 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
@@ -26,6 +29,7 @@ import (
 	"golang.org/x/sys/cpu"
 
 	"github.com/lima-vm/lima/pkg/identifierutil"
+	"github.com/lima-vm/lima/pkg/localpathutil"
 	. "github.com/lima-vm/lima/pkg/must"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/osutil"
@@ -284,7 +288,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 	}
 	y.Arch = ptr.Of(ResolveArch(y.Arch))
 
-	y.Images = append(append(o.Images, y.Images...), d.Images...)
+	y.Images = slices.Concat(o.Images, y.Images, d.Images)
 	for i := range y.Images {
 		img := &y.Images[i]
 		if img.Arch == "" {
@@ -352,7 +356,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		y.Disk = ptr.Of(defaultDiskSizeAsString())
 	}
 
-	y.AdditionalDisks = append(append(o.AdditionalDisks, y.AdditionalDisks...), d.AdditionalDisks...)
+	y.AdditionalDisks = slices.Concat(o.AdditionalDisks, y.AdditionalDisks, d.AdditionalDisks)
 
 	if y.Audio.Device == nil {
 		y.Audio.Device = d.Audio.Device
@@ -394,7 +398,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		y.Firmware.LegacyBIOS = ptr.Of(false)
 	}
 
-	y.Firmware.Images = append(append(o.Firmware.Images, y.Firmware.Images...), d.Firmware.Images...)
+	y.Firmware.Images = slices.Concat(o.Firmware.Images, y.Firmware.Images, d.Firmware.Images)
 	for i := range y.Firmware.Images {
 		f := &y.Firmware.Images[i]
 		if f.Arch == "" {
@@ -475,7 +479,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 	}
 	y.HostResolver.Hosts = hosts
 
-	y.Provision = append(append(o.Provision, y.Provision...), d.Provision...)
+	y.Provision = slices.Concat(o.Provision, y.Provision, d.Provision)
 	for i := range y.Provision {
 		provision := &y.Provision[i]
 		if provision.Mode == "" {
@@ -484,10 +488,47 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		if provision.Mode == ProvisionModeDependency && provision.SkipDefaultDependencyResolution == nil {
 			provision.SkipDefaultDependencyResolution = ptr.Of(false)
 		}
-		if out, err := executeGuestTemplate(provision.Script, instDir, y.User, y.Param); err == nil {
-			provision.Script = out.String()
-		} else {
-			logrus.WithError(err).Warnf("Couldn't process provisioning script %q as a template", provision.Script)
+		if provision.Mode == ProvisionModeData {
+			if provision.Content == nil {
+				provision.Content = ptr.Of("")
+			} else {
+				if out, err := executeGuestTemplate(*provision.Content, instDir, y.User, y.Param); err == nil {
+					provision.Content = ptr.Of(out.String())
+				} else {
+					logrus.WithError(err).Warnf("Couldn't process data content %q as a template", *provision.Content)
+				}
+			}
+			if provision.Overwrite == nil {
+				provision.Overwrite = ptr.Of(true)
+			}
+			if provision.Owner == nil {
+				provision.Owner = ptr.Of("root:root")
+			} else {
+				if out, err := executeGuestTemplate(*provision.Owner, instDir, y.User, y.Param); err == nil {
+					provision.Owner = ptr.Of(out.String())
+				} else {
+					logrus.WithError(err).Warnf("Couldn't owner %q as a template", *provision.Owner)
+				}
+			}
+			// Path is required; validation will throw an error when it is nil
+			if provision.Path != nil {
+				if out, err := executeGuestTemplate(*provision.Path, instDir, y.User, y.Param); err == nil {
+					provision.Path = ptr.Of(out.String())
+				} else {
+					logrus.WithError(err).Warnf("Couldn't process path %q as a template", *provision.Path)
+				}
+			}
+			if provision.Permissions == nil {
+				provision.Permissions = ptr.Of("644")
+			}
+		}
+		// TODO Turn Script into a pointer; it is a plain string for historical reasons only
+		if provision.Script != "" {
+			if out, err := executeGuestTemplate(provision.Script, instDir, y.User, y.Param); err == nil {
+				provision.Script = out.String()
+			} else {
+				logrus.WithError(err).Warnf("Couldn't process provisioning script %q as a template", provision.Script)
+			}
 		}
 	}
 
@@ -535,7 +576,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		}
 	}
 
-	y.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), d.Containerd.Archives...)
+	y.Containerd.Archives = slices.Concat(o.Containerd.Archives, y.Containerd.Archives, d.Containerd.Archives)
 	if len(y.Containerd.Archives) == 0 {
 		y.Containerd.Archives = defaultContainerdArchives()
 	}
@@ -546,7 +587,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		}
 	}
 
-	y.Probes = append(append(o.Probes, y.Probes...), d.Probes...)
+	y.Probes = slices.Concat(o.Probes, y.Probes, d.Probes)
 	for i := range y.Probes {
 		probe := &y.Probes[i]
 		if probe.Mode == "" {
@@ -562,13 +603,13 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		}
 	}
 
-	y.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...)
+	y.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, d.PortForwards)
 	for i := range y.PortForwards {
 		FillPortForwardDefaults(&y.PortForwards[i], instDir, y.User, y.Param)
 		// After defaults processing the singular HostPort and GuestPort values should not be used again.
 	}
 
-	y.CopyToHost = append(append(o.CopyToHost, y.CopyToHost...), d.CopyToHost...)
+	y.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, d.CopyToHost)
 	for i := range y.CopyToHost {
 		FillCopyToHostDefaults(&y.CopyToHost[i], instDir, y.User, y.Param)
 	}
@@ -605,7 +646,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 
 	networks := make([]Network, 0, len(d.Networks)+len(y.Networks)+len(o.Networks))
 	iface := make(map[string]int)
-	for _, nw := range append(append(d.Networks, y.Networks...), o.Networks...) {
+	for _, nw := range slices.Concat(d.Networks, y.Networks, o.Networks) {
 		if i, ok := iface[nw.Interface]; ok {
 			if nw.Socket != "" {
 				networks[i].Socket = nw.Socket
@@ -649,7 +690,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		}
 	}
 
-	y.MountTypesUnsupported = append(append(o.MountTypesUnsupported, y.MountTypesUnsupported...), d.MountTypesUnsupported...)
+	y.MountTypesUnsupported = slices.Concat(o.MountTypesUnsupported, y.MountTypesUnsupported, d.MountTypesUnsupported)
 	mountTypesUnsupported := make(map[string]struct{})
 	for _, f := range y.MountTypesUnsupported {
 		mountTypesUnsupported[f] = struct{}{}
@@ -698,7 +739,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 	// Only works for exact matches; does not normalize case or resolve symlinks.
 	mounts := make([]Mount, 0, len(d.Mounts)+len(y.Mounts)+len(o.Mounts))
 	location := make(map[string]int)
-	for _, mount := range append(append(d.Mounts, y.Mounts...), o.Mounts...) {
+	for _, mount := range slices.Concat(d.Mounts, y.Mounts, o.Mounts) {
 		if out, err := executeHostTemplate(mount.Location, instDir, y.Param); err == nil {
 			mount.Location = out.String()
 		} else {
@@ -782,8 +823,13 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 				mounts[i].NineP.Cache = ptr.Of(Default9pCacheForRO)
 			}
 		}
+		if location, err := localpathutil.Expand(mount.Location); err == nil {
+			mounts[i].Location = location
+		} else {
+			logrus.WithError(err).Warnf("Couldn't expand location %q", mount.Location)
+		}
 		if mount.MountPoint == nil {
-			mounts[i].MountPoint = ptr.Of(mount.Location)
+			mounts[i].MountPoint = ptr.Of(mounts[i].Location)
 		}
 	}
 
@@ -829,11 +875,8 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 		y.CACertificates.RemoveDefaults = ptr.Of(false)
 	}
 
-	caFiles := unique(append(append(d.CACertificates.Files, y.CACertificates.Files...), o.CACertificates.Files...))
-	y.CACertificates.Files = caFiles
-
-	caCerts := unique(append(append(d.CACertificates.Certs, y.CACertificates.Certs...), o.CACertificates.Certs...))
-	y.CACertificates.Certs = caCerts
+	y.CACertificates.Files = unique(slices.Concat(d.CACertificates.Files, y.CACertificates.Files, o.CACertificates.Files))
+	y.CACertificates.Certs = unique(slices.Concat(d.CACertificates.Certs, y.CACertificates.Certs, o.CACertificates.Certs))
 
 	if runtime.GOOS == "darwin" && IsNativeArch(AARCH64) {
 		if y.Rosetta.Enabled == nil {
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index 3f4e8520..39a7c557 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
@@ -7,6 +10,7 @@ import (
 )
 
 type LimaYAML struct {
+	Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty" jsonschema:"nullable"`
 	MinimumLimaVersion    *string       `yaml:"minimumLimaVersion,omitempty" json:"minimumLimaVersion,omitempty" jsonschema:"nullable"`
 	VMType                *VMType       `yaml:"vmType,omitempty" json:"vmType,omitempty" jsonschema:"nullable"`
 	VMOpts                VMOpts        `yaml:"vmOpts,omitempty" json:"vmOpts,omitempty"`
@@ -50,6 +54,13 @@ type LimaYAML struct {
 	User                 User           `yaml:"user,omitempty" json:"user,omitempty"`
 }
 
+type BaseTemplates []LocatorWithDigest
+
+type LocatorWithDigest struct {
+	URL    string  `yaml:"url" json:"url"`
+	Digest *string `yaml:"digest,omitempty" json:"digest,omitempty" jsonschema:"nullable"` // TODO currently unused
+}
+
 type (
 	OS        = string
 	Arch      = string
@@ -210,13 +221,25 @@ const (
 	ProvisionModeBoot       ProvisionMode = "boot"
 	ProvisionModeDependency ProvisionMode = "dependency"
 	ProvisionModeAnsible    ProvisionMode = "ansible"
+	ProvisionModeData       ProvisionMode = "data"
 )
 
 type Provision struct {
-	Mode                            ProvisionMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
-	SkipDefaultDependencyResolution *bool         `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
-	Script                          string        `yaml:"script" json:"script"`
-	Playbook                        string        `yaml:"playbook,omitempty" json:"playbook,omitempty"`
+	Mode                            ProvisionMode      `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
+	SkipDefaultDependencyResolution *bool              `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
+	Script                          string             `yaml:"script" json:"script"`
+	File                            *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
+	Playbook                        string             `yaml:"playbook,omitempty" json:"playbook,omitempty"`
+	// All ProvisionData fields must be nil unless Mode is ProvisionModeData
+	ProvisionData `yaml:",inline"` // Flatten fields for "strict" YAML mode
+}
+
+type ProvisionData struct {
+	Content     *string `yaml:"content,omitempty" json:"content,omitempty" jsonschema:"nullable"`
+	Overwrite   *bool   `yaml:"overwrite,omitempty" json:"overwrite,omitempty" jsonschema:"nullable"`
+	Owner       *string `yaml:"owner,omitempty" json:"owner,omitempty"` // any owner string supported by `chown`, defaults to "root:root"
+	Path        *string `yaml:"path,omitempty" json:"path,omitempty"`
+	Permissions *string `yaml:"permissions,omitempty" json:"permissions,omitempty"`
 }
 
 type Containerd struct {
@@ -232,10 +255,11 @@ const (
 )
 
 type Probe struct {
-	Mode        ProbeMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=readiness"`
-	Description string    `yaml:"description,omitempty" json:"description,omitempty"`
-	Script      string    `yaml:"script,omitempty" json:"script,omitempty"`
-	Hint        string    `yaml:"hint,omitempty" json:"hint,omitempty"`
+	Mode        ProbeMode          `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=readiness"`
+	Description string             `yaml:"description,omitempty" json:"description,omitempty"`
+	Script      string             `yaml:"script,omitempty" json:"script,omitempty"`
+	File        *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
+	Hint        string             `yaml:"hint,omitempty" json:"hint,omitempty"`
 }
 
 type Proto = string
diff --git a/pkg/limayaml/load.go b/pkg/limayaml/load.go
index 58a47a0a..87cb4890 100644
--- a/pkg/limayaml/load.go
+++ b/pkg/limayaml/load.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
diff --git a/pkg/limayaml/marshal.go b/pkg/limayaml/marshal.go
index afbecc68..0380fa9b 100644
--- a/pkg/limayaml/marshal.go
+++ b/pkg/limayaml/marshal.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
@@ -35,16 +38,49 @@ func unmarshalDisk(dst *Disk, b []byte) error {
 	return yaml.Unmarshal(b, dst)
 }
 
-func Unmarshal(data []byte, v any, comment string) error {
-	if err := yaml.UnmarshalWithOptions(data, v, yaml.CustomUnmarshaler[Disk](unmarshalDisk)); err != nil {
+// unmarshalBaseTemplates unmarshalls `base` which is either a string or a list of Locators.
+func unmarshalBaseTemplates(dst *BaseTemplates, b []byte) error {
+	var s string
+	if err := yaml.Unmarshal(b, &s); err == nil {
+		*dst = BaseTemplates{LocatorWithDigest{URL: s}}
+		return nil
+	}
+	var locator LocatorWithDigest
+	if err := yaml.Unmarshal(b, &locator); err == nil {
+		*dst = BaseTemplates{locator}
+		return nil
+	}
+	return yaml.UnmarshalWithOptions(b, dst, yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest))
+}
+
+// unmarshalLocator unmarshalls a locator which is either a string or a Locator struct.
+func unmarshalLocatorWithDigest(dst *LocatorWithDigest, b []byte) error {
+	var s string
+	if err := yaml.Unmarshal(b, &s); err == nil {
+		*dst = LocatorWithDigest{URL: s}
+		return nil
+	}
+	return yaml.Unmarshal(b, dst)
+}
+
+func Unmarshal(data []byte, y *LimaYAML, comment string) error {
+	opts := []yaml.DecodeOption{
+		yaml.CustomUnmarshaler[BaseTemplates](unmarshalBaseTemplates),
+		yaml.CustomUnmarshaler[Disk](unmarshalDisk),
+		yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest),
+	}
+	if err := yaml.UnmarshalWithOptions(data, y, opts...); err != nil {
 		return fmt.Errorf("failed to unmarshal YAML (%s): %w", comment, err)
 	}
-	// the go-yaml library doesn't catch all markup errors, unfortunately
+	// The go-yaml library doesn't catch all markup errors, unfortunately
 	// make sure to get a "second opinion", using the same library as "yq"
 	if err := yqutil.ValidateContent(data); err != nil {
 		return fmt.Errorf("failed to unmarshal YAML (%s): %w", comment, err)
 	}
-	if err := yaml.UnmarshalWithOptions(data, v, yaml.Strict(), yaml.CustomUnmarshaler[Disk](unmarshalDisk)); err != nil {
+	// Finally log a warning if the YAML file violates the "strict" rules
+	opts = append(opts, yaml.Strict())
+	var ignore LimaYAML
+	if err := yaml.UnmarshalWithOptions(data, &ignore, opts...); err != nil {
 		logrus.WithField("comment", comment).WithError(err).Warn("Non-strict YAML detected; please check for typos")
 	}
 	return nil
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index e60e3740..fb93a08b 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
@@ -9,6 +12,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"runtime"
+	"strconv"
 	"strings"
 	"unicode"
 
@@ -47,6 +51,9 @@ func validateFileObject(f File, fieldName string) error {
 }
 
 func Validate(y *LimaYAML, warn bool) error {
+	if len(y.Base) > 0 {
+		return errors.New("field `base` must be empty for YAML validation")
+	}
 	if y.MinimumLimaVersion != nil {
 		if _, err := versionutil.Parse(*y.MinimumLimaVersion); err != nil {
 			return fmt.Errorf("field `minimumLimaVersion` must be a semvar value, got %q: %w", *y.MinimumLimaVersion, err)
@@ -148,6 +155,7 @@ func Validate(y *LimaYAML, warn bool) error {
 			return fmt.Errorf("field `mounts[%d].location` must be an absolute path, got %q",
 				i, f.Location)
 		}
+		// f.Location has already been expanded in FillDefaults(), but that function cannot return errors.
 		loc, err := localpathutil.Expand(f.Location)
 		if err != nil {
 			return fmt.Errorf("field `mounts[%d].location` refers to an unexpandable path: %q: %w", i, f.Location, err)
@@ -168,6 +176,10 @@ func Validate(y *LimaYAML, warn bool) error {
 		case *y.User.Home:
 			return fmt.Errorf("field `mounts[%d].mountPoint` is the reserved internal home directory %q", i, *y.User.Home)
 		}
+		// There is no tilde-expansion for guest filenames
+		if strings.HasPrefix(*f.MountPoint, "~") {
+			return fmt.Errorf("field `mounts[%d].mountPoint` must not start with \"~\"", i)
+		}
 
 		if _, err := units.RAMInBytes(*f.NineP.Msize); err != nil {
 			return fmt.Errorf("field `msize` has an invalid value: %w", err)
@@ -203,21 +215,61 @@ func Validate(y *LimaYAML, warn bool) error {
 	// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.
 
 	for i, p := range y.Provision {
-		switch p.Mode {
-		case ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot:
-			if p.SkipDefaultDependencyResolution != nil {
-				return fmt.Errorf("field `provision[%d].mode` cannot set skipDefaultDependencyResolution, only valid on scripts of type %q",
-					i, ProvisionModeDependency)
+		if p.File != nil {
+			if p.File.URL != "" {
+				return fmt.Errorf("field `provision[%d].file.url` must be empty during validation (script should already be embedded)", i)
+			}
+			if p.File.Digest != nil {
+				return fmt.Errorf("field `provision[%d].file.digest` support is not yet implemented", i)
 			}
-		case ProvisionModeDependency:
-		case ProvisionModeAnsible:
+		}
+		switch p.Mode {
+		case ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeData, ProvisionModeDependency, ProvisionModeAnsible:
 		default:
-			return fmt.Errorf("field `provision[%d].mode` must one of %q, %q, %q, %q, or %q",
-				i, ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeDependency, ProvisionModeAnsible)
+			return fmt.Errorf("field `provision[%d].mode` must one of %q, %q, %q, %q, %q, or %q",
+				i, ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeDependency, ProvisionModeAnsible, ProvisionModeAnsible)
+		}
+		if p.Mode != ProvisionModeDependency && p.SkipDefaultDependencyResolution != nil {
+			return fmt.Errorf("field `provision[%d].mode` cannot set skipDefaultDependencyResolution, only valid on scripts of type %q",
+				i, ProvisionModeDependency)
+		}
+		if p.Mode == ProvisionModeData {
+			if p.Path == nil {
+				return fmt.Errorf("field `provision[%d].path` must not be empty when mode is %q", i, ProvisionModeData)
+			}
+			if !path.IsAbs(*p.Path) {
+				return fmt.Errorf("field `provision[%d].path` must be an absolute path", i)
+			}
+			if p.Content == nil {
+				return fmt.Errorf("field `provision[%d].content` must not be empty when mode is %q", i, ProvisionModeData)
+			}
+			// FillDefaults makes sure that p.Permissions is not nil
+			if _, err := strconv.ParseInt(*p.Permissions, 8, 64); err != nil {
+				return fmt.Errorf("field `provision[%d].permissions` must be an octal number: %w", i, err)
+			}
+		} else {
+			if p.Script == "" && p.Mode != ProvisionModeAnsible {
+				return fmt.Errorf("field `provision[%d].script` must not be empty", i)
+			}
+			if p.Content != nil {
+				return fmt.Errorf("field `provision[%d].content` can only be set when mode is %q", i, ProvisionModeData)
+			}
+			if p.Overwrite != nil {
+				return fmt.Errorf("field `provision[%d].overwrite` can only be set when mode is %q", i, ProvisionModeData)
+			}
+			if p.Owner != nil {
+				return fmt.Errorf("field `provision[%d].owner` can only be set when mode is %q", i, ProvisionModeData)
+			}
+			if p.Path != nil {
+				return fmt.Errorf("field `provision[%d].path` can only be set when mode is %q", i, ProvisionModeData)
+			}
+			if p.Permissions != nil {
+				return fmt.Errorf("field `provision[%d].permissions` can only be set when mode is %q", i, ProvisionModeData)
+			}
 		}
 		if p.Playbook != "" {
 			if p.Mode != ProvisionModeAnsible {
-				return fmt.Errorf("field `provision[%d].mode must be %q if playbook is set", i, ProvisionModeAnsible)
+				return fmt.Errorf("field `provision[%d].playbook can only be set when mode is %q", i, ProvisionModeAnsible)
 			}
 			if p.Script != "" {
 				return fmt.Errorf("field `provision[%d].script must be empty if playbook is set", i)
@@ -243,6 +295,14 @@ func Validate(y *LimaYAML, warn bool) error {
 		}
 	}
 	for i, p := range y.Probes {
+		if p.File != nil {
+			if p.File.URL != "" {
+				return fmt.Errorf("field `probe[%d].file.url` must be empty during validation (script should already be embedded)", i)
+			}
+			if p.File.Digest != nil {
+				return fmt.Errorf("field `probe[%d].file.digest` support is not yet implemented", i)
+			}
+		}
 		if !strings.HasPrefix(p.Script, "#!") {
 			return fmt.Errorf("field `probe[%d].script` must start with a '#!' line", i)
 		}
diff --git a/pkg/localpathutil/localpathutil.go b/pkg/localpathutil/localpathutil.go
index ba5124dc..3081763b 100644
--- a/pkg/localpathutil/localpathutil.go
+++ b/pkg/localpathutil/localpathutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package localpathutil
 
 import (
diff --git a/pkg/lockutil/lockutil_unix.go b/pkg/lockutil/lockutil_unix.go
index 1c9e18c1..0679b7c2 100644
--- a/pkg/lockutil/lockutil_unix.go
+++ b/pkg/lockutil/lockutil_unix.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // From https://github.com/containerd/nerdctl/blob/v0.13.0/pkg/lockutil/lockutil_unix.go
 /*
    Copyright The containerd Authors.
diff --git a/pkg/lockutil/lockutil_windows.go b/pkg/lockutil/lockutil_windows.go
index f191559a..6653ae3f 100644
--- a/pkg/lockutil/lockutil_windows.go
+++ b/pkg/lockutil/lockutil_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // From https://github.com/containerd/nerdctl/blob/v0.13.0/pkg/lockutil/lockutil_windows.go
 /*
    Copyright The containerd Authors.
diff --git a/pkg/logrusutil/logrusutil.go b/pkg/logrusutil/logrusutil.go
index 98a76d9d..109c2981 100644
--- a/pkg/logrusutil/logrusutil.go
+++ b/pkg/logrusutil/logrusutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package logrusutil
 
 import (
diff --git a/pkg/must/must.go b/pkg/must/must.go
index 8dbada99..cec07544 100644
--- a/pkg/must/must.go
+++ b/pkg/must/must.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package must
 
 func Must[T any](obj T, err error) T {
diff --git a/pkg/nativeimgutil/nativeimgutil.go b/pkg/nativeimgutil/nativeimgutil.go
index 3f2a368a..733f2dc5 100644
--- a/pkg/nativeimgutil/nativeimgutil.go
+++ b/pkg/nativeimgutil/nativeimgutil.go
@@ -1,22 +1,48 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // Package nativeimgutil provides image utilities that do not depend on `qemu-img` binary.
 package nativeimgutil
 
 import (
+	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"os"
 	"path/filepath"
 
-	"github.com/containerd/continuity/fs"
+	containerdfs "github.com/containerd/continuity/fs"
 	"github.com/docker/go-units"
 	"github.com/lima-vm/go-qcow2reader"
 	"github.com/lima-vm/go-qcow2reader/convert"
 	"github.com/lima-vm/go-qcow2reader/image/qcow2"
 	"github.com/lima-vm/go-qcow2reader/image/raw"
 	"github.com/lima-vm/lima/pkg/progressbar"
+	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
 )
 
+// CreateRawDataDisk creates an empty raw data disk.
+func CreateRawDataDisk(dir string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+	f, err := os.Create(dataDisk)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	return f.Truncate(int64(size))
+}
+
+// ResizeRawDataDisk resizes a raw data disk.
+func ResizeRawDataDisk(dir string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+	return os.Truncate(dataDisk, int64(size))
+}
+
 // ConvertToRaw converts a source disk into a raw disk.
 // source and dest may be same.
 // ConvertToRaw is a NOP if source == dest, and no resizing is needed.
@@ -106,7 +132,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
 func convertRawToRaw(source, dest string, size *int64) error {
 	if source != dest {
 		// continuity attempts clonefile
-		if err := fs.CopyFile(dest, source); err != nil {
+		if err := containerdfs.CopyFile(dest, source); err != nil {
 			return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
 		}
 	}
diff --git a/pkg/networks/commands.go b/pkg/networks/commands.go
index 8003812d..c5bccc5f 100644
--- a/pkg/networks/commands.go
+++ b/pkg/networks/commands.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/networks/config.go b/pkg/networks/config.go
index a7707d36..984a7a51 100644
--- a/pkg/networks/config.go
+++ b/pkg/networks/config.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/networks/const.go b/pkg/networks/const.go
index 16671fd6..8baf4920 100644
--- a/pkg/networks/const.go
+++ b/pkg/networks/const.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 const (
diff --git a/pkg/networks/networks.go b/pkg/networks/networks.go
index cf2d376b..12a1555d 100644
--- a/pkg/networks/networks.go
+++ b/pkg/networks/networks.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import "net"
diff --git a/pkg/networks/reconcile/reconcile.go b/pkg/networks/reconcile/reconcile.go
index c40ab4b3..35c269ef 100644
--- a/pkg/networks/reconcile/reconcile.go
+++ b/pkg/networks/reconcile/reconcile.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/networks/sudoers.go b/pkg/networks/sudoers.go
index 08a45f59..3b15fdd4 100644
--- a/pkg/networks/sudoers.go
+++ b/pkg/networks/sudoers.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/networks/usernet/client.go b/pkg/networks/usernet/client.go
index 94e372ab..014b9397 100644
--- a/pkg/networks/usernet/client.go
+++ b/pkg/networks/usernet/client.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usernet
 
 import (
diff --git a/pkg/networks/usernet/config.go b/pkg/networks/usernet/config.go
index ec0f0d84..76940d60 100644
--- a/pkg/networks/usernet/config.go
+++ b/pkg/networks/usernet/config.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usernet
 
 import (
diff --git a/pkg/networks/usernet/dnshosts/dnshosts.go b/pkg/networks/usernet/dnshosts/dnshosts.go
index 6d0f4501..00272f3b 100644
--- a/pkg/networks/usernet/dnshosts/dnshosts.go
+++ b/pkg/networks/usernet/dnshosts/dnshosts.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // From https://raw.githubusercontent.com/abiosoft/colima/v0.5.5/daemon/process/gvproxy/dnshosts_test.go
 /*
 	MIT License
diff --git a/pkg/networks/usernet/gvproxy.go b/pkg/networks/usernet/gvproxy.go
index 02c86e89..814dab0f 100644
--- a/pkg/networks/usernet/gvproxy.go
+++ b/pkg/networks/usernet/gvproxy.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usernet
 
 import (
diff --git a/pkg/networks/usernet/recoincile.go b/pkg/networks/usernet/recoincile.go
index d0026654..7304e724 100644
--- a/pkg/networks/usernet/recoincile.go
+++ b/pkg/networks/usernet/recoincile.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usernet
 
 import (
diff --git a/pkg/networks/usernet/udpfileconn.go b/pkg/networks/usernet/udpfileconn.go
index fa804d5c..96b64e3d 100644
--- a/pkg/networks/usernet/udpfileconn.go
+++ b/pkg/networks/usernet/udpfileconn.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usernet
 
 import (
diff --git a/pkg/networks/validate.go b/pkg/networks/validate.go
index 8f11dfdc..4ae9eaf2 100644
--- a/pkg/networks/validate.go
+++ b/pkg/networks/validate.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/osutil/dns_darwin.go b/pkg/osutil/dns_darwin.go
index a1949cb9..01358be6 100644
--- a/pkg/osutil/dns_darwin.go
+++ b/pkg/osutil/dns_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/dns_others.go b/pkg/osutil/dns_others.go
index e02f804c..032974d7 100644
--- a/pkg/osutil/dns_others.go
+++ b/pkg/osutil/dns_others.go
@@ -1,5 +1,8 @@
 //go:build !darwin
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 func DNSAddresses() ([]string, error) {
diff --git a/pkg/osutil/machineid.go b/pkg/osutil/machineid.go
index caf1be3e..4bdc6e17 100644
--- a/pkg/osutil/machineid.go
+++ b/pkg/osutil/machineid.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/osutil_linux.go b/pkg/osutil/osutil_linux.go
index f4a1d1ef..cec55451 100644
--- a/pkg/osutil/osutil_linux.go
+++ b/pkg/osutil/osutil_linux.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/osutil_others.go b/pkg/osutil/osutil_others.go
index ace79fd0..e10d5f9d 100644
--- a/pkg/osutil/osutil_others.go
+++ b/pkg/osutil/osutil_others.go
@@ -1,5 +1,8 @@
 //go:build !linux && !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/osutil_unix.go b/pkg/osutil/osutil_unix.go
index a8764a8c..e220c472 100644
--- a/pkg/osutil/osutil_unix.go
+++ b/pkg/osutil/osutil_unix.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/osutil_windows.go b/pkg/osutil/osutil_windows.go
index 4849597d..ead56d70 100644
--- a/pkg/osutil/osutil_windows.go
+++ b/pkg/osutil/osutil_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/osversion_darwin.go b/pkg/osutil/osversion_darwin.go
index 6c72c983..477a1c00 100644
--- a/pkg/osutil/osversion_darwin.go
+++ b/pkg/osutil/osversion_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/osversion_others.go b/pkg/osutil/osversion_others.go
index c54d0f1d..2d2781fb 100644
--- a/pkg/osutil/osversion_others.go
+++ b/pkg/osutil/osversion_others.go
@@ -1,5 +1,8 @@
 //go:build !darwin
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/rosetta_darwin.go b/pkg/osutil/rosetta_darwin.go
index 48e58ae7..d0b1069d 100644
--- a/pkg/osutil/rosetta_darwin.go
+++ b/pkg/osutil/rosetta_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/rosetta_others.go b/pkg/osutil/rosetta_others.go
index 71170313..c9e9ba27 100644
--- a/pkg/osutil/rosetta_others.go
+++ b/pkg/osutil/rosetta_others.go
@@ -1,5 +1,8 @@
 //go:build !darwin
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 func IsBeingRosettaTranslated() bool {
diff --git a/pkg/osutil/user.go b/pkg/osutil/user.go
index d1883c59..b34f5c3d 100644
--- a/pkg/osutil/user.go
+++ b/pkg/osutil/user.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
@@ -11,6 +14,7 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/lima-vm/lima/pkg/ioutilx"
 	. "github.com/lima-vm/lima/pkg/must"
 	"github.com/lima-vm/lima/pkg/version/versionutil"
 	"github.com/sirupsen/logrus"
@@ -141,9 +145,11 @@ func LimaUser(limaVersion string, warn bool) *user.User {
 				warnings = append(warnings, warning)
 				limaUser.Gid = formatUidGid(gid)
 			}
-			home, err := call([]string{"cygpath", limaUser.HomeDir})
+			home, err := ioutilx.WindowsSubsystemPath(limaUser.HomeDir)
 			if err != nil {
 				logrus.Debug(err)
+			} else {
+				home += ".linux"
 			}
 			if home == "" {
 				drive := filepath.VolumeName(limaUser.HomeDir)
@@ -151,8 +157,13 @@ func LimaUser(limaVersion string, warn bool) *user.User {
 				// replace C: with /c
 				prefix := strings.ToLower(fmt.Sprintf("/%c", drive[0]))
 				home = strings.Replace(home, drive, prefix, 1)
+				home += ".linux"
 			}
 			if !regexPath.MatchString(limaUser.HomeDir) {
+				// Trim prefix of well known default mounts
+				if strings.HasPrefix(home, "/mnt/") {
+					home = strings.TrimPrefix(home, "/mnt")
+				}
 				warning := fmt.Sprintf("local home %q is not a valid Linux path (must match %q); using %q home instead",
 					limaUser.HomeDir, regexPath.String(), home)
 				warnings = append(warnings, warning)
diff --git a/pkg/portfwd/client.go b/pkg/portfwd/client.go
index 0f104fdf..1c24c069 100644
--- a/pkg/portfwd/client.go
+++ b/pkg/portfwd/client.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package portfwd
 
 import (
diff --git a/pkg/portfwd/control_others.go b/pkg/portfwd/control_others.go
index 82f1c890..d6e666ac 100644
--- a/pkg/portfwd/control_others.go
+++ b/pkg/portfwd/control_others.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package portfwd
 
 import (
diff --git a/pkg/portfwd/control_windows.go b/pkg/portfwd/control_windows.go
index d9e3dc04..db41ab11 100644
--- a/pkg/portfwd/control_windows.go
+++ b/pkg/portfwd/control_windows.go
@@ -1,10 +1,12 @@
-//go:build windows
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
 
 package portfwd
 
 import (
-	"golang.org/x/sys/windows"
 	"syscall"
+
+	"golang.org/x/sys/windows"
 )
 
 func Control(_, _ string, c syscall.RawConn) (err error) {
diff --git a/pkg/portfwd/forward.go b/pkg/portfwd/forward.go
index e6475dfa..846fb98b 100644
--- a/pkg/portfwd/forward.go
+++ b/pkg/portfwd/forward.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package portfwd
 
 import (
diff --git a/pkg/portfwd/listener.go b/pkg/portfwd/listener.go
index 9911edcb..edd798f0 100644
--- a/pkg/portfwd/listener.go
+++ b/pkg/portfwd/listener.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package portfwd
 
 import (
diff --git a/pkg/portfwd/listener_darwin.go b/pkg/portfwd/listener_darwin.go
index d5f83a7c..f884550f 100644
--- a/pkg/portfwd/listener_darwin.go
+++ b/pkg/portfwd/listener_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package portfwd
 
 import (
diff --git a/pkg/portfwd/listener_others.go b/pkg/portfwd/listener_others.go
index 74473f7b..bab11084 100644
--- a/pkg/portfwd/listener_others.go
+++ b/pkg/portfwd/listener_others.go
@@ -1,5 +1,8 @@
 //go:build !darwin
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package portfwd
 
 import (
diff --git a/pkg/portfwdserver/server.go b/pkg/portfwdserver/server.go
index 985fbeb3..54f48a6e 100644
--- a/pkg/portfwdserver/server.go
+++ b/pkg/portfwdserver/server.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package portfwdserver
 
 import (
diff --git a/pkg/progressbar/progressbar.go b/pkg/progressbar/progressbar.go
index eea971df..32e8b1d6 100644
--- a/pkg/progressbar/progressbar.go
+++ b/pkg/progressbar/progressbar.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package progressbar
 
 import (
diff --git a/pkg/ptr/ptr.go b/pkg/ptr/ptr.go
index 4fb6d49d..c3640b2f 100644
--- a/pkg/ptr/ptr.go
+++ b/pkg/ptr/ptr.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // Package ptr holds utilities for taking pointer references to values.
 package ptr
 
diff --git a/pkg/qemu/entitlementutil/entitlementutil.go b/pkg/qemu/entitlementutil/entitlementutil.go
index 9d73581e..afe2db96 100644
--- a/pkg/qemu/entitlementutil/entitlementutil.go
+++ b/pkg/qemu/entitlementutil/entitlementutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // Package entitlementutil provides a workaround for https://github.com/lima-vm/lima/issues/1742
 package entitlementutil
 
diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/qemu/imgutil/imgutil.go
index d181161f..2fffbfd1 100644
--- a/pkg/qemu/imgutil/imgutil.go
+++ b/pkg/qemu/imgutil/imgutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package imgutil
 
 import (
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 13cb263e..adc75222 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package qemu
 
 import (
@@ -28,7 +31,6 @@ import (
 	"github.com/lima-vm/lima/pkg/fileutils"
 	"github.com/lima-vm/lima/pkg/iso9660util"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	"github.com/lima-vm/lima/pkg/localpathutil"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
@@ -932,11 +934,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 	if *y.MountType == limayaml.NINEP || *y.MountType == limayaml.VIRTIOFS {
 		for i, f := range y.Mounts {
 			tag := fmt.Sprintf("mount%d", i)
-			location, err := localpathutil.Expand(f.Location)
-			if err != nil {
-				return "", nil, err
-			}
-			if err := os.MkdirAll(location, 0o755); err != nil {
+			if err := os.MkdirAll(f.Location, 0o755); err != nil {
 				return "", nil, err
 			}
 
@@ -944,7 +942,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 			case limayaml.NINEP:
 				options := "local"
 				options += fmt.Sprintf(",mount_tag=%s", tag)
-				options += fmt.Sprintf(",path=%s", location)
+				options += fmt.Sprintf(",path=%s", f.Location)
 				options += fmt.Sprintf(",security_model=%s", *f.NineP.SecurityModel)
 				if !*f.Writable {
 					options += ",readonly"
@@ -1064,10 +1062,6 @@ func FindVirtiofsd(qemuExe string) (string, error) {
 
 func VirtiofsdCmdline(cfg Config, mountIndex int) ([]string, error) {
 	mount := cfg.LimaYAML.Mounts[mountIndex]
-	location, err := localpathutil.Expand(mount.Location)
-	if err != nil {
-		return nil, err
-	}
 
 	vhostSock := filepath.Join(cfg.InstanceDir, fmt.Sprintf(filenames.VhostSock, mountIndex))
 	// qemu_driver has to wait for the socket to appear, so make sure any old ones are removed here.
@@ -1077,7 +1071,7 @@ func VirtiofsdCmdline(cfg Config, mountIndex int) ([]string, error) {
 
 	return []string{
 		"--socket-path", vhostSock,
-		"--shared-dir", location,
+		"--shared-dir", mount.Location,
 	}, nil
 }
 
diff --git a/pkg/qemu/qemu_driver.go b/pkg/qemu/qemu_driver.go
index 23b75488..81d7739a 100644
--- a/pkg/qemu/qemu_driver.go
+++ b/pkg/qemu/qemu_driver.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package qemu
 
 import (
diff --git a/pkg/reflectutil/reflectutil.go b/pkg/reflectutil/reflectutil.go
index 7cdd5f5a..1a49df09 100644
--- a/pkg/reflectutil/reflectutil.go
+++ b/pkg/reflectutil/reflectutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // This file has been adapted from https://github.com/containerd/nerdctl/blob/v1.0.0/pkg/reflectutil/reflectutil.go
 /*
    Copyright The containerd Authors.
diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go
index c6c41730..520a41d9 100644
--- a/pkg/snapshot/snapshot.go
+++ b/pkg/snapshot/snapshot.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package snapshot
 
 import (
diff --git a/pkg/sshutil/format.go b/pkg/sshutil/format.go
index 772b4857..c2efda18 100644
--- a/pkg/sshutil/format.go
+++ b/pkg/sshutil/format.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package sshutil
 
 import (
diff --git a/pkg/sshutil/sshutil.go b/pkg/sshutil/sshutil.go
index 563bc5ed..1d8a5f15 100644
--- a/pkg/sshutil/sshutil.go
+++ b/pkg/sshutil/sshutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package sshutil
 
 import (
@@ -95,8 +98,15 @@ func DefaultPubKeys(loadDotSSH bool) ([]PubKey, error) {
 		}
 		if err := lockutil.WithDirLock(configDir, func() error {
 			// no passphrase, no user@host comment
+			privPath := filepath.Join(configDir, filenames.UserPrivateKey)
+			if runtime.GOOS == "windows" {
+				privPath, err = ioutilx.WindowsSubsystemPath(privPath)
+				if err != nil {
+					return err
+				}
+			}
 			keygenCmd := exec.Command("ssh-keygen", "-t", "ed25519", "-q", "-N", "",
-				"-C", "lima", "-f", filepath.Join(configDir, filenames.UserPrivateKey))
+				"-C", "lima", "-f", privPath)
 			logrus.Debugf("executing %v", keygenCmd.Args)
 			if out, err := keygenCmd.CombinedOutput(); err != nil {
 				return fmt.Errorf("failed to run %v: %q: %w", keygenCmd.Args, string(out), err)
@@ -168,12 +178,11 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
 		return nil, err
 	}
 	var opts []string
-	if runtime.GOOS == "windows" {
-		privateKeyPath = ioutilx.CanonicalWindowsPath(privateKeyPath)
-		opts = []string{fmt.Sprintf(`IdentityFile='%s'`, privateKeyPath)}
-	} else {
-		opts = []string{fmt.Sprintf(`IdentityFile="%s"`, privateKeyPath)}
+	idf, err := identityFileEntry(privateKeyPath)
+	if err != nil {
+		return nil, err
 	}
+	opts = []string{idf}
 
 	// Append all private keys corresponding to ~/.ssh/*.pub to keep old instances working
 	// that had been created before lima started using an internal identity.
@@ -204,11 +213,11 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
 				// Fail on permission-related and other path errors
 				return nil, err
 			}
-			if runtime.GOOS == "windows" {
-				opts = append(opts, fmt.Sprintf(`IdentityFile='%s'`, privateKeyPath))
-			} else {
-				opts = append(opts, fmt.Sprintf(`IdentityFile="%s"`, privateKeyPath))
+			idf, err = identityFileEntry(privateKeyPath)
+			if err != nil {
+				return nil, err
 			}
+			opts = append(opts, idf)
 		}
 	}
 
@@ -253,6 +262,17 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
 	return opts, nil
 }
 
+func identityFileEntry(privateKeyPath string) (string, error) {
+	if runtime.GOOS == "windows" {
+		privateKeyPath, err := ioutilx.WindowsSubsystemPath(privateKeyPath)
+		if err != nil {
+			return "", err
+		}
+		return fmt.Sprintf(`IdentityFile='%s'`, privateKeyPath), nil
+	}
+	return fmt.Sprintf(`IdentityFile="%s"`, privateKeyPath), nil
+}
+
 // SSHOpts adds the following options to CommonOptions: User, ControlMaster, ControlPath, ControlPersist.
 func SSHOpts(sshPath, instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) {
 	controlSock := filepath.Join(instDir, filenames.SSHSock)
@@ -265,7 +285,10 @@ func SSHOpts(sshPath, instDir, username string, useDotSSH, forwardAgent, forward
 	}
 	controlPath := fmt.Sprintf(`ControlPath="%s"`, controlSock)
 	if runtime.GOOS == "windows" {
-		controlSock = ioutilx.CanonicalWindowsPath(controlSock)
+		controlSock, err = ioutilx.WindowsSubsystemPath(controlSock)
+		if err != nil {
+			return nil, err
+		}
 		controlPath = fmt.Sprintf(`ControlPath='%s'`, controlSock)
 	}
 	opts = append(opts,
diff --git a/pkg/store/dirnames/dirnames.go b/pkg/store/dirnames/dirnames.go
index 1b5a250e..89118ed0 100644
--- a/pkg/store/dirnames/dirnames.go
+++ b/pkg/store/dirnames/dirnames.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package dirnames
 
 import (
diff --git a/pkg/store/disk.go b/pkg/store/disk.go
index aa815d01..c4d0f2a9 100644
--- a/pkg/store/disk.go
+++ b/pkg/store/disk.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package store
 
 import (
diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go
index ebb76a0c..09f6c93b 100644
--- a/pkg/store/filenames/filenames.go
+++ b/pkg/store/filenames/filenames.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // Package filenames defines the names of the files that appear under an instance dir
 // or inside the config directory.
 //
@@ -21,6 +24,7 @@ const (
 	NetworksConfig = "networks.yaml"
 	Default        = "default.yaml"
 	Override       = "override.yaml"
+	Base           = "base.yaml"
 )
 
 // Filenames that may appear under an instance directory
diff --git a/pkg/store/instance.go b/pkg/store/instance.go
index 7daf4537..2ddd4380 100644
--- a/pkg/store/instance.go
+++ b/pkg/store/instance.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package store
 
 import (
diff --git a/pkg/store/instance_unix.go b/pkg/store/instance_unix.go
index 10b340ab..4f509be3 100644
--- a/pkg/store/instance_unix.go
+++ b/pkg/store/instance_unix.go
@@ -1,5 +1,8 @@
 //go:build !windows
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package store
 
 import "github.com/lima-vm/lima/pkg/limayaml"
diff --git a/pkg/store/instance_windows.go b/pkg/store/instance_windows.go
index d7457ec2..208bbfb8 100644
--- a/pkg/store/instance_windows.go
+++ b/pkg/store/instance_windows.go
@@ -1,8 +1,10 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package store
 
 import (
 	"fmt"
-	"os/exec"
 	"regexp"
 	"strings"
 
@@ -23,7 +25,7 @@ func inspectStatus(instDir string, inst *Instance, y *limayaml.LimaYAML) {
 		inst.SSHLocalPort = 22
 
 		if inst.Status == StatusRunning {
-			sshAddr, err := getWslSSHAddress(inst.Name)
+			sshAddr, err := GetSSHAddress(inst.Name)
 			if err == nil {
 				inst.SSHAddress = sshAddr
 			} else {
@@ -115,21 +117,5 @@ func GetWslStatus(instName string) (string, error) {
 }
 
 func GetSSHAddress(instName string) (string, error) {
-	return getWslSSHAddress(instName)
-}
-
-// GetWslSSHAddress runs a hostname command to get the IP from inside of a wsl2 VM.
-//
-// Expected output (whitespace preserved, [] for optional):
-// PS > wsl -d <distroName> bash -c hostname -I | cut -d' ' -f1
-// 168.1.1.1 [10.0.0.1]
-func getWslSSHAddress(instName string) (string, error) {
-	distroName := "lima-" + instName
-	cmd := exec.Command("wsl.exe", "-d", distroName, "bash", "-c", `hostname -I | cut -d ' ' -f1`)
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		return "", fmt.Errorf("failed to get hostname for instance %q, err: %w (out=%q)", instName, err, string(out))
-	}
-
-	return strings.TrimSpace(string(out)), nil
+	return "127.0.0.1", nil
 }
diff --git a/pkg/store/store.go b/pkg/store/store.go
index 84885c32..60732d10 100644
--- a/pkg/store/store.go
+++ b/pkg/store/store.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package store
 
 import (
diff --git a/pkg/sysprof/network_darwin.go b/pkg/sysprof/network_darwin.go
index 87871adb..fabd12d8 100644
--- a/pkg/sysprof/network_darwin.go
+++ b/pkg/sysprof/network_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package sysprof
 
 type SPNetworkDataType struct {
diff --git a/pkg/sysprof/sysprof_darwin.go b/pkg/sysprof/sysprof_darwin.go
index d7fdf398..e555287c 100644
--- a/pkg/sysprof/sysprof_darwin.go
+++ b/pkg/sysprof/sysprof_darwin.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package sysprof
 
 import (
diff --git a/pkg/textutil/textutil.go b/pkg/textutil/textutil.go
index 26b4ce17..e02e63ad 100644
--- a/pkg/textutil/textutil.go
+++ b/pkg/textutil/textutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package textutil
 
 import (
diff --git a/pkg/uiutil/uiutil.go b/pkg/uiutil/uiutil.go
index 2491b86c..d5c5f508 100644
--- a/pkg/uiutil/uiutil.go
+++ b/pkg/uiutil/uiutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package uiutil
 
 import (
diff --git a/pkg/usrlocalsharelima/usrlocalsharelima.go b/pkg/usrlocalsharelima/usrlocalsharelima.go
index 72cb22d0..fcaca4ad 100644
--- a/pkg/usrlocalsharelima/usrlocalsharelima.go
+++ b/pkg/usrlocalsharelima/usrlocalsharelima.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usrlocalsharelima
 
 import (
diff --git a/pkg/version/version.go b/pkg/version/version.go
index 829b0467..c3e1fd30 100644
--- a/pkg/version/version.go
+++ b/pkg/version/version.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package version
 
 // Version is filled on compilation time.
diff --git a/pkg/version/versionutil/versionutil.go b/pkg/version/versionutil/versionutil.go
index d95d11bd..b4390fe7 100644
--- a/pkg/version/versionutil/versionutil.go
+++ b/pkg/version/versionutil/versionutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package versionutil
 
 import (
diff --git a/pkg/vz/disk.go b/pkg/vz/disk.go
index 4e5c7b23..596bc1ba 100644
--- a/pkg/vz/disk.go
+++ b/pkg/vz/disk.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
diff --git a/pkg/vz/errors_darwin.go b/pkg/vz/errors_darwin.go
index 7d5b870a..d5bc7549 100644
--- a/pkg/vz/errors_darwin.go
+++ b/pkg/vz/errors_darwin.go
@@ -1,5 +1,8 @@
 //go:build darwin && !no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import "errors"
diff --git a/pkg/vz/network_darwin.go b/pkg/vz/network_darwin.go
index cbd506a4..39f0cb90 100644
--- a/pkg/vz/network_darwin.go
+++ b/pkg/vz/network_darwin.go
@@ -1,5 +1,8 @@
 //go:build darwin && !no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
diff --git a/pkg/vz/rosetta_directory_share.go b/pkg/vz/rosetta_directory_share.go
index aac0f00a..f1b193a7 100644
--- a/pkg/vz/rosetta_directory_share.go
+++ b/pkg/vz/rosetta_directory_share.go
@@ -1,5 +1,8 @@
 //go:build darwin && !arm64 && !no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
diff --git a/pkg/vz/rosetta_directory_share_arm64.go b/pkg/vz/rosetta_directory_share_arm64.go
index 4d8a173e..a692dc9f 100644
--- a/pkg/vz/rosetta_directory_share_arm64.go
+++ b/pkg/vz/rosetta_directory_share_arm64.go
@@ -1,5 +1,8 @@
 //go:build darwin && arm64 && !no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
diff --git a/pkg/vz/vm_darwin.go b/pkg/vz/vm_darwin.go
index aeb43076..407af20c 100644
--- a/pkg/vz/vm_darwin.go
+++ b/pkg/vz/vm_darwin.go
@@ -1,5 +1,8 @@
 //go:build darwin && !no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
@@ -22,7 +25,6 @@ import (
 	"github.com/lima-vm/lima/pkg/driver"
 	"github.com/lima-vm/lima/pkg/iso9660util"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	"github.com/lima-vm/lima/pkg/localpathutil"
 	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/networks/usernet"
@@ -545,18 +547,14 @@ func attachFolderMounts(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineCo
 	var mounts []vz.DirectorySharingDeviceConfiguration
 	if *driver.Instance.Config.MountType == limayaml.VIRTIOFS {
 		for i, mount := range driver.Instance.Config.Mounts {
-			expandedPath, err := localpathutil.Expand(mount.Location)
-			if err != nil {
-				return err
-			}
-			if _, err := os.Stat(expandedPath); errors.Is(err, os.ErrNotExist) {
-				err := os.MkdirAll(expandedPath, 0o750)
+			if _, err := os.Stat(mount.Location); errors.Is(err, os.ErrNotExist) {
+				err := os.MkdirAll(mount.Location, 0o750)
 				if err != nil {
 					return err
 				}
 			}
 
-			directory, err := vz.NewSharedDirectory(expandedPath, !*mount.Writable)
+			directory, err := vz.NewSharedDirectory(mount.Location, !*mount.Writable)
 			if err != nil {
 				return err
 			}
diff --git a/pkg/vz/vz_driver_darwin.go b/pkg/vz/vz_driver_darwin.go
index e05afb70..ef50a10a 100644
--- a/pkg/vz/vz_driver_darwin.go
+++ b/pkg/vz/vz_driver_darwin.go
@@ -1,5 +1,8 @@
 //go:build darwin && !no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
@@ -109,7 +112,7 @@ func (l *LimaVzDriver) Validate() error {
 	}
 
 	for i, image := range l.Instance.Config.Images {
-		if unknown := reflectutil.UnknownNonEmptyFields(image, "File"); len(unknown) > 0 {
+		if unknown := reflectutil.UnknownNonEmptyFields(image, "File", "Kernel", "Initrd"); len(unknown) > 0 {
 			logrus.Warnf("vmType %s: ignoring images[%d]: %+v", *l.Instance.Config.VMType, i, unknown)
 		}
 	}
diff --git a/pkg/vz/vz_driver_others.go b/pkg/vz/vz_driver_others.go
index b8e414e1..66265628 100644
--- a/pkg/vz/vz_driver_others.go
+++ b/pkg/vz/vz_driver_others.go
@@ -1,5 +1,8 @@
 //go:build !darwin || no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
diff --git a/pkg/windows/process_windows.go b/pkg/windows/process_windows.go
index a885d7a5..9c88bbaf 100644
--- a/pkg/windows/process_windows.go
+++ b/pkg/windows/process_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package windows
 
 import (
diff --git a/pkg/windows/registry_windows.go b/pkg/windows/registry_windows.go
index 76a77132..9ba599b3 100644
--- a/pkg/windows/registry_windows.go
+++ b/pkg/windows/registry_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package windows
 
 import (
diff --git a/pkg/windows/wsl_util_windows.go b/pkg/windows/wsl_util_windows.go
index a3fe3138..33e92d91 100644
--- a/pkg/windows/wsl_util_windows.go
+++ b/pkg/windows/wsl_util_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package windows
 
 import (
diff --git a/pkg/wsl2/fs.go b/pkg/wsl2/fs.go
index b21186e0..c116396d 100644
--- a/pkg/wsl2/fs.go
+++ b/pkg/wsl2/fs.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package wsl2
 
 import (
diff --git a/pkg/wsl2/vm_windows.go b/pkg/wsl2/vm_windows.go
index 4b54ba73..d0b537e3 100644
--- a/pkg/wsl2/vm_windows.go
+++ b/pkg/wsl2/vm_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package wsl2
 
 import (
diff --git a/pkg/wsl2/wsl_driver_others.go b/pkg/wsl2/wsl_driver_others.go
index 7a116e5e..4ad86d02 100644
--- a/pkg/wsl2/wsl_driver_others.go
+++ b/pkg/wsl2/wsl_driver_others.go
@@ -1,5 +1,8 @@
 //go:build !windows || no_wsl
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package wsl2
 
 import (
diff --git a/pkg/wsl2/wsl_driver_windows.go b/pkg/wsl2/wsl_driver_windows.go
index cdf0c292..5adcb5ed 100644
--- a/pkg/wsl2/wsl_driver_windows.go
+++ b/pkg/wsl2/wsl_driver_windows.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package wsl2
 
 import (
diff --git a/pkg/yqutil/yqutil.go b/pkg/yqutil/yqutil.go
index 1b0a6ab9..0ee68b83 100644
--- a/pkg/yqutil/yqutil.go
+++ b/pkg/yqutil/yqutil.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package yqutil
 
 import (
@@ -36,25 +39,12 @@ func ValidateContent(content []byte) error {
 	return err
 }
 
-// EvaluateExpression evaluates the yq expression and returns the modified yaml.
-func EvaluateExpression(expression string, content []byte) ([]byte, error) {
+// EvaluateExpressionPlain evaluates the yq expression and returns the yq result.
+func EvaluateExpressionPlain(expression, content string) (string, error) {
 	if expression == "" {
 		return content, nil
 	}
 	logrus.Debugf("Evaluating yq expression: %q", expression)
-	formatter, err := yamlfmtBasicFormatter()
-	if err != nil {
-		return nil, err
-	}
-	// `ApplyFeatures()` is being called directly before passing content to `yqlib`.
-	// This results in `ApplyFeatures()` being called twice with `FeatureApplyBefore`:
-	// once here and once inside `formatter.Format`.
-	// Currently, calling `ApplyFeatures()` with `FeatureApplyBefore` twice is not an issue,
-	// but future changes to `yamlfmt` might cause problems if it is called twice.
-	_, contentModified, err := formatter.Features.ApplyFeatures(context.Background(), content, yamlfmt.FeatureApplyBefore)
-	if err != nil {
-		return nil, err
-	}
 	memory := logging.NewMemoryBackend(0)
 	backend := logging.AddModuleLevel(memory)
 	logging.SetBackend(backend)
@@ -65,7 +55,7 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
 	encoderPrefs.ColorsEnabled = false
 	encoder := yqlib.NewYamlEncoder(encoderPrefs)
 	decoder := yqlib.NewYamlDecoder(yqlib.ConfiguredYamlPreferences)
-	out, err := yqlib.NewStringEvaluator().EvaluateAll(expression, string(contentModified), encoder, decoder)
+	out, err := yqlib.NewStringEvaluator().EvaluateAll(expression, content, encoder, decoder)
 	if err != nil {
 		logger := logrus.StandardLogger()
 		for node := memory.Head(); node != nil; node = node.Next() {
@@ -87,6 +77,32 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
 				entry.Debug(message)
 			}
 		}
+		return "", err
+	}
+	return out, nil
+}
+
+// EvaluateExpression evaluates the yq expression and returns the output formatted with yamlfmt.
+func EvaluateExpression(expression string, content []byte) ([]byte, error) {
+	if expression == "" {
+		return content, nil
+	}
+	formatter, err := yamlfmtBasicFormatter()
+	if err != nil {
+		return nil, err
+	}
+	// `ApplyFeatures()` is being called directly before passing content to `yqlib`.
+	// This results in `ApplyFeatures()` being called twice with `FeatureApplyBefore`:
+	// once here and once inside `formatter.Format`.
+	// Currently, calling `ApplyFeatures()` with `FeatureApplyBefore` twice is not an issue,
+	// but future changes to `yamlfmt` might cause problems if it is called twice.
+	_, contentModified, err := formatter.Features.ApplyFeatures(context.Background(), content, yamlfmt.FeatureApplyBefore)
+	if err != nil {
+		return nil, err
+	}
+
+	out, err := EvaluateExpressionPlain(expression, string(contentModified))
+	if err != nil {
 		return nil, err
 	}
 	return formatter.Format([]byte(out))
diff --git a/templates/README.md b/templates/README.md
index fc935278..585b49fa 100644
--- a/templates/README.md
+++ b/templates/README.md
@@ -30,6 +30,7 @@ Distro:
 - [`ubuntu-lts`](./ubuntu-lts.yaml): Ubuntu LTS (same as `ubuntu.yaml` but pinned to an LTS version)
 - [`experimental/gentoo`](./experimental/gentoo.yaml): [experimental] Gentoo
 - [`experimental/opensuse-tumbleweed`](./experimental/opensuse-tumbleweed.yaml): [experimental] openSUSE Tumbleweed
+- [`experimental/debian-sid`](./experimental/debian-sid.yaml): [experimental] Debian Sid
 
 Container engines:
 - [`apptainer`](./apptainer.yaml): Apptainer
diff --git a/templates/default.yaml b/templates/default.yaml
index 471c9661..181b66e7 100644
--- a/templates/default.yaml
+++ b/templates/default.yaml
@@ -211,6 +211,12 @@ containerd:
 # Provisioning scripts need to be idempotent because they might be called
 # multiple times, e.g. when the host VM is being restarted.
 # The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
+#
+# EXPERIMENTAL Alternatively the script can be provided using the "file" property. This file is read when the instance
+# is created and then stored under the "script" property. When "file" is specified "script" must be empty.
+# The "file" property can either be a string (URL), or an object with a "url" and "digest" properties.
+# The "digest" property is currently unused.
+# Relative script files will be resolved relative to the location of the template file.
 # 🟢 Builtin default: []
 # provision:
 # # `system` is executed with root privileges
@@ -222,12 +228,9 @@ containerd:
 #     apt-get install -y vim
 # # `user` is executed without root privileges
 # - mode: user
-#   script: |
-#     #!/bin/bash
-#     set -eux -o pipefail
-#     cat <<EOF > ~/.vimrc
-#     set number
-#     EOF
+#   file:
+#     url: user-provisioning.sh
+#     digest: deadbeef
 # # `boot` is executed directly by /bin/sh as part of cloud-init-local.service's early boot process,
 # # which is why there is no hash-bang specified in the example
 # # See cloud-init docs for more info https://docs.cloud-init.io/en/latest/reference/examples.html#run-commands-on-first-boot
@@ -250,10 +253,30 @@ containerd:
 # # See ansible docs, and `ansible-config`, for more info https://docs.ansible.com/ansible/latest/playbook_guide/
 # - mode: ansible
 #   playbook: playbook.yaml
+# # `data` is a file that is written to the guest filesystem and not executed at all.
+# # The file is written after the boot scripts, but before any other provisioning scripts are run.
+# # Note that reverse-sshfs mounts are not established at this time; other mount types are already mounted.
+# # The `path` and `content` properties are required. The `file` property can be used the same way as with
+# # other provisioning scripts, in which case `content` must be empty. The `owner` defaults to "root:root";
+# # the permissions default to 644. The `overwrite` property defaults to `true`, in which case the file will
+# # be overwritten on every boot.
+# # `path`, `contents`, and `owner` are evaluated as guest templates (see above).
+# - mode: data
+#   path: /etc/conf.d/example
+#   content: |
+#     FOO=bar
+#   owner: "root:root"
+#   permissions: 644
+#   overwrite: false
 
 # Probe scripts to check readiness.
 # The scripts run in user mode. They must start with a '#!' line.
 # The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
+# EXPERIMENTAL Alternatively the script can be provided using the "file" property. This file is read when the instance
+# is created and then stored under the "script" property. When "file" is specified "script" must be empty.
+# The "file" property can either be a string (URL), or an object with a "url" and "digest" properties.
+# The "digest" property is currently unused.
+# Relative script files will be resolved relative to the location of the template file.
 # 🟢 Builtin default: []
 # probes:
 # # Only `readiness` probes are supported right now.
@@ -279,6 +302,17 @@ containerd:
 # 🟢 Builtin default: not set
 minimumLimaVersion: null
 
+# EXPERIMENTAL
+# Default settings can be imported from base templates. These will be merged in when the instance
+# is created, and the combined template is stored in the instance directory.
+# This setting ca be either a single string (URL), or a list of locators.
+# A locator is again either a string (URL), or an object with "url" and "digest" properties, e.g.
+# base: [{url: ./base.yaml, digest: decafbad}, …]
+# The "digest" property is currently unused.
+# Any relative base template name will be resolved relative to the location of the main template.
+# 🟢 Builtin default: no base template
+base: null
+
 # User to be used inside the VM
 user:
   # User name. An explicitly specified username is not validated by Lima.
@@ -561,6 +595,12 @@ nestedVirtualization: null
 # on each restart. It can be used to globally override settings, e.g. make
 # the mount of the home directory writable.
 
+# EXPERIMENTAL
+# A third mechanism is $LIMA_HOME/_config/base.yaml. It is similar to
+# `default.yaml` but will be merged during the `base` template embedding
+# when the instance is created, and not on every start. It becomes part
+# of the lima.yaml file.
+
 # On each instance start the config settings are determined: If a value is
 # not set in `lima.yaml`, then the `default.yaml` is used. If that file
 # doesn't exist, or the value is not defined in the file, then the builtin
diff --git a/templates/experimental/debian-sid.yaml b/templates/experimental/debian-sid.yaml
new file mode 100644
index 00000000..d3c5f5fc
--- /dev/null
+++ b/templates/experimental/debian-sid.yaml
@@ -0,0 +1,11 @@
+# This template requires Lima v0.7.0 or later
+images:
+- location: "https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-genericcloud-amd64-daily.qcow2"
+  arch: "x86_64"
+- location: "https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-genericcloud-arm64-daily.qcow2"
+  arch: "aarch64"
+mountTypesUnsupported: ["9p"]
+mounts:
+- location: "~"
+- location: "/tmp/lima"
+  writable: true
diff --git a/templates/experimental/wsl2.yaml b/templates/experimental/wsl2.yaml
index 3fc32b02..ae67e332 100644
--- a/templates/experimental/wsl2.yaml
+++ b/templates/experimental/wsl2.yaml
@@ -4,9 +4,9 @@ vmType: wsl2
 
 images:
 # Source: https://github.com/runfinch/finch-core/blob/main/Dockerfile
-- location: "https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1694791577.tar.gz"
+- location: "https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1741837119.tar.gz"
   arch: "x86_64"
-  digest: "sha256:2d4d2e7386450899c6d0587fd0db21afadb31d974fa744aa9365c883935c5341"
+  digest: "sha256:1ebee4c785fc4d31fd514365694a7d4d50a95093526c051f76dc63d8ba9fafe6"
 
 mountType: wsl2
 
diff --git a/website/Makefile b/website/Makefile
index 83e3e37e..d58a8607 100644
--- a/website/Makefile
+++ b/website/Makefile
@@ -1,3 +1,5 @@
+# SPDX-FileCopyrightText: Copyright The Lima Authors
+# SPDX-License-Identifier: Apache-2.0
 # See also: package.json
 NPM ?= npm
 
diff --git a/website/content/en/docs/community/subprojects.md b/website/content/en/docs/community/subprojects.md
index c599c836..b9778e5d 100644
--- a/website/content/en/docs/community/subprojects.md
+++ b/website/content/en/docs/community/subprojects.md
@@ -5,7 +5,9 @@ weight: 90
 
 Some portions of Lima are useful for other projects too and split out to separate repos:
 
+<!-- sorted by importance to end users -->
 - <https://github.com/lima-vm/socket_vmnet>: vmnet.framework support for unmodified rootless QEMU
+- <https://github.com/lima-vm/lima-actions>: run Lima on GitHub Actions
 - <https://github.com/lima-vm/go-qcow2reader>: qcow2 reader for Go
 - <https://github.com/lima-vm/sshocker>: ssh + reverse sshfs + port forwarder, in Docker-like CLI (predecessor of Lima)
 - <https://github.com/lima-vm/alpine-lima>: Create an alpine based image for lima
diff --git a/website/content/en/docs/dev/internals.md b/website/content/en/docs/dev/internals.md
index 48aa0b08..8e3bbe4a 100644
--- a/website/content/en/docs/dev/internals.md
+++ b/website/content/en/docs/dev/internals.md
@@ -186,6 +186,10 @@ The volume label is "cidata", as defined by [cloud-init NoCloud](https://docs.cl
 - `LIMA_CIDATA_MOUNTS`: the number of the Lima mounts
 - `LIMA_CIDATA_MOUNTS_%d_MOUNTPOINT`: the N-th mount point of Lima mounts (N=0, 1, ...)
 - `LIMA_CIDATA_MOUNTTYPE`: the type of the Lima mounts ("reverse-sshfs", "9p", ...)
+- `LIMA_CIDATA_DATAFILE_%08d_OVERWRITE`: set to "true" if the datafile should be overwritten if it already exists.
+- `LIMA_CIDATA_DATAFILE_%08d_OWNER`: set to the owner of the datafile.
+- `LIMA_CIDATA_DATAFILE_%08d_PATH`: set to the path the datafile should be copied to.
+- `LIMA_CIDATA_DATAFILE_%08d_PERMISSIONS`: set to the file permissions (in octal) for the datafile.
 - `LIMA_CIDATA_CONTAINERD_USER`: set to "1" if rootless containerd to be set up
 - `LIMA_CIDATA_CONTAINERD_SYSTEM`: set to "1" if system-wide containerd to be set up
 - `LIMA_CIDATA_CONTAINERD_ARCHIVE`: the name of the containerd archive. `nerdctl-full.tgz`
diff --git a/website/content/en/docs/examples/gha.md b/website/content/en/docs/examples/gha.md
index 91708943..3cb79b13 100644
--- a/website/content/en/docs/examples/gha.md
+++ b/website/content/en/docs/examples/gha.md
@@ -30,34 +30,15 @@ jobs:
     - name: Check out code
       uses: actions/checkout@v4
 
-    - name: "Install QEMU"
-      run: |
-        set -eux
-        sudo apt-get update
-        sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86 qemu-utils
-        sudo modprobe kvm
-        # `sudo usermod -aG kvm $(whoami)` does not take an effect on GHA
-        sudo chown $(whoami) /dev/kvm
-
-    - name: "Install Lima"
-      env:
-        GITHUB_TOKEN: ${{ github.token }}  # required by `gh attestation verify`
-      run: |
-        set -eux
-        LIMA_VERSION=$(curl -fsSL https://api.github.com/repos/lima-vm/lima/releases/latest | jq -r .tag_name)
-        FILE="lima-${LIMA_VERSION:1}-Linux-x86_64.tar.gz"
-        curl -fOSL https://github.com/lima-vm/lima/releases/download/${LIMA_VERSION}/${FILE}
-        gh attestation verify --owner=lima-vm "${FILE}"
-        sudo tar Cxzvf /usr/local "${FILE}"
-        rm -f "${FILE}"
-        # Export LIMA_VERSION For the GHA cache key
-        echo "LIMA_VERSION=${LIMA_VERSION}" >>$GITHUB_ENV
+    - name: "Set up Lima"
+      uses: lima-vm/lima-actions/setup@v1
+      id: lima-actions-setup
 
     - name: "Cache ~/.cache/lima"
       uses: actions/cache@v4
       with:
         path: ~/.cache/lima
-        key: lima-${{ env.LIMA_VERSION }}
+        key: lima-${{ steps.lima-actions-setup.outputs.version }}
 
     - name: "Start an instance of Fedora"
       run: |
@@ -73,6 +54,8 @@ jobs:
         limactl shell another curl http://lima-default.internal
 ```
 
+See also <https://github.com/lima-vm/lima-actions>.
+
 ### Plain mode
 
 The `--plain` mode is useful when you want the VM instance to be as close as possible to a physical host:
@@ -82,14 +65,14 @@ The `--plain` mode is useful when you want the VM instance to be as close as pos
       # --plain is set to disable file sharing, port forwarding, built-in containerd, etc.
       run: limactl start --plain --name=default --cpus=1 --memory=1 --network=lima:user-v2 template://fedora
 
+    - name: "Set up SSH"
+      uses: lima-vm/lima-actions/ssh@v1
+
     - name: "Initialize Fedora"
       # plain old rsync and ssh are used for the initialization of the guest,
       # so that people who are not familiar with Lima can understand the initialization steps.
       run: |
         set -eux -o pipefail
-        # Initialize SSH
-        mkdir -p -m 0700 ~/.ssh
-        cat ~/.lima/default/ssh.config >> ~/.ssh/config
         # Sync the current directory to /tmp/repo in the guest
         rsync -a -e ssh . lima-default:/tmp/repo
         # Install packages
diff --git a/website/content/en/docs/releases/experimental.md b/website/content/en/docs/releases/experimental.md
index 2310e41c..d443fce8 100644
--- a/website/content/en/docs/releases/experimental.md
+++ b/website/content/en/docs/releases/experimental.md
@@ -13,17 +13,22 @@ The following features are experimental and subject to change:
 - `audio.device`
 - `arch: armv7l`
 - `mountInotify: true`
+- `base:` and variations like `base[]:` and `base[].url`
+- `provision[].file:` and alternative `provision[].file.url:`  
+- `probes[].file:` and alternative `probes[].file.url:`
 
 The following commands are experimental and subject to change:
 
 - `limactl snapshot *`
 - `limactl tunnel`
+- `limactl template *`
 
 ## Graduated
 
 The following features were experimental in the past:
 
 ### Until v1.0
+
 - `mountType: 9p`
 - `vmType: vz` and relevant configurations (`mountType: virtiofs`, `rosetta`, `[]networks.vzNAT`)
 - `mode: user-v2` in `networks.yml` and relevant configuration in `lima.yaml`



Test patch:
diff --git a/cmd/limactl/editflags/editflags_test.go b/cmd/limactl/editflags/editflags_test.go
index 51d9a076..2dceac15 100644
--- a/cmd/limactl/editflags/editflags_test.go
+++ b/cmd/limactl/editflags/editflags_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package editflags
 
 import (
diff --git a/pkg/autostart/autostart_test.go b/pkg/autostart/autostart_test.go
index 58b8fb39..d4070244 100644
--- a/pkg/autostart/autostart_test.go
+++ b/pkg/autostart/autostart_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package autostart
 
 import (
diff --git a/pkg/cidata/cidata_test.go b/pkg/cidata/cidata_test.go
index daa0dd11..1e3fafc5 100644
--- a/pkg/cidata/cidata_test.go
+++ b/pkg/cidata/cidata_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package cidata
 
 import (
diff --git a/pkg/cidata/fuzz_test.go b/pkg/cidata/fuzz_test.go
index baebe645..4305b13a 100644
--- a/pkg/cidata/fuzz_test.go
+++ b/pkg/cidata/fuzz_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package cidata
 
 import (
diff --git a/pkg/cidata/template_test.go b/pkg/cidata/template_test.go
index 79be8a95..31f7b2df 100644
--- a/pkg/cidata/template_test.go
+++ b/pkg/cidata/template_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package cidata
 
 import (
diff --git a/pkg/downloader/downloader_test.go b/pkg/downloader/downloader_test.go
index 045805bc..48d4f467 100644
--- a/pkg/downloader/downloader_test.go
+++ b/pkg/downloader/downloader_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package downloader
 
 import (
diff --git a/pkg/downloader/fuzz_test.go b/pkg/downloader/fuzz_test.go
index e52f28ae..5b6ba071 100644
--- a/pkg/downloader/fuzz_test.go
+++ b/pkg/downloader/fuzz_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package downloader
 
 import (
diff --git a/pkg/guestagent/iptables/iptables_test.go b/pkg/guestagent/iptables/iptables_test.go
index 263d2ec3..2830591f 100644
--- a/pkg/guestagent/iptables/iptables_test.go
+++ b/pkg/guestagent/iptables/iptables_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package iptables
 
 import (
diff --git a/pkg/guestagent/kubernetesservice/kubernetesservice_test.go b/pkg/guestagent/kubernetesservice/kubernetesservice_test.go
index c1ff43cb..94630ecf 100644
--- a/pkg/guestagent/kubernetesservice/kubernetesservice_test.go
+++ b/pkg/guestagent/kubernetesservice/kubernetesservice_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package kubernetesservice
 
 import (
diff --git a/pkg/guestagent/procnettcp/fuzz_test.go b/pkg/guestagent/procnettcp/fuzz_test.go
index c29af227..9d3fa7b3 100644
--- a/pkg/guestagent/procnettcp/fuzz_test.go
+++ b/pkg/guestagent/procnettcp/fuzz_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package procnettcp
 
 import (
diff --git a/pkg/guestagent/procnettcp/procnettcp_test.go b/pkg/guestagent/procnettcp/procnettcp_test.go
index e994028a..f61467ee 100644
--- a/pkg/guestagent/procnettcp/procnettcp_test.go
+++ b/pkg/guestagent/procnettcp/procnettcp_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package procnettcp
 
 import (
diff --git a/pkg/hostagent/dns/dns_test.go b/pkg/hostagent/dns/dns_test.go
index feaeecf7..c9644974 100644
--- a/pkg/hostagent/dns/dns_test.go
+++ b/pkg/hostagent/dns/dns_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package dns
 
 import (
diff --git a/pkg/identifierutil/identifierutil_test.go b/pkg/identifierutil/identifierutil_test.go
index 15286bfa..a8ec2a74 100644
--- a/pkg/identifierutil/identifierutil_test.go
+++ b/pkg/identifierutil/identifierutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package identifierutil
 
 import (
diff --git a/pkg/iso9660util/fuzz_test.go b/pkg/iso9660util/fuzz_test.go
index b07e24e3..b3cf9a72 100644
--- a/pkg/iso9660util/fuzz_test.go
+++ b/pkg/iso9660util/fuzz_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package iso9660util
 
 import (
diff --git a/pkg/limatmpl/abs_test.go b/pkg/limatmpl/abs_test.go
new file mode 100644
index 00000000..b4f192f6
--- /dev/null
+++ b/pkg/limatmpl/abs_test.go
@@ -0,0 +1,299 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package limatmpl
+
+import (
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+type useAbsLocatorsTestCase struct {
+	description string
+	locator     string
+	template    string
+	expected    string
+}
+
+var useAbsLocatorsTestCases = []useAbsLocatorsTestCase{
+	{
+		"Template without base or script file",
+		"template://foo",
+		`arch: aarch64`,
+		`arch: aarch64`,
+	},
+	{
+		"Single string base template",
+		"template://foo",
+		`base: bar.yaml`,
+		`base: template://bar.yaml`,
+	},
+	{
+		"Flow style array of one base template",
+		"template://foo",
+		`base: {url: bar.yaml, digest: deadbeef}`,
+		// not sure why the quotes around the URL were added; maybe because we don't copy the style from the source
+		`base: {url: 'template://bar.yaml', digest: deadbeef}`,
+	},
+	{
+		"Flow style array of sequence of two base URLs",
+		"template://foo",
+		`base: [bar.yaml, baz.yaml]`,
+		`base: ['template://bar.yaml', 'template://baz.yaml']`,
+	},
+	{
+		"Flow style array of sequence of two base locator objects",
+		"template://foo",
+		`base: [{url: bar.yaml, digest: deadbeef}, {url: baz.yaml, digest: decafbad}]`,
+		`base: [{url: 'template://bar.yaml', digest: deadbeef}, {url: 'template://baz.yaml', digest: decafbad}]`,
+	},
+	{
+		"Block style array of one base template",
+		"template://foo",
+		`
+base:
+- bar.yaml
+`,
+		`
+base:
+- template://bar.yaml`,
+	},
+	{
+		"Block style of four base templates",
+		"template://foo",
+		`
+base:
+- bar.yaml
+- template://my
+- https://example.com/my.yaml
+- baz.yaml
+`,
+		`
+base:
+- template://bar.yaml
+- template://my
+- https://example.com/my.yaml
+- template://baz.yaml
+`,
+	},
+	{
+		"Provisioning and probe scripts",
+		"template://experimental/foo",
+		`
+provision:
+- mode: user
+  file: userscript.sh
+- mode: system
+  file:
+    url: systemscript.sh
+    digest: abc123
+probes:
+- file: probe.sh
+- file:
+    url: probe.sh
+    digest: digest
+`,
+		`
+provision:
+- mode: user
+  file: template://experimental/userscript.sh
+- mode: system
+  file:
+    url: template://experimental/systemscript.sh
+    digest: abc123
+probes:
+- file: template://experimental/probe.sh
+- file:
+    url: template://experimental/probe.sh
+    digest: digest
+`,
+	},
+}
+
+func TestUseAbsLocators(t *testing.T) {
+	for _, tc := range useAbsLocatorsTestCases {
+		t.Run(tc.description, func(t *testing.T) { RunUseAbsLocatorTest(t, tc) })
+	}
+}
+
+func RunUseAbsLocatorTest(t *testing.T, tc useAbsLocatorsTestCase) {
+	tmpl := &Template{
+		Bytes:   []byte(strings.TrimSpace(tc.template)),
+		Locator: tc.locator,
+	}
+	err := tmpl.UseAbsLocators()
+	assert.NilError(t, err, tc.description)
+
+	actual := strings.TrimSpace(string(tmpl.Bytes))
+	expected := strings.TrimSpace(tc.expected)
+	assert.Equal(t, actual, expected, tc.description)
+}
+
+func TestBasePath(t *testing.T) {
+	// On Windows the root will be something like "C:\"
+	root, err := filepath.Abs("/")
+	assert.NilError(t, err)
+	volume := filepath.VolumeName(root)
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("/foo")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, root)
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("/foo/bar")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, filepath.Clean(volume+"/foo"))
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("template://foo")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "template://")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("template://foo/bar")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "template://foo")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("http://host/foo")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "http://host")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("http://host/foo/bar")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "http://host/foo")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("file:///foo")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "file:///")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := basePath("file:///foo/bar")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "file:///foo")
+	})
+}
+
+func TestAbsPath(t *testing.T) {
+	root, err := filepath.Abs("/")
+	assert.NilError(t, err)
+	volume := filepath.VolumeName(root)
+
+	t.Run("If the locator is already an absolute path, it is returned unchanged", func(t *testing.T) {
+		actual, err := absPath(volume+"/foo", volume+"/root")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, filepath.Clean(volume+"/foo"))
+	})
+
+	t.Run("If the locator is a rooted path without volume name, then the volume will be added", func(t *testing.T) {
+		actual, err := absPath("/foo", volume+"/root")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, filepath.Clean(volume+"/foo"))
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("template://foo", volume+"/root")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "template://foo")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("http://host/foo", volume+"/root")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "http://host/foo")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("file:///foo", volume+"/root")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "file:///foo")
+	})
+
+	t.Run("Can't have relative path when reading from STDIN", func(t *testing.T) {
+		_, err = absPath("foo", "-")
+		assert.ErrorContains(t, err, "STDIN")
+	})
+
+	t.Run("Relative paths must be underneath the basePath", func(t *testing.T) {
+		_, err = absPath("../foo", volume+"/root")
+		assert.ErrorContains(t, err, "'../'")
+	})
+
+	t.Run("locator must not be empty", func(t *testing.T) {
+		_, err = absPath("", "foo")
+		assert.ErrorContains(t, err, "locator is empty")
+	})
+
+	t.Run("basePath must not be empty", func(t *testing.T) {
+		_, err = absPath("foo", "")
+		assert.ErrorContains(t, err, "basePath is empty")
+	})
+
+	t.Run("", func(t *testing.T) {
+		_, err = absPath("./foo", "")
+		assert.ErrorContains(t, err, "empty")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("./foo", volume+"/root")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, filepath.Clean(volume+"/root/foo"))
+	})
+
+	if runtime.GOOS == "windows" {
+		t.Run("Relative locators must not include volume names", func(t *testing.T) {
+			_, err := absPath(volume+"foo", volume+"/root")
+			assert.ErrorContains(t, err, "volume")
+		})
+	}
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("foo", "template://")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "template://foo")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("bar", "template://foo")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "template://foo/bar")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("foo", "http://host")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "http://host/foo")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("bar", "http://host/foo")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "http://host/foo/bar")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("foo", "file:///")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "file:///foo")
+	})
+
+	t.Run("", func(t *testing.T) {
+		actual, err := absPath("bar", "file:///foo")
+		assert.NilError(t, err)
+		assert.Equal(t, actual, "file:///foo/bar")
+	})
+}
diff --git a/pkg/limatmpl/embed_test.go b/pkg/limatmpl/embed_test.go
new file mode 100644
index 00000000..1dae4665
--- /dev/null
+++ b/pkg/limatmpl/embed_test.go
@@ -0,0 +1,477 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package limatmpl_test
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/lima-vm/lima/pkg/limatmpl"
+	"github.com/lima-vm/lima/pkg/limayaml"
+	"github.com/sirupsen/logrus"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/assert/cmp"
+)
+
+type embedTestCase struct {
+	description string
+	template    string
+	base        string
+	expected    string
+}
+
+// Notes:
+// * When the template starts with "#" then the comparison will be textual instead of structural.
+//   This is required to verify comment handling.
+// * If the description starts with "TODO" then the test is expected to fail (until it is fixed).
+// * If the description starts with "ERROR" then the test is expected to fail with an error containing the expected string.
+// * base is split on "---\n" and stored as base0.yaml, base1.yaml, ... in the same dir as the template.
+// * If any base template starts with "#!" then the extension will be .sh instead of .yaml.
+// * The template is automatically prefixed with "base: base0.yaml" unless base0 is a script.
+// * All line comments will be separated by 2 spaces from the value on output.
+// * Merge order of additionalDisks, mounts, and networks depends on the logic in the
+//   combineListEntries() functions and will not follow the order of the base template(s).
+
+var embedTestCases = []embedTestCase{
+	{
+		"Empty template",
+		"",
+		"vmType: qemu",
+		"vmType: qemu",
+	},
+	{
+		"Base doesn't override existing values",
+		"vmType: vz",
+		"{arch: aarch64, vmType: qemu}",
+		"{arch: aarch64, vmType: vz}",
+	},
+	{
+		"Comments are copied over as well",
+		`#
+# VM Type is QEMU
+vmType: qemu # QEMU
+`,
+		`
+# Arch is x86_64
+arch: x86_64 # X86
+`,
+		`
+# VM Type is QEMU
+vmType: qemu  # QEMU
+# Arch is x86_64
+arch: x86_64  # X86
+`,
+	},
+	{
+		"mountTypesUnsupported are concatenated and duplicates removed",
+		"mountTypesUnsupported: [9p,reverse-sshfs]",
+		"mountTypesUnsupported: [9p,virtiofs]",
+		"mountTypesUnsupported: [9p,reverse-sshfs,virtiofs]",
+	},
+	{
+		"minimumLimaVersion (including comments) is updated when the base version is higher",
+		`#
+# Works with Lima 0.8.0 and later
+minimumLimaVersion: 0.8.0 # needs 0.8.0
+`,
+		`
+# Requires at least 1.0.2
+minimumLimaVersion: 1.0.2    # or later
+`,
+		`
+# Requires at least 1.0.2
+minimumLimaVersion: 1.0.2  # or later
+`,
+	},
+	{
+		"vmOpts.qmu.minimumVersion is updated when the base version is higher",
+		"vmOpts: {qemu: {minimumVersion: 8.2.1}}",
+		"vmOpts: {qemu: {minimumVersion: 9.1.0}}",
+		"vmOpts: {qemu: {minimumVersion: 9.1.0}}",
+	},
+	{
+		"dns list is not appended, but the highest priority one is picked",
+		"dns: [1.1.1.1]",
+		"dns: [8.8.8.8, 1.2.3.4]",
+		"dns: [1.1.1.1]",
+	},
+	{
+		"Update comments on existing maps and lists that don't have comments yet",
+		`#
+additionalDisks:
+- name: disk1 # One
+`,
+		`
+# Mount additional disks
+additionalDisks: # comment
+# This is disk2
+- name: disk2 # Two
+`,
+		`
+# Mount additional disks
+additionalDisks:  # comment
+- name: disk1  # One
+# This is disk2
+- name: disk2  # Two
+`,
+	},
+	{
+		"probes and provision scripts are prepended instead of appended",
+		"probes: [{script: 1}]\nprovision: [{script: One}]",
+		"probes: [{script: 2}]\nprovision: [{script: Two}]",
+		"probes: [{script: 2},{script: 1}]\nprovision: [{script: Two},{script: One}]",
+	},
+	{
+		"additionalDisks append, but merge fields on shared name",
+		"additionalDisks: [{name: disk1}]",
+		"additionalDisks: [{name: disk2},{name: disk1, format: true}]",
+		"additionalDisks: [{name: disk1, format: true},{name: disk2}]",
+	},
+	{
+		// This test fails because there are 2 spurious newlines in the merged output
+		"TODO mounts append, but merge fields on shared mountPoint",
+		`#
+# My mounts
+mounts:
+- location: loc1  # mountPoint loc1
+- location: loc1
+  mountPoint: loc2
+`,
+		`
+mounts:
+# will update mountPoint loc2
+- location: loc1
+  mountPoint: loc2
+  writable: true
+  # SSHFS
+  sshfs:  # ssh
+    followSymlinks: true
+# will create new mountPoint loc3
+- location: loc1
+  mountPoint: loc3
+  writable: true
+`,
+		`
+# My mounts
+mounts:
+- location: loc1  # mountPoint loc1
+# will update mountPoint loc2
+- location: loc1
+  mountPoint: loc2
+  writable: true
+  # SSHFS
+  sshfs:  # ssh
+    followSymlinks: true
+# will create new mountPoint loc3
+- location: loc1
+  mountPoint: loc3
+  writable: true
+`,
+	},
+	{
+		// This entry can be deleted when the previous one no longer fails
+		"mounts append, but merge fields on shared mountPoint (no comments version)",
+		`mounts: [{location: loc1}, {location: loc1, mountPoint: loc2}]`,
+		`mounts: [{location: loc1, mountPoint: loc2, writable: true, sshfs: {followSymlinks: true}}, {location: loc1, mountPoint: loc3, writable: true}]`,
+		`mounts: [{location: loc1}, {location: loc1, mountPoint: loc2, writable: true, sshfs: {followSymlinks: true}}, {location: loc1, mountPoint: loc3, writable: true}]`,
+	},
+	{
+		"template:// URLs are not embedded when embedAll is false",
+		// also tests file.url format
+		``,
+		`
+base: template://default
+provision:
+- file:
+    url: template://provision.sh
+probes:
+- file:
+    url: template://probe.sh
+`,
+		`
+base: template://default
+provision:
+- file: template://provision.sh
+probes:
+- file: template://probe.sh
+`,
+	},
+	{
+		"ERROR Each template must only be embedded once",
+		`#
+arch: aarch64
+`,
+		`
+base: base0.yaml
+# failure would mean this test loops forever, not that it fails the test
+vmType: qemu
+`,
+		`base template loop detected`,
+	},
+	{
+		"ERROR All bases following template:// bases must be template:// URLs too when embedAll is false",
+		``,
+		`base: [template://default, base1.yaml]`,
+		"after not embedding",
+	},
+	{
+		"ERROR All bases following template:// bases must be template:// URLs too when embedAll is false",
+		``,
+		`
+base: [base1.yaml, base2.yaml]
+---
+base: template://default
+---
+base: baseX.yaml`,
+		"after not embedding",
+	},
+	{
+		"Bases are embedded depth-first",
+		`#`,
+		`
+base: [base1.yaml, {url: base2.yaml}] # also test file.url format
+additionalDisks: [disk0]
+---
+base: base3.yaml
+additionalDisks: [disk1]
+---
+additionalDisks: [disk2]
+---
+additionalDisks: [disk3]
+`,
+		`
+additionalDisks: [disk0, disk1, disk3, disk2]
+`,
+	},
+	{
+		"additionalDisks with name '*' are merged with all previous entries",
+		`
+additionalDisks:
+- name: disk1
+- name: disk2
+- name: disk3
+  format: false
+`,
+		`
+additionalDisks:
+- name: disk4
+- name: "*"
+  format: true # will apply to disk1, disk2, and disk4
+- name: disk5
+`,
+		`
+additionalDisks:
+- name: disk1
+  format: true
+- name: disk2
+  format: true
+- name: disk3
+  format: false
+- name: disk4
+  format: true
+- name: disk5
+`,
+	},
+	{
+		// This test fails because the yq commands don't handle comments properly; may need to be fixed in yq
+		"TODO additionalDisks will be upgraded from string to map",
+		`#
+additionalDisks:
+# my head comment
+- mine # my line comment
+`,
+		`
+# head comment
+additionalDisks: # line comment
+- name: "*"
+  format: true # formatting is good for you
+`,
+		`
+# head comment
+additionalDisks:  # line comment
+# my head comment
+- name: mine  # my line comment
+  format: true  # formatting is good for you
+`,
+	},
+	{
+		// This entry can be deleted when the previous one no longer fails
+		"additionalDisks will be upgraded from string to map (no comments version)",
+		`additionalDisks: [mine]`,
+		`additionalDisks: [{name: "*", format: true}]`,
+		`additionalDisks: [{name: mine, format: true}]`,
+	},
+	{
+		"networks without interface name are not merged",
+		`
+networks:
+- interface: lima1
+`,
+		`
+networks:
+- interface: lima2
+# The metric will not be merged with anything
+- metric: 250
+- interface: lima1
+  metric: 100     # will be set on the first entry
+- interface: '*'  # wildcard
+  metric: 123     # will be set on the first entry
+`,
+		`
+networks:
+- interface: lima1
+  metric: 100  # will be set on the first entry
+- interface: lima2
+  metric: 123  # will be set on the first entry
+# The metric will not be merged with anything
+- metric: 250
+`,
+	},
+	{
+		"Scripts are embedded with comments moved",
+		`#
+# Hi There!
+provision:
+# This script will be merged from an external file
+- file: base1.sh # This comment will move to the "script" key
+# This is just a data file
+- mode: data
+  file: base1.sh # This comment will move to the "content" key
+  path: /tmp/data
+`,
+		`
+# base0.yaml is ignored
+---
+#!/usr/bin/env bash
+echo "This is base1.sh"
+`,
+		// TODO: the empty line after the `path` is unexpected
+		`
+# Hi There!
+provision:
+# This script will be merged from an external file
+- script: |-  # This comment will move to the "script" key
+    #!/usr/bin/env bash
+    echo "This is base1.sh"
+# This is just a data file
+- mode: data
+  content: |-  # This comment will move to the "content" key
+    #!/usr/bin/env bash
+    echo "This is base1.sh"
+  path: /tmp/data
+
+# base0.yaml is ignored
+`,
+	},
+	{
+		"Script files are embedded even when no base property exists",
+		"provision: [{file: base0.sh}]",
+		"#! my script",
+		`provision: [{script: "#! my script"}]`,
+	},
+	{
+		"ERROR base digest is not yet implemented (1)",
+		"",
+		"base: {url: base.yaml, digest: deafbad}",
+		"not yet implemented",
+	},
+	{
+		"ERROR base digest is not yet implemented (2)",
+		"",
+		"base: [{url: base.yaml, digest: deafbad}]",
+		"not yet implemented",
+	},
+	{
+		"Image URLs will be converted into a template",
+		"",
+		"base: https://example.com/lima-linux-riscv64.img",
+		"{arch: riscv64, images: [{location: https://example.com/lima-linux-riscv64.img, arch: riscv64}]}",
+	},
+}
+
+func TestEmbed(t *testing.T) {
+	focus := os.Getenv("TEST_FOCUS")
+	for _, tc := range embedTestCases {
+		if focus != "" {
+			if !strings.Contains(tc.description, focus) {
+				continue
+			}
+			logrus.SetLevel(logrus.DebugLevel)
+		}
+		t.Run(tc.description, func(t *testing.T) { RunEmbedTest(t, tc) })
+	}
+	logrus.SetLevel(logrus.InfoLevel)
+}
+
+func RunEmbedTest(t *testing.T, tc embedTestCase) {
+	todo := strings.HasPrefix(tc.description, "TODO")
+	expectError := strings.HasPrefix(tc.description, "ERROR")
+	stringCompare := strings.HasPrefix(tc.template, "#")
+
+	// Normalize testcase data
+	tc.template = strings.TrimSpace(strings.TrimPrefix(tc.template, "#"))
+	tc.base = strings.TrimSpace(tc.base)
+	tc.expected = strings.TrimSpace(tc.expected)
+
+	// Change to temp directory so all template and script names don't include a slash.
+	cwd, err := os.Getwd()
+	assert.NilError(t, err, "Getting current working directory")
+	err = os.Chdir(t.TempDir())
+	assert.NilError(t, err, "Changing directory to t.TempDir()")
+	defer func() {
+		_ = os.Chdir(cwd)
+	}()
+
+	for i, base := range strings.Split(tc.base, "---\n") {
+		extension := ".yaml"
+		if strings.HasPrefix(base, "#!") {
+			extension = ".sh"
+		}
+		baseFilename := fmt.Sprintf("base%d%s", i, extension)
+		err := os.WriteFile(baseFilename, []byte(base), 0o600)
+		assert.NilError(t, err, tc.description)
+	}
+	tmpl := &limatmpl.Template{
+		Bytes:   []byte(fmt.Sprintf("base: base0.yaml\n%s", tc.template)),
+		Locator: "tmpl.yaml",
+	}
+	// Don't include `base` property if base0 is a script
+	if strings.HasPrefix(tc.base, "#!") {
+		tmpl.Bytes = []byte(tc.template)
+	}
+	err = tmpl.Embed(context.TODO(), false, false)
+	if expectError {
+		assert.ErrorContains(t, err, tc.expected, tc.description)
+		return
+	}
+	assert.NilError(t, err, tc.description)
+
+	if stringCompare {
+		actual := strings.TrimSpace(string(tmpl.Bytes))
+		if todo {
+			assert.Assert(t, actual != tc.expected, tc.description)
+		} else {
+			assert.Equal(t, actual, tc.expected, tc.description)
+		}
+		return
+	}
+
+	err = tmpl.Unmarshal()
+	assert.NilError(t, err, tc.description)
+
+	var expected limayaml.LimaYAML
+	err = limayaml.Unmarshal([]byte(tc.expected), &expected, "expected")
+	assert.NilError(t, err, tc.description)
+
+	if todo {
+		// using reflect.DeepEqual because cmp.DeepEqual can't easily be negated
+		assert.Assert(t, !reflect.DeepEqual(tmpl.Config, &expected), tc.description)
+	} else {
+		assert.Assert(t, cmp.DeepEqual(tmpl.Config, &expected), tc.description)
+	}
+}
diff --git a/pkg/limatmpl/locator_test.go b/pkg/limatmpl/locator_test.go
new file mode 100644
index 00000000..dcd23361
--- /dev/null
+++ b/pkg/limatmpl/locator_test.go
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package limatmpl_test
+
+import (
+	"fmt"
+	"runtime"
+	"testing"
+
+	"github.com/lima-vm/lima/pkg/limatmpl"
+	"github.com/lima-vm/lima/pkg/limayaml"
+	"gotest.tools/v3/assert"
+)
+
+func TestInstNameFromImageURL(t *testing.T) {
+	t.Run("strips image format and compression method", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("linux.iso.bz2", "unknown")
+		assert.Equal(t, name, "linux")
+	})
+	t.Run("removes generic tags", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("linux-linux_cloudimg.base-x86_64.raw", "unknown")
+		assert.Equal(t, name, "linux-x86_64")
+	})
+	t.Run("removes Alpine `nocloud_` prefix", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("nocloud_linux-x86_64.raw", "unknown")
+		assert.Equal(t, name, "linux-x86_64")
+	})
+	t.Run("removes date tag", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("linux-20250101.raw", "unknown")
+		assert.Equal(t, name, "linux")
+	})
+	t.Run("removes date tag including time", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("linux-20250101-2000.raw", "unknown")
+		assert.Equal(t, name, "linux")
+	})
+	t.Run("removes date tag including zero time", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("linux-20250101.0.raw", "unknown")
+		assert.Equal(t, name, "linux")
+	})
+	t.Run("replace arch with archlinux", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("arch-aarch64.raw", "unknown")
+		assert.Equal(t, name, "archlinux-aarch64")
+	})
+	t.Run("don't replace arch in the middle of the name", func(t *testing.T) {
+		name := limatmpl.InstNameFromImageURL("my-arch-aarch64.raw", "unknown")
+		assert.Equal(t, name, "my-arch-aarch64")
+	})
+	t.Run("removes native arch", func(t *testing.T) {
+		arch := limayaml.NewArch(runtime.GOARCH)
+		image := fmt.Sprintf("linux_cloudimg.base-%s.qcow2.gz", arch)
+		name := limatmpl.InstNameFromImageURL(image, arch)
+		assert.Equal(t, name, "linux")
+	})
+}
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index 1a2f91b8..71f7e981 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
@@ -143,7 +146,9 @@ func TestFillDefault(t *testing.T) {
 			},
 		},
 		Mounts: []Mount{
-			{Location: "/tmp"},
+			// Location will be passed through localpathutil.Expand() which will normalize the name
+			// (add a drive letter). So we must start with a valid local path to match it again later.
+			{Location: filepath.Clean(os.TempDir())},
 			{Location: "{{.Dir}}/{{.Param.ONE}}", MountPoint: ptr.Of("/mnt/{{.Param.ONE}}")},
 		},
 		MountType: ptr.Of(NINEP),
@@ -228,7 +233,7 @@ func TestFillDefault(t *testing.T) {
 	expect.Mounts[0].NineP.Cache = ptr.Of(Default9pCacheForRO)
 	expect.Mounts[0].Virtiofs.QueueSize = nil
 	// Only missing Mounts field is Writable, and the default value is also the null value: false
-	expect.Mounts[1].Location = fmt.Sprintf("%s/%s", instDir, y.Param["ONE"])
+	expect.Mounts[1].Location = filepath.Join(instDir, y.Param["ONE"])
 	expect.Mounts[1].MountPoint = ptr.Of(fmt.Sprintf("/mnt/%s", y.Param["ONE"]))
 	expect.Mounts[1].Writable = ptr.Of(false)
 	expect.Mounts[1].SSHFS.Cache = ptr.Of(true)
@@ -318,6 +323,9 @@ func TestFillDefault(t *testing.T) {
 	// User-provided defaults should override any builtin defaults
 
 	// Choose values that are different from the "builtin" defaults
+
+	// Calling filepath.Abs() to add a drive letter on Windows
+	varLog, _ := filepath.Abs("/var/log")
 	d = LimaYAML{
 		VMType: ptr.Of("vz"),
 		OS:     ptr.Of("unknown"),
@@ -382,7 +390,7 @@ func TestFillDefault(t *testing.T) {
 
 		Mounts: []Mount{
 			{
-				Location: "/var/log",
+				Location: varLog,
 				Writable: ptr.Of(false),
 			},
 		},
@@ -500,18 +508,18 @@ func TestFillDefault(t *testing.T) {
 
 	expect = y
 
-	expect.Provision = append(append([]Provision{}, y.Provision...), dExpect.Provision...)
-	expect.Probes = append(append([]Probe{}, y.Probes...), dExpect.Probes...)
-	expect.PortForwards = append(append([]PortForward{}, y.PortForwards...), dExpect.PortForwards...)
-	expect.CopyToHost = append(append([]CopyToHost{}, y.CopyToHost...), dExpect.CopyToHost...)
-	expect.Containerd.Archives = append(append([]File{}, y.Containerd.Archives...), dExpect.Containerd.Archives...)
+	expect.Provision = slices.Concat(y.Provision, dExpect.Provision)
+	expect.Probes = slices.Concat(y.Probes, dExpect.Probes)
+	expect.PortForwards = slices.Concat(y.PortForwards, dExpect.PortForwards)
+	expect.CopyToHost = slices.Concat(y.CopyToHost, dExpect.CopyToHost)
+	expect.Containerd.Archives = slices.Concat(y.Containerd.Archives, dExpect.Containerd.Archives)
 	expect.Containerd.Archives[2].Arch = *expect.Arch
-	expect.AdditionalDisks = append(append([]Disk{}, y.AdditionalDisks...), dExpect.AdditionalDisks...)
-	expect.Firmware.Images = append(append([]FileWithVMType{}, y.Firmware.Images...), dExpect.Firmware.Images...)
+	expect.AdditionalDisks = slices.Concat(y.AdditionalDisks, dExpect.AdditionalDisks)
+	expect.Firmware.Images = slices.Concat(y.Firmware.Images, dExpect.Firmware.Images)
 
 	// Mounts and Networks start with lowest priority first, so higher priority entries can overwrite
-	expect.Mounts = append(append([]Mount{}, dExpect.Mounts...), y.Mounts...)
-	expect.Networks = append(append([]Network{}, dExpect.Networks...), y.Networks...)
+	expect.Mounts = slices.Concat(dExpect.Mounts, y.Mounts)
+	expect.Networks = slices.Concat(dExpect.Networks, y.Networks)
 
 	expect.HostResolver.Hosts["default"] = dExpect.HostResolver.Hosts["default"]
 
@@ -590,7 +598,7 @@ func TestFillDefault(t *testing.T) {
 
 		Mounts: []Mount{
 			{
-				Location: "/var/log",
+				Location: varLog,
 				Writable: ptr.Of(true),
 				SSHFS: SSHFS{
 					Cache:          ptr.Of(false),
@@ -675,20 +683,20 @@ func TestFillDefault(t *testing.T) {
 
 	expect = o
 
-	expect.Provision = append(append(o.Provision, y.Provision...), dExpect.Provision...)
-	expect.Probes = append(append(o.Probes, y.Probes...), dExpect.Probes...)
-	expect.PortForwards = append(append(o.PortForwards, y.PortForwards...), dExpect.PortForwards...)
-	expect.CopyToHost = append(append(o.CopyToHost, y.CopyToHost...), dExpect.CopyToHost...)
-	expect.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), dExpect.Containerd.Archives...)
+	expect.Provision = slices.Concat(o.Provision, y.Provision, dExpect.Provision)
+	expect.Probes = slices.Concat(o.Probes, y.Probes, dExpect.Probes)
+	expect.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, dExpect.PortForwards)
+	expect.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, dExpect.CopyToHost)
+	expect.Containerd.Archives = slices.Concat(o.Containerd.Archives, y.Containerd.Archives, dExpect.Containerd.Archives)
 	expect.Containerd.Archives[3].Arch = *expect.Arch
-	expect.AdditionalDisks = append(append(o.AdditionalDisks, y.AdditionalDisks...), dExpect.AdditionalDisks...)
-	expect.Firmware.Images = append(append(o.Firmware.Images, y.Firmware.Images...), dExpect.Firmware.Images...)
+	expect.AdditionalDisks = slices.Concat(o.AdditionalDisks, y.AdditionalDisks, dExpect.AdditionalDisks)
+	expect.Firmware.Images = slices.Concat(o.Firmware.Images, y.Firmware.Images, dExpect.Firmware.Images)
 
 	expect.HostResolver.Hosts["default"] = dExpect.HostResolver.Hosts["default"]
 	expect.HostResolver.Hosts["MY.Host"] = dExpect.HostResolver.Hosts["host.lima.internal"]
 
 	// o.Mounts just makes dExpect.Mounts[0] writable because the Location matches
-	expect.Mounts = append(append([]Mount{}, dExpect.Mounts...), y.Mounts...)
+	expect.Mounts = slices.Concat(dExpect.Mounts, y.Mounts)
 	expect.Mounts[0].Writable = ptr.Of(true)
 	expect.Mounts[0].SSHFS.Cache = ptr.Of(false)
 	expect.Mounts[0].SSHFS.FollowSymlinks = ptr.Of(true)
@@ -702,7 +710,7 @@ func TestFillDefault(t *testing.T) {
 	expect.MountInotify = ptr.Of(true)
 
 	// o.Networks[1] is overriding the dExpect.Networks[0].Lima entry for the "def0" interface
-	expect.Networks = append(append(dExpect.Networks, y.Networks...), o.Networks[0])
+	expect.Networks = slices.Concat(dExpect.Networks, y.Networks, []Network{o.Networks[0]})
 	expect.Networks[0].Lima = o.Networks[1].Lima
 
 	// Only highest prio DNS are retained
diff --git a/pkg/limayaml/limayaml_test.go b/pkg/limayaml/limayaml_test.go
index 20e2c774..345dee54 100644
--- a/pkg/limayaml/limayaml_test.go
+++ b/pkg/limayaml/limayaml_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
diff --git a/pkg/limayaml/load_test.go b/pkg/limayaml/load_test.go
index c5bb5c2d..8f96a069 100644
--- a/pkg/limayaml/load_test.go
+++ b/pkg/limayaml/load_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
diff --git a/pkg/limayaml/marshal_test.go b/pkg/limayaml/marshal_test.go
index dda6d2b5..29f2f72c 100644
--- a/pkg/limayaml/marshal_test.go
+++ b/pkg/limayaml/marshal_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go
index 4895531e..3a690510 100644
--- a/pkg/limayaml/validate_test.go
+++ b/pkg/limayaml/validate_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package limayaml
 
 import (
@@ -46,6 +49,37 @@ func TestValidateProbes(t *testing.T) {
 
 	err = Validate(y, false)
 	assert.Error(t, err, "field `probe[0].script` must start with a '#!' line")
+
+	invalidProbe = `probes: [{file: {digest: decafbad}}]`
+	y, err = Load([]byte(invalidProbe+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.Error(t, err, "field `probe[0].file.digest` support is not yet implemented")
+}
+
+func TestValidateProvisionData(t *testing.T) {
+	images := `images: [{location: /}]`
+	validData := `provision: [{mode: data, path: /tmp, content: hello}]`
+	y, err := Load([]byte(validData+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.NilError(t, err)
+
+	invalidData := `provision: [{mode: data, content: hello}]`
+	y, err = Load([]byte(invalidData+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.Error(t, err, "field `provision[0].path` must not be empty when mode is \"data\"")
+
+	invalidData = `provision: [{mode: data, path: /tmp, content: hello, permissions: 9}]`
+	y, err = Load([]byte(invalidData+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.ErrorContains(t, err, "provision[0].permissions` must be an octal number")
 }
 
 func TestValidateAdditionalDisks(t *testing.T) {
diff --git a/pkg/lockutil/lockutil_test.go b/pkg/lockutil/lockutil_test.go
index 25225152..b72ad6b6 100644
--- a/pkg/lockutil/lockutil_test.go
+++ b/pkg/lockutil/lockutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package lockutil
 
 import (
diff --git a/pkg/logrusutil/logrusutil_test.go b/pkg/logrusutil/logrusutil_test.go
index 84e95c74..a2fb381e 100644
--- a/pkg/logrusutil/logrusutil_test.go
+++ b/pkg/logrusutil/logrusutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package logrusutil
 
 import (
diff --git a/pkg/nativeimgutil/fuzz_test.go b/pkg/nativeimgutil/fuzz_test.go
index 82d021d6..7a3face5 100644
--- a/pkg/nativeimgutil/fuzz_test.go
+++ b/pkg/nativeimgutil/fuzz_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package nativeimgutil
 
 import (
diff --git a/pkg/nativeimgutil/nativeimgutil_test.go b/pkg/nativeimgutil/nativeimgutil_test.go
index 64df728b..71c4d4d1 100644
--- a/pkg/nativeimgutil/nativeimgutil_test.go
+++ b/pkg/nativeimgutil/nativeimgutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package nativeimgutil
 
 import (
diff --git a/pkg/networks/commands_darwin_test.go b/pkg/networks/commands_darwin_test.go
index d44179e1..5ee47ffb 100644
--- a/pkg/networks/commands_darwin_test.go
+++ b/pkg/networks/commands_darwin_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/networks/commands_test.go b/pkg/networks/commands_test.go
index 7ec20c9f..b3b958b5 100644
--- a/pkg/networks/commands_test.go
+++ b/pkg/networks/commands_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/networks/config_test.go b/pkg/networks/config_test.go
index e8057429..75c5f94c 100644
--- a/pkg/networks/config_test.go
+++ b/pkg/networks/config_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package networks
 
 import (
diff --git a/pkg/networks/usernet/config_test.go b/pkg/networks/usernet/config_test.go
index 164622cd..832085a4 100644
--- a/pkg/networks/usernet/config_test.go
+++ b/pkg/networks/usernet/config_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usernet
 
 import (
diff --git a/pkg/networks/usernet/dnshosts/dnshosts_test.go b/pkg/networks/usernet/dnshosts/dnshosts_test.go
index c8e1984d..9e6f3261 100644
--- a/pkg/networks/usernet/dnshosts/dnshosts_test.go
+++ b/pkg/networks/usernet/dnshosts/dnshosts_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 // From https://raw.githubusercontent.com/abiosoft/colima/v0.5.5/daemon/process/gvproxy/dnshosts_test.go
 /*
 	MIT License
diff --git a/pkg/networks/usernet/gvproxy_test.go b/pkg/networks/usernet/gvproxy_test.go
index 11f7863c..bc4e77df 100644
--- a/pkg/networks/usernet/gvproxy_test.go
+++ b/pkg/networks/usernet/gvproxy_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package usernet
 
 import (
diff --git a/pkg/osutil/machineid_test.go b/pkg/osutil/machineid_test.go
index 2ac71806..0e2a2162 100644
--- a/pkg/osutil/machineid_test.go
+++ b/pkg/osutil/machineid_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/osutil/user_test.go b/pkg/osutil/user_test.go
index fe1aa054..0cf1cc57 100644
--- a/pkg/osutil/user_test.go
+++ b/pkg/osutil/user_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package osutil
 
 import (
diff --git a/pkg/ptr/ptr_test.go b/pkg/ptr/ptr_test.go
index 7bad73ad..936169cd 100644
--- a/pkg/ptr/ptr_test.go
+++ b/pkg/ptr/ptr_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package ptr
 
 import (
diff --git a/pkg/qemu/imgutil/imgutil_test.go b/pkg/qemu/imgutil/imgutil_test.go
index b9c8dfc6..bb630f48 100644
--- a/pkg/qemu/imgutil/imgutil_test.go
+++ b/pkg/qemu/imgutil/imgutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package imgutil
 
 import (
diff --git a/pkg/qemu/qemu_test.go b/pkg/qemu/qemu_test.go
index 4f2dd8ee..422e1fbb 100644
--- a/pkg/qemu/qemu_test.go
+++ b/pkg/qemu/qemu_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package qemu
 
 import (
diff --git a/pkg/sshutil/sshutil_test.go b/pkg/sshutil/sshutil_test.go
index ef2b754d..972c3037 100644
--- a/pkg/sshutil/sshutil_test.go
+++ b/pkg/sshutil/sshutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package sshutil
 
 import (
diff --git a/pkg/store/fuzz_test.go b/pkg/store/fuzz_test.go
index f4527891..511bbdb7 100644
--- a/pkg/store/fuzz_test.go
+++ b/pkg/store/fuzz_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package store
 
 import (
diff --git a/pkg/store/instance_test.go b/pkg/store/instance_test.go
index bdbbba7d..b0b87c1c 100644
--- a/pkg/store/instance_test.go
+++ b/pkg/store/instance_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package store
 
 import (
diff --git a/pkg/templatestore/templatestore.go b/pkg/templatestore/templatestore.go
index 4ee0d87b..104d26b7 100644
--- a/pkg/templatestore/templatestore.go
+++ b/pkg/templatestore/templatestore.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package templatestore
 
 import (
@@ -5,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"unicode"
 
 	securejoin "github.com/cyphar/filepath-securejoin"
 	"github.com/lima-vm/lima/pkg/usrlocalsharelima"
@@ -20,11 +24,17 @@ func Read(name string) ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	yamlPath, err := securejoin.SecureJoin(filepath.Join(dir, "templates"), name+".yaml")
+	ext := filepath.Ext(name)
+	// Append .yaml extension if name doesn't have an extension, or if it starts with a digit.
+	// So "docker.sh" would remain unchanged but "ubuntu-24.04" becomes "ubuntu-24.04.yaml".
+	if len(ext) < 2 || unicode.IsDigit(rune(ext[1])) {
+		name += ".yaml"
+	}
+	filePath, err := securejoin.SecureJoin(filepath.Join(dir, "templates"), name)
 	if err != nil {
 		return nil, err
 	}
-	return os.ReadFile(yamlPath)
+	return os.ReadFile(filePath)
 }
 
 const Default = "default"
diff --git a/pkg/textutil/textutil_test.go b/pkg/textutil/textutil_test.go
index 033e2d29..b8784ece 100644
--- a/pkg/textutil/textutil_test.go
+++ b/pkg/textutil/textutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package textutil
 
 import (
diff --git a/pkg/version/versionutil/versionutil_test.go b/pkg/version/versionutil/versionutil_test.go
index 46f69a79..d6526ae8 100644
--- a/pkg/version/versionutil/versionutil_test.go
+++ b/pkg/version/versionutil/versionutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package versionutil
 
 import (
diff --git a/pkg/vz/network_darwin_test.go b/pkg/vz/network_darwin_test.go
index ead4135e..7e8c976c 100644
--- a/pkg/vz/network_darwin_test.go
+++ b/pkg/vz/network_darwin_test.go
@@ -1,5 +1,8 @@
 //go:build darwin && !no_vz
 
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package vz
 
 import (
diff --git a/pkg/yqutil/fuzz_test.go b/pkg/yqutil/fuzz_test.go
index 0ac06321..368f85ba 100644
--- a/pkg/yqutil/fuzz_test.go
+++ b/pkg/yqutil/fuzz_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package yqutil
 
 import (
diff --git a/pkg/yqutil/yqutil_test.go b/pkg/yqutil/yqutil_test.go
index e1055435..e1785cad 100644
--- a/pkg/yqutil/yqutil_test.go
+++ b/pkg/yqutil/yqutil_test.go
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
 package yqutil
 
 import (