diff --git a/cmd/limactl/autostart.go b/cmd/limactl/autostart.go
new file mode 100644
index 00000000000..1e3d5c7cf73
--- /dev/null
+++ b/cmd/limactl/autostart.go
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+ "github.com/spf13/cobra"
+)
+
+func newAutostartCommand() *cobra.Command {
+ autostartCommand := &cobra.Command{
+ Use: "autostart",
+ Short: "Manage automatic startup of Lima instances",
+ GroupID: advancedCommand,
+ }
+ autostartCommand.AddCommand(newAutostartEnableCommand(), newAutostartDisableCommand())
+ return autostartCommand
+}
+
+func newAutostartEnableCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "enable INSTANCE",
+ Short: "Register an instance to start automatically",
+ Args: WrapArgsError(cobra.ExactArgs(1)),
+ RunE: autostartEnableAction,
+ ValidArgsFunction: autostartComplete,
+ }
+ flags := cmd.Flags()
+ flags.String(
+ "condition", "login",
+ "When to start the instance: \"login\" (user session) or \"boot\" (system boot, macOS only)",
+ )
+ flags.String(
+ "user", "",
+ "macOS username to run the instance as when --condition=boot (default: $USER)",
+ )
+ return cmd
+}
+
+func newAutostartDisableCommand() *cobra.Command {
+ return &cobra.Command{
+ Use: "disable INSTANCE",
+ Short: "Unregister an instance from automatic startup",
+ Args: WrapArgsError(cobra.ExactArgs(1)),
+ RunE: autostartDisableAction,
+ ValidArgsFunction: autostartComplete,
+ }
+}
+
+func autostartComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return bashCompleteInstanceNames(cmd)
+}
diff --git a/cmd/limactl/autostart_darwin.go b/cmd/limactl/autostart_darwin.go
new file mode 100644
index 00000000000..dc5cfdb9680
--- /dev/null
+++ b/cmd/limactl/autostart_darwin.go
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+
+ "github.com/lima-vm/lima/v2/pkg/autostart"
+ "github.com/lima-vm/lima/v2/pkg/autostart/launchd"
+ "github.com/lima-vm/lima/v2/pkg/store"
+ "github.com/lima-vm/lima/v2/pkg/textutil"
+)
+
+func autostartEnableAction(cmd *cobra.Command, args []string) error {
+ condition, err := cmd.Flags().GetString("condition")
+ if err != nil {
+ return err
+ }
+
+ ctx := cmd.Context()
+ inst, err := store.Inspect(ctx, args[0])
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("instance %q not found", args[0])
+ }
+ return err
+ }
+
+ switch condition {
+ case "login":
+ if err := autostart.RegisterToStartAtLogin(ctx, inst); err != nil {
+ return fmt.Errorf("failed to register instance %#q to start at login: %w", inst.Name, err)
+ }
+ logrus.Infof("Instance %#q registered to start at login", inst.Name)
+ case "boot":
+ userName, err := cmd.Flags().GetString("user")
+ if err != nil {
+ return err
+ }
+ if userName == "" {
+ userName = os.Getenv("USER")
+ }
+ if userName == "" {
+ return errors.New("could not determine user; pass --user")
+ }
+ return daemonInstall(ctx, inst.Name, inst.Dir, userName)
+ default:
+ return fmt.Errorf("unknown condition %q: must be \"login\" or \"boot\"", condition)
+ }
+ return nil
+}
+
+func autostartDisableAction(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ inst, err := store.Inspect(ctx, args[0])
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("instance %q not found", args[0])
+ }
+ return err
+ }
+
+ // Check for a LaunchAgent (login) registration first.
+ if registered, err := autostart.IsRegistered(ctx, inst); err != nil {
+ return err
+ } else if registered {
+ if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil {
+ return fmt.Errorf("failed to unregister instance %#q from start at login: %w", inst.Name, err)
+ }
+ logrus.Infof("Instance %#q unregistered from start at login", inst.Name)
+ return nil
+ }
+
+ // Check for a LaunchDaemon (boot) installation.
+ daemonPath := launchd.GetDaemonPlistPath(inst.Name)
+ if _, err := os.Stat(daemonPath); err == nil {
+ return daemonUninstall(ctx, inst.Name)
+ }
+
+ logrus.Infof("Instance %#q is not registered for automatic startup", inst.Name)
+ return nil
+}
+
+func daemonInstall(ctx context.Context, instName, workDir, userName string) error {
+ selfExe, err := os.Executable()
+ if err != nil {
+ return fmt.Errorf("could not determine limactl path: %w", err)
+ }
+
+ content, err := textutil.ExecuteTemplate(launchd.DaemonTemplate, map[string]string{
+ "Binary": selfExe,
+ "Instance": instName,
+ "WorkDir": workDir,
+ "UserName": userName,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to render daemon plist: %w", err)
+ }
+
+ tmp, err := os.CreateTemp("", "io.lima-vm.daemon.*.plist")
+ if err != nil {
+ return fmt.Errorf("failed to create temp file: %w", err)
+ }
+ defer os.Remove(tmp.Name())
+ if _, err := tmp.Write(content); err != nil {
+ return fmt.Errorf("failed to write temp plist: %w", err)
+ }
+ tmp.Close()
+
+ destPath := launchd.GetDaemonPlistPath(instName)
+ svcTarget := "system/" + launchd.DaemonServiceNameFrom(instName)
+
+ _ = runSudo(ctx, "launchctl", "bootout", svcTarget)
+ if err := runSudo(ctx, "install", "-m", "644", tmp.Name(), destPath); err != nil {
+ return fmt.Errorf("failed to install plist to %s: %w", destPath, err)
+ }
+ if err := runSudo(ctx, "launchctl", "enable", svcTarget); err != nil {
+ return fmt.Errorf("failed to enable LaunchDaemon: %w", err)
+ }
+ if err := runSudo(ctx, "launchctl", "bootstrap", "system", destPath); err != nil {
+ return fmt.Errorf("failed to bootstrap LaunchDaemon: %w", err)
+ }
+
+ logrus.Infof("LaunchDaemon installed for instance %q (runs as %q at boot)", instName, userName)
+ logrus.Infof("Plist: %s", destPath)
+ return nil
+}
+
+func daemonUninstall(ctx context.Context, instName string) error {
+ svcTarget := "system/" + launchd.DaemonServiceNameFrom(instName)
+ destPath := launchd.GetDaemonPlistPath(instName)
+
+ _ = runSudo(ctx, "launchctl", "bootout", svcTarget)
+ _ = runSudo(ctx, "launchctl", "disable", svcTarget)
+ if err := runSudo(ctx, "rm", "-f", destPath); err != nil {
+ return fmt.Errorf("failed to remove %s: %w", destPath, err)
+ }
+ logrus.Infof("LaunchDaemon uninstalled for instance %q", instName)
+ return nil
+}
+
+// runSudo executes a command under sudo, inheriting the terminal so password prompts work.
+func runSudo(ctx context.Context, args ...string) error {
+ cmd := exec.CommandContext(ctx, "sudo", args...) //nolint:gosec // args are constructed internally, not from user input
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Stdin = os.Stdin
+ logrus.Debugf("running: sudo %v", args)
+ return cmd.Run()
+}
diff --git a/cmd/limactl/autostart_others.go b/cmd/limactl/autostart_others.go
new file mode 100644
index 00000000000..a888aa8930b
--- /dev/null
+++ b/cmd/limactl/autostart_others.go
@@ -0,0 +1,67 @@
+//go:build !darwin
+
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+
+ "github.com/lima-vm/lima/v2/pkg/autostart"
+ "github.com/lima-vm/lima/v2/pkg/store"
+)
+
+func autostartEnableAction(cmd *cobra.Command, args []string) error {
+ condition, err := cmd.Flags().GetString("condition")
+ if err != nil {
+ return err
+ }
+ if condition == "boot" {
+ return errors.New("--condition=boot is only supported on macOS")
+ }
+
+ ctx := cmd.Context()
+ inst, err := store.Inspect(ctx, args[0])
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("instance %q not found", args[0])
+ }
+ return err
+ }
+
+ if err := autostart.RegisterToStartAtLogin(ctx, inst); err != nil {
+ return fmt.Errorf("failed to register instance %#q to start at login: %w", inst.Name, err)
+ }
+ logrus.Infof("Instance %#q registered to start at login", inst.Name)
+ return nil
+}
+
+func autostartDisableAction(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ inst, err := store.Inspect(ctx, args[0])
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("instance %q not found", args[0])
+ }
+ return err
+ }
+
+ if registered, err := autostart.IsRegistered(ctx, inst); err != nil {
+ return err
+ } else if !registered {
+ logrus.Infof("Instance %#q is not registered for automatic startup", inst.Name)
+ return nil
+ }
+
+ if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil {
+ return fmt.Errorf("failed to unregister instance %#q from start at login: %w", inst.Name, err)
+ }
+ logrus.Infof("Instance %#q unregistered from start at login", inst.Name)
+ return nil
+}
diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go
index 31f322f6930..9ed78b3eb85 100644
--- a/cmd/limactl/main.go
+++ b/cmd/limactl/main.go
@@ -205,6 +205,7 @@ func newApp() *cobra.Command {
newTemplateCommand(),
newRestartCommand(),
newSudoersCommand(),
+ newAutostartCommand(),
newStartAtLoginCommand(),
newNetworkCommand(),
newCloneCommand(),
diff --git a/cmd/limactl/start-at-login.go b/cmd/limactl/start-at-login.go
index b7ca2aaf0b2..ba9fb013fb8 100644
--- a/cmd/limactl/start-at-login.go
+++ b/cmd/limactl/start-at-login.go
@@ -11,6 +11,7 @@ func newStartAtLoginCommand() *cobra.Command {
startAtLoginCommand := &cobra.Command{
Use: "start-at-login INSTANCE",
Short: "Register/Unregister an autostart file for the instance",
+ Deprecated: "use \"limactl autostart\" instead",
Args: WrapArgsError(cobra.MaximumNArgs(1)),
RunE: startAtLoginAction,
ValidArgsFunction: startAtLoginComplete,
diff --git a/pkg/autostart/autostart_test.go b/pkg/autostart/autostart_test.go
index bc6c274d9c7..170e72095c9 100644
--- a/pkg/autostart/autostart_test.go
+++ b/pkg/autostart/autostart_test.go
@@ -22,6 +22,11 @@ var (
requestStart: launchd.RequestStart,
requestStop: launchd.RequestStop,
}
+ LaunchdDaemon = &TemplateFileBasedManager{
+ filePath: launchd.GetDaemonPlistPath,
+ template: launchd.DaemonTemplate,
+ extraTemplateVars: map[string]string{"UserName": "alice"},
+ }
Systemd = &TemplateFileBasedManager{
filePath: systemd.GetUnitPath,
template: systemd.Template,
@@ -73,6 +78,43 @@ func TestRenderTemplate(t *testing.T) {
Background
+`,
+ GetExecutable: func() (string, error) {
+ return "/limactl", nil
+ },
+ WorkDir: "/some/path",
+ },
+ {
+ Manager: LaunchdDaemon,
+ Name: "render darwin launchd daemon plist",
+ InstanceName: "k3s",
+ Expected: `
+
+
+
+ Label
+ io.lima-vm.daemon.k3s
+ UserName
+ alice
+ ProgramArguments
+
+ /limactl
+ start
+ k3s
+ --foreground
+
+ RunAtLoad
+
+ StandardErrorPath
+ launchd.stderr.log
+ StandardOutPath
+ launchd.stdout.log
+ WorkingDirectory
+ /some/path
+ ProcessType
+ Background
+
+
`,
GetExecutable: func() (string, error) {
return "/limactl", nil
diff --git a/pkg/autostart/launchd/io.lima-vm.daemon.INSTANCE.plist b/pkg/autostart/launchd/io.lima-vm.daemon.INSTANCE.plist
new file mode 100644
index 00000000000..a846980869f
--- /dev/null
+++ b/pkg/autostart/launchd/io.lima-vm.daemon.INSTANCE.plist
@@ -0,0 +1,27 @@
+
+
+
+
+ Label
+ io.lima-vm.daemon.{{ .Instance }}
+ UserName
+ {{ .UserName }}
+ ProgramArguments
+
+ {{ .Binary }}
+ start
+ {{ .Instance }}
+ --foreground
+
+ RunAtLoad
+
+ StandardErrorPath
+ launchd.stderr.log
+ StandardOutPath
+ launchd.stdout.log
+ WorkingDirectory
+ {{ .WorkDir }}
+ ProcessType
+ Background
+
+
diff --git a/pkg/autostart/launchd/launchd.go b/pkg/autostart/launchd/launchd.go
index 3ec52980ed2..afdce13d163 100644
--- a/pkg/autostart/launchd/launchd.go
+++ b/pkg/autostart/launchd/launchd.go
@@ -19,6 +19,9 @@ import (
//go:embed io.lima-vm.autostart.INSTANCE.plist
var Template string
+//go:embed io.lima-vm.daemon.INSTANCE.plist
+var DaemonTemplate string
+
// GetPlistPath returns the path to the launchd plist file for the given instance name.
func GetPlistPath(instName string) string {
return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", os.Getenv("HOME"), ServiceNameFrom(instName))
@@ -95,3 +98,13 @@ func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) {
}
return false, nil
}
+
+// GetDaemonPlistPath returns the path to the system LaunchDaemon plist for the given instance.
+func GetDaemonPlistPath(instName string) string {
+ return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", DaemonServiceNameFrom(instName))
+}
+
+// DaemonServiceNameFrom returns the launchd daemon service name for the given instance name.
+func DaemonServiceNameFrom(instName string) string {
+ return fmt.Sprintf("io.lima-vm.daemon.%s", instName)
+}
diff --git a/pkg/autostart/launchd/launchd_test.go b/pkg/autostart/launchd/launchd_test.go
index b26943cb633..de20ef4a4e0 100644
--- a/pkg/autostart/launchd/launchd_test.go
+++ b/pkg/autostart/launchd/launchd_test.go
@@ -30,3 +30,51 @@ func TestGetPlistPath(t *testing.T) {
})
}
}
+
+func TestGetDaemonPlistPath(t *testing.T) {
+ tests := []struct {
+ Name string
+ InstanceName string
+ Expected string
+ }{
+ {
+ Name: "k3s instance",
+ InstanceName: "k3s",
+ Expected: "/Library/LaunchDaemons/io.lima-vm.daemon.k3s.plist",
+ },
+ {
+ Name: "default instance",
+ InstanceName: "default",
+ Expected: "/Library/LaunchDaemons/io.lima-vm.daemon.default.plist",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.Name, func(t *testing.T) {
+ assert.Equal(t, GetDaemonPlistPath(tt.InstanceName), tt.Expected)
+ })
+ }
+}
+
+func TestDaemonServiceNameFrom(t *testing.T) {
+ tests := []struct {
+ Name string
+ InstanceName string
+ Expected string
+ }{
+ {
+ Name: "k3s instance",
+ InstanceName: "k3s",
+ Expected: "io.lima-vm.daemon.k3s",
+ },
+ {
+ Name: "default instance",
+ InstanceName: "default",
+ Expected: "io.lima-vm.daemon.default",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.Name, func(t *testing.T) {
+ assert.Equal(t, DaemonServiceNameFrom(tt.InstanceName), tt.Expected)
+ })
+ }
+}
diff --git a/pkg/autostart/managers.go b/pkg/autostart/managers.go
index 87480dbb9b0..4d2475ef483 100644
--- a/pkg/autostart/managers.go
+++ b/pkg/autostart/managers.go
@@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
+ "maps"
"os"
"path/filepath"
"runtime"
@@ -51,6 +52,7 @@ type TemplateFileBasedManager struct {
enabler func(ctx context.Context, enable bool, instName string) error
filePath func(instName string) string
template string
+ extraTemplateVars map[string]string
autoStartedIdentifier func() string
requestStart func(ctx context.Context, inst *limatype.Instance) error
requestStop func(ctx context.Context, inst *limatype.Instance) (bool, error)
@@ -118,13 +120,13 @@ func (t *TemplateFileBasedManager) renderTemplate(instName, workDir string, getE
if err != nil {
return nil, err
}
- return textutil.ExecuteTemplate(
- t.template,
- map[string]string{
- "Binary": selfExeAbs,
- "Instance": instName,
- "WorkDir": workDir,
- })
+ data := map[string]string{
+ "Binary": selfExeAbs,
+ "Instance": instName,
+ "WorkDir": workDir,
+ }
+ maps.Copy(data, t.extraTemplateVars)
+ return textutil.ExecuteTemplate(t.template, data)
}
func (t *TemplateFileBasedManager) AutoStartedIdentifier() string {
diff --git a/pkg/autostart/managers_darwin.go b/pkg/autostart/managers_darwin.go
index 8451c4ce8dc..ff998e69319 100644
--- a/pkg/autostart/managers_darwin.go
+++ b/pkg/autostart/managers_darwin.go
@@ -16,3 +16,16 @@ func Manager() autoStartManager {
requestStop: launchd.RequestStop,
}
}
+
+// DaemonManager returns an autostart manager for rendering and tracking system LaunchDaemon plists.
+// The userName is the macOS user the daemon will run as.
+// Note: install/uninstall require privileged operations (writing to /Library/LaunchDaemons/ and
+// interacting with the system launchctl domain) that are handled by the `limactl daemon` CLI
+// command via sudo rather than by this manager directly.
+func DaemonManager(userName string) autoStartManager {
+ return &TemplateFileBasedManager{
+ filePath: launchd.GetDaemonPlistPath,
+ template: launchd.DaemonTemplate,
+ extraTemplateVars: map[string]string{"UserName": userName},
+ }
+}
diff --git a/pkg/autostart/managers_linux.go b/pkg/autostart/managers_linux.go
index bbe334331b2..0996d2c5c02 100644
--- a/pkg/autostart/managers_linux.go
+++ b/pkg/autostart/managers_linux.go
@@ -5,6 +5,11 @@ package autostart
import "github.com/lima-vm/lima/v2/pkg/autostart/systemd"
+// DaemonManager is not supported on Linux; use systemd user services instead.
+func DaemonManager(_ string) autoStartManager {
+ return ¬SupportedManager{}
+}
+
// Manager returns the autostart manager for Linux.
func Manager() autoStartManager {
if systemd.IsRunningSystemd() {
diff --git a/pkg/autostart/managers_others.go b/pkg/autostart/managers_others.go
index f8c864ab397..b35ecfb7619 100644
--- a/pkg/autostart/managers_others.go
+++ b/pkg/autostart/managers_others.go
@@ -9,3 +9,8 @@ package autostart
func Manager() autoStartManager {
return ¬SupportedManager{}
}
+
+// DaemonManager is not supported on this OS.
+func DaemonManager(_ string) autoStartManager {
+ return ¬SupportedManager{}
+}
diff --git a/website/content/en/docs/releases/deprecated.md b/website/content/en/docs/releases/deprecated.md
index 75719d42fa9..b1d6cc462eb 100644
--- a/website/content/en/docs/releases/deprecated.md
+++ b/website/content/en/docs/releases/deprecated.md
@@ -11,6 +11,7 @@ The following features are deprecated:
- Environment variable `LIMA_SSH_OVER_VSOCK`: deprecated in Lima v2.0.2 (Use the YAML property `.ssh.overVsock`)
- YAML property `cpuType`: deprecated in Lima v2.0.0 (Use `vmOpts.qemu.cpuType` instead)
- YAML property `rosetta`: deprecated in Lima v2.0.0 (Use `vmOpts.vz.rosetta` instead)
+- `limactl start-at-login` command: deprecated in Lima v2.2.0 (Use `limactl autostart` instead, which also adds support for starting instances at system boot via `--condition=boot`)
## Removed features
- YAML property `network`: deprecated in [Lima v0.7.0](https://github.com/lima-vm/lima/commit/07e68230e70b21108d2db3ca5e0efd0e43842fbd)
diff --git a/website/content/en/docs/security/_index.md b/website/content/en/docs/security/_index.md
index eb5429bb7c7..74b8c7023bf 100644
--- a/website/content/en/docs/security/_index.md
+++ b/website/content/en/docs/security/_index.md
@@ -28,7 +28,7 @@ sudo softwareupdate --install "Name of the Update"
Alternatively , you can set the [`upgradePackages`](https://github.com/lima-vm/lima/blob/a28905cb1bd332cc7178c30f4e42d4c6bf1b2a34/templates/default.yaml#L181) in your template to `true` for most Linux distributions (except `alpine-iso`, for example).
-> ⚠️ Rapidly updating can reduce exposure to known CVEs, but it can also increase exposure to upstream supply chain compromises (for example, [the XZ backdoor](https://en.wikipedia.org/wiki/XZ_Utils_backdoor)).
+> ⚠️ Rapidly updating can reduce exposure to known CVEs, but it can also increase exposure to upstream supply chain compromises (for example, [the XZ backdoor](https://en.wikipedia.org/wiki/XZ_Utils_backdoor)).
## Security model
diff --git a/website/content/en/docs/usage/autostart.md b/website/content/en/docs/usage/autostart.md
new file mode 100644
index 00000000000..c29d5da06ac
--- /dev/null
+++ b/website/content/en/docs/usage/autostart.md
@@ -0,0 +1,61 @@
+---
+title: Automatic Startup
+weight: 4
+---
+
+| ⚡ Requirement | Lima >= 2.2 |
+|----------------|-------------|
+
+Lima instances can be registered to start automatically using `limactl autostart`.
+Two conditions are supported: `login` (start when the user logs in) and `boot`
+(start at system boot, before any user session). This replaces the older
+`limactl start-at-login` command, which is deprecated as of Lima v2.2.
+
+## Starting instances automatically
+
+Use `limactl autostart enable` to register a Lima instance to start automatically.
+Use `limactl autostart disable` to remove the registration.
+
+### At user login (macOS and Linux)
+
+```bash
+# Register
+limactl autostart enable default
+
+# Unregister
+limactl autostart disable default
+```
+
+On macOS this installs a LaunchAgent in `~/Library/LaunchAgents/`. On Linux it
+installs a systemd user service. The instance starts in the background on the
+next login and on subsequent logins.
+
+### At system boot, without a user session (macOS only)
+
+For headless macOS servers where no user session is expected, use
+`--condition=boot`. This installs a system LaunchDaemon that starts the instance
+at boot, before any user logs in.
+
+```bash
+# Register (prompts for sudo once)
+limactl autostart enable --condition=boot k3s
+
+# Unregister
+limactl autostart disable k3s
+```
+
+The `--user` flag specifies which macOS user the instance runs as (default:
+`$USER`). The plist is installed to
+`/Library/LaunchDaemons/io.lima-vm.daemon..plist`.
+
+## Lima < 2.2
+
+Use `limactl start-at-login` (equivalent to `limactl autostart enable --condition=login`):
+
+```bash
+# Register
+limactl start-at-login default
+
+# Unregister
+limactl start-at-login --enabled=false default
+```