# minimal.dev — full text > Minimal is a declarative isolation runtime for Claude Code, Gemini, Codex, Pi, and more: run multiple agents safely on your stack, powered by a verified software supply chain. This file concatenates every documentation page, blog post and changelog entry on minimal.dev. Each section names the page it mirrors in a `Source:` line; the index of pages is at https://minimal.dev/llms.txt. --- # Getting started > Quickstart for Minimal: install the CLI, initialize a project, and define your toolchain and dependencies for local-first, sandboxed builds. Source: https://minimal.dev/docs/start/1-getting-started Last updated: 2026-07-29 New to Minimal? Like everyday usage, getting started with Minimal is simple. This documentation will walk you through how to install Minimal, how to initialize projects, and how to define your toolchain and dependencies for use across the org. As a local-first, declarative build system, Minimal makes sure that all builds and dev environments run entirely on your machine within isolated sandboxes. Minimal creates a consistent foundation for all users, whether developer, AI agent, or CI, around the org's tech stack. Minimal is available for download on both Linux and macOS. [Learn more.](https://minimal.dev) --- # Install > Install Minimal on Linux (x86_64/aarch64) or macOS (Apple Silicon). Source: https://minimal.dev/docs/start/1.5-install Last updated: 2026-07-29 ## Linux ### Requirements - **x86_64** or **aarch64** architecture - Kernel 5.10 or later with unprivileged user namespaces enabled. Ubuntu 24.04 and later restrict them by default; see the [Linux host setup guide](https://minimal.dev/docs/reference/linux-host-setup) for the fix. ### Install ```shell curl --proto "=https" --tlsv1.2 -fsSL https://go.minimal.dev/stable | sh ``` ### Verify ```shell min --version ``` If installed correctly, it prints the version. ## macOS On macOS, Minimal boots a lightweight Linux microVM via libkrun on Apple's Hypervisor.framework. ### Requirements - **Apple Silicon** (M1 or later). Intel Macs are not supported ### Install Copy and paste the following into your terminal: ```shell curl --proto "=https" --tlsv1.2 -fsSL https://go.minimal.dev/stable | sh ``` ### Verify ```shell min --version ``` If installed correctly, it prints the version. ## Upgrade Re-run the install command for your platform. The installer checks each installed component against the current release and re-downloads only the ones that changed, swapping them into place atomically. Note: upgrading restarts the Minimal daemon, which interrupts any sessions that are currently active. When sessions are running, the installer lists them and asks on your terminal before ending them; declining leaves everything as it was. For a scripted upgrade that must not stop to ask, set `MINIMAL_INSTALL_FORCE_STOP=1`, or pass the flag through the shell: ```shell curl --proto "=https" --tlsv1.2 -fsSL https://go.minimal.dev/stable | sh -s -- --force-stop ``` ## Uninstall ```shell curl --proto "=https" --tlsv1.2 -fsSL https://go.minimal.dev/stable | sh -s -- --uninstall ``` This removes every file a previous install placed, including the shell integration. It accepts `--force` (also remove files you modified), `--purge` (also delete Minimal's data, state, and cache directories), and `--dry-run` (show what would be removed). --- # Migrate to Sessions > Migration to the new architecture after v0.5.0 release for Linux and macOS. Source: https://minimal.dev/docs/start/migrate-to-minimal-session Last updated: 2026-10-06 If you're on a release **older than v0.5.0**, you'll need to update your configuration to use the new Sessions architecture. Check your version with `minimal --version` or `min --version`. ## Upgrading from v0.4.1 or earlier > **Note:** `tasks` are not supported by v0.5.0. If you rely on `tasks` for non-interactive, job-like workflows, keep `minimal` (<=v0.4.1) installed for tasks, and install `min` (v0.5.0) alongside it for sessions. The Minimal command is becoming `min`, which won't conflict with the legacy `minimal` command. `minimal` will keep working for the foreseeable future, but its installer will no longer be available after v0.5.0 ships. To install `min`, see [install](https://minimal.dev/docs/start/1.5-install). If you no longer need `minimal`, uninstall it with: ```shell ./.minimal/shim/uninstall.sh ``` ### Linux: use `mip` in place of `minimal` To keep ephemeral build systems on Linux working, the `min` installer also installs `mip` (Minimal-In-Process), which is functionally identical to `minimal`. Update any scripts using `minimal` to use `mip` instead — see [cli-mip](https://minimal.dev/docs/reference/cli-mip) for full docs. | Old | New | |---|---| | `minimal run` | `mip run` | | `minimal update` | `mip update` | | `minimal add` | `mip add` | | `minimal status` | `mip status` | | `minimal build` | `mip build` | | `minimal test` | `mip test` | | `minimal materialize` | `mip materialize` | | `minimal package` | `mip package` | | `minimal cache` | `mip cache` | | `minimal check` | `mip check` | | `minimal dep` | `mip dep` | | `minimal completions` | `mip completions` | | `minimal help` | `mip help` | The in-VM command `min add` is unchanged. ## Changes to `minimal.toml` Prior to v0.5.0, `minimal.toml` had five sections: `upstream`, `harness`, `defaults`, `outputs`, and `tasks`. | Section | Status | |---|---| | `[upstream]` | Unchanged | | `[harness]` | Renamed to [`[stack]`](https://minimal.dev/docs/concepts/stacks); the `[harness]` spelling is no longer accepted | | `[defaults]` | `profiles` deprecated — see [Profiles](#profiles) | | `[outputs]` | Unchanged | | `[tasks]` | Interactive tasks (bash, shell, interactive agent sessions) moved to [`[session]`](#session-new); non-interactive tasks (test, build) aren't supported in v0.5.0 | ### `[session]` (new) `session` is a new concept in v0.5.0 for interactive environments — shell, bash, and interactive agent entry points. Migrate any task using `shell`, `bash`, or an interactive agent entry point to a session definition; see [the session configuration reference](https://minimal.dev/docs/reference/minimal-dot-toml#session---what-every-contributors-session-gets-session) for how. Unlike `tasks`, which supported multiple definitions each invoked with `minimal run `, a project has only one `session`: ```shell min session activate # creates a session, prints its ID min session attach # enters an existing session min session activate --attach # creates and enters a session in one step ``` ## Profiles Profiles have been deprecated: - `profile.env_vars` → `session.vars` - `profile.packages` → `session.packages` See [the session configuration reference](https://minimal.dev/docs/reference/minimal-dot-toml#session---what-every-contributors-session-gets-session) for how to define a session. --- # Initialize a project > Set up a project with min init, declare your session's tools in minimal.toml, then enter an isolated session with min session activate. Source: https://minimal.dev/docs/start/2-setup Last updated: 2026-08-25 Minimal reads a single `minimal.toml` at the root of your repository. It declares where your software comes from, how the project builds, and what every developer's session gets. Generate a starting point with `min init`, then enter a session. ## Run `min init` From your project directory, run: ```shell min init ``` `min init` inspects the source tree, detects the stack (Go, Rust, pnpm, npm, Bun, Deno, pip, uv, Gradle, Make, and more), and proposes a `minimal.toml`, asking for confirmation before writing it (pass `-y` to skip the prompt): ```toml # Generated by `min init` [upstream] # Source of software & tooling repo = "https://github.com/gominimal/pkgs" branch = "main" locked_commit = "9d36aa3d0fec09658fe3bbc676beeae49a89c53d" [stack] use = "rust" [defaults] state_key = "dev" [session] # min session attach packages = ["base", "vim"] ``` - `[upstream]` pins the software supply chain: the package registry and the exact commit it is read from. - `[stack]` declares how the project builds. See the [stack reference](https://minimal.dev/docs/reference/stack-specs). - `[defaults]` sets build- and task-plane defaults, such as the `state_key` that tasks share persistent state under. - `[session]` is the one that matters for day-to-day work: it lists the tools every contributor's session gets. See the [`minimal.toml` reference](https://minimal.dev/docs/reference/minimal-dot-toml) for the full schema. ## Declare your session's tools Edit the `packages` list under `[session]` to add whatever your team works with. Extend the existing list; do not add a second `[session]` table, since TOML does not allow a table to be defined twice. ```toml [session] # min session attach packages = ["base", "vim", "git", "gh", "ripgrep", "claude-code"] ``` Every name resolves against the [Minimal Public Registry](https://github.com/gominimal/pkgs). Commit `minimal.toml` to your repository so every teammate, and every AI agent, gets an identical session. ## Enter a session Create and attach to an isolated session for the project: ```shell min session activate --attach . ``` Minimal builds or fetches the declared packages, composes them into a sandbox seeded with a copy of your project (streamed in as a tarball), and drops you into a shell inside it. The `[session]` tools are on your `PATH`, and nothing touches the host. Type `exit` to leave; the session stays up, so you can `min session attach` back to it later. Next: [start a dev shell](https://minimal.dev/docs/start/4-dev-shell), or [work alongside an agent](https://minimal.dev/docs/start/5-agents). --- # Dev shell > Enter an isolated interactive development session with only your declared tools. Covers customization, host file access, and environment variables. Source: https://minimal.dev/docs/start/4-dev-shell Last updated: 2026-10-04 In Minimal, your dev shell is a **session**, which is a long-lived, isolated development environment for a project that the `min` CLI drives. It gives you an interactive shell with only the tools you declare, isolated from your host system. Use it for everyday development and ad-hoc commands. You can also use it to debug or to work interactively with your project's toolchain. ## Launch a shell From your project directory, activate a session and attach to it in one step: ```shell $ min session activate --attach . ``` This creates a session for the project and drops you into its interactive shell. The path argument defaults to the current directory, so `min session activate --attach` on its own works too. Activation uploads your project files into the session's workspace, so the shell starts with a copy of your project. Edits you make inside stay in the session. To bring them back to the host, use `git push min://`. Committing inside the session and pulling from it also works. ## What is in the session? A session's tools come from the project's `[session]` block in `minimal.toml`. List the packages every contributor needs when working on this codebase: ```toml [session] packages = ["git", "curl", "ripgrep"] ``` Inside the session you have access to your project's source code and these declared tools, but nothing else from the host system. Any [loadouts](https://minimal.dev/docs/concepts/loadouts) you apply and the packages the project's stack declares also come in. The session cannot globally install software, read unrelated files, or modify your system. Every developer working on the project gets the same isolated environment. ## Customizing your session ### Add more tools Add packages to the project's `[session]` block so everyone picks them up: ```toml [session] packages = ["git", "curl", "ripgrep", "jq", "nano"] ``` To add a package mid-session, run `min add` from inside the running session. You do not need to edit config by hand or restart your shell: ```shell $ min add nano ``` This installs the tool into the running session and records it in the project's `[session]` `packages` list, so the next activation gets it too. See the [`min` in-sandbox commands](https://minimal.dev/docs/reference/sandbox-operations#add) for the full helper reference. ### Access host files By default, only your project directory is available inside the session. To bring specific host files or directories in, declare `patches` in the `[session]` block. Each patch names a `source` on the host and a `dest` under the session user's home directory: ```toml [session] packages = ["git"] patches = [ { source = "~/.gitconfig", dest = "~/.gitconfig" }, { source = "~/.ssh", dest = "~/.ssh" }, ] ``` A relative `source` resolves against the project directory, so `source = "config/myrc"` names a file in the repository. Activation rejects a source that uses `..` to climb out of the project. ### Set environment variables The session starts with a clean environment. By default, host environment variables do not pass through. Set variables explicitly, or inherit them from the host, under `[session.vars]`: ```toml [session.vars] EDITOR = "nano" AWS_PROFILE = { inherit = true } ``` A string value like `EDITOR = "nano"` defines a fixed variable. Using `{ inherit = true }` passes through the value from your host environment, which is useful for credentials or configuration that varies per developer. Inheriting this way is mandatory, not opportunistic. If `AWS_PROFILE` is not set in your host environment, activation fails rather than starting the session without it: ``` error: Composition gating failed: could not resolve pending var `AWS_PROFILE`: environment variable not found ``` Since the project commits `minimal.toml`, that applies to everyone on the project. Write `{ inherit = true, default = "..." }` instead when the variable is truly optional. A loadout's `[vars]` takes the opposite view of the same spelling and only warns. See [Where `{ inherit = true }` differs](https://minimal.dev/docs/reference/minimal-dot-toml#inherit-divergence). ### Run setup on activation To run setup steps when a session comes up, declare lifecycle hooks in the `[session]` block: ```toml [[session.lifecycle_hooks]] on_activate = { type = "inline", value = "cargo fetch >/dev/null 2>&1 || true" } ``` ## Session lifecycle Exit the shell to detach. The session keeps running in the background. To reattach, list, or remove sessions: ```shell $ min session attach # reattach to a session (resolves from the current directory) $ min ls # list sessions $ min session destroy # terminate a session by name or id (or --all) ``` For running one-shot commands in their own sandbox rather than an interactive session, see [Tasks](https://minimal.dev/docs/start/6-tasks). --- # Agent shell > Run AI coding agents like Claude Code inside an isolated session with declared tools and host credentials. Source: https://minimal.dev/docs/start/5-agents Last updated: 2026-07-30 Minimal can sandbox AI coding agents the same way it sandboxes your dev tools: an agent runs inside a [session](https://minimal.dev/docs/start/4-dev-shell), with access to your source code and only the tools and host state you declare. ## Example: Claude Code This documentation site is itself built and maintained using Claude Code inside a Minimal session. Add the agent's package to the project's `[session]` block: ```toml [session] packages = ["claude-code", "base"] ``` Then activate the session and run the agent inside it: ```shell $ min session activate --attach . $ claude ``` Claude Code launches inside the session with your project's source code and a read-only system containing the `claude-code` binary and core utilities from `base`. The session has no additional access to anything on your host system unless you explicitly declare it. Note that sessions are driven interactively: attaching needs a terminal. Launch the agent from inside an attached shell as shown above, rather than scripting it from the host. ## Adding more tools Agents often need additional tools to be effective. Add packages to the `[session]` block just like any other: ```toml [session] packages = ["claude-code", "base", "git", "curl"] ``` ## Passing through host credentials Use `patches` to give the agent access to host files it needs, like authentication state. Each patch maps a host `source` to a `dest` under the session user's home directory: ```toml [session] packages = ["claude-code", "base", "git"] patches = [ { source = "~/.gitconfig", dest = "~/.gitconfig" }, { source = "~/.ssh", dest = "~/.ssh" }, ] ``` Environment variables can be inherited from the host as well, under `[session.vars]`: ```toml [session.vars] ANTHROPIC_API_KEY = { inherit = true } ``` Anything you pass through is available to the agent and to any code it runs: a patched `~/.ssh` means the agent can use your SSH keys, and an inherited `ANTHROPIC_API_KEY` is readable by every process in the session. Pass through the minimum the agent needs, and prefer scoped or short-lived credentials over long-lived ones. Activation helps you hold this line: each value inherited from your environment and each file patch is gated against your user policy, and anything your policy cannot decide is surfaced for approval before it enters the session. ## Why sandbox agents? Running an AI agent in a Minimal session means it can only access the tools and files you declare. It cannot install arbitrary software, read unrelated files, or modify your system. This is the same isolation model that Minimal applies to builds and dev shells, applied to agents. --- # Building > Run builds and tests in sandboxed environments using stacks. Covers extra dependencies, test commands, and build state persistence. Source: https://minimal.dev/docs/start/3-building Last updated: 2026-07-29 Minimal builds your code reproducibly inside a sandbox, using a [stack](https://minimal.dev/docs/concepts/stacks) to wire up the right tools and build commands for your language or build system. A build is a **one-shot** command that runs in its own fresh sandbox and exits, driven by the `mip` CLI (Linux-only; on macOS, work inside a [dev session](https://minimal.dev/docs/start/4-dev-shell) instead). That is different from an interactive dev session, which is a long-lived environment you attach to with `min` for interactive development. ## Running a build ```shell $ mip run build ``` The stack configured in your `minimal.toml` determines what happens. For example, a Rust stack runs `cargo build --release`, while a pnpm stack runs roughly `pnpm install && pnpm build`. ## How stacks work When you declare a stack, it provides: - **Build packages**: compilers, build tools, and other dependencies needed during compilation - **Runtime packages**: libraries needed wherever the built software runs - **Build commands**: the default commands executed by `mip run build` - **Environment variables**: compiler flags, paths, and other configuration ```toml [stack] use = "go" ``` With just this configuration, `mip run build` will run `go build` with the Go compiler and all necessary toolchain packages available in the sandbox. ## Adding extra dependencies Most projects need additional dependencies beyond what the stack provides. Declare them in the `[stack]` section: ```toml [stack] use = "rust" build_packages = ["protobuf", "perl"] runtime_packages = ["openssl"] ``` Or add them with the CLI: ```shell $ min add --build protobuf $ min add --runtime openssl ``` ## Running tests Similarly to `mip run build`, you can run your test suite with: ```shell $ mip run test ``` This runs the `test` task from your `minimal.toml`; define one with `[tasks.test]`. Stacks only ship a default `build` task. ## Persisting build state By default, each task invocation starts from a clean state. To cache build artifacts across runs (like `node_modules` or `target/`), set a `state_key`: ```toml [defaults] state_key = "dev" ``` Tasks sharing the same `state_key` share cached state, so your builds do not start from scratch every time. --- # Tasks > Define named tasks in minimal.toml that run commands in sandboxed environments with specific packages and env vars. Source: https://minimal.dev/docs/start/6-tasks Last updated: 2026-08-28 Tasks define commands that run in a [sandbox](https://minimal.dev/docs/concepts/sandboxing) with exactly your declared dependencies. Every developer on your team runs the same task in the same environment. A task is a **one-shot** command that runs in its own fresh sandbox and exits, driven by the `mip` CLI (Linux-only; inside a session, `min task run ` runs the same tasks on any platform; from the host, `min session run ` runs one against a session you already have; the bare `min run ` is a legacy alias). That makes it different from an interactive [dev session](https://minimal.dev/docs/start/4-dev-shell), which is a long-lived environment you attach to with `min`. Reach for a task to run builds, tests, linters, and deploys; reach for a session for interactive development. ## Defining a task Tasks are defined in your `minimal.toml` and run with `mip run `: ```toml [tasks.lint] packages = ["python", "ruff"] exec = "ruff check ." [tasks.greet] bash = "echo Hello from $(uname -s) && date" ``` ```shell $ mip run greet Hello from Linux Tue Mar 17 12:00:00 UTC 2026 ``` Use `exec` for a single command, or `bash` for shell scripts. ## Environment variables Set environment variables directly, or inherit them from the host: ```toml [tasks.deploy] packages = ["railway"] exec = "railway up" env_vars.RAILS_ENV = "production" env_vars.GITHUB_TOKEN = { inherit = true } ``` `inherit = true` copies the value from your host environment into the task's sandbox, where every command the task runs can read it — including scripts the repository itself provides. Inherit sparingly, and prefer scoped, short-lived tokens over long-lived ambient credentials. ## Mapping host files Use `patches` to give a task access to specific files or directories on the host: ```toml [tasks.deploy] packages = ["railway"] exec = "railway up" patches.dir."~/.config/railway" = "read-only" ``` Map host files `read-only` unless the task genuinely needs to write them: a `read-write` mapping lets anything the task runs modify those host files in place. See the [tasks reference](https://minimal.dev/docs/reference/tasks) for the full schema. --- # Packages > Add build, runtime, session, and task-specific package dependencies using the min and mip CLIs or minimal.toml. Source: https://minimal.dev/docs/start/7-packages Last updated: 2026-07-29 Minimal makes it easy to add tools and dependencies to your project. Where a package lives depends on when you need it: building your code, running it, developing interactively, or inside a specific task. ## Build and runtime dependencies Build and runtime dependencies belong to your build plane and are managed with `min add`: ```shell # Add as a build dependency (needed to compile your code) $ min add --build protobuf # Add as a runtime dependency (needed wherever your code runs) $ min add --runtime openssl ``` Each command updates your `minimal.toml` automatically. **Build dependencies** are packages needed during compilation but not at runtime. They are declared in the `[stack]` section: ```toml [stack] use = "rust" build_packages = ["protobuf", "perl"] ``` **Runtime dependencies** are packages that must be present wherever the built software runs: ```toml [stack] use = "rust" runtime_packages = ["openssl"] ``` ## Session tools Tools you want available while developing interactively go in the project's `[session]` block, not the build plane. Every contributor's [dev session](https://minimal.dev/docs/start/4-dev-shell) picks them up: ```toml [session] packages = ["git", "ripgrep", "jq"] ``` To add a tool to a running session without editing config, run `min add ` from inside the session shell. ## Task-specific packages Packages can also be scoped to individual [tasks](https://minimal.dev/docs/reference/tasks), where they are only available when that task runs. The task must already be declared in `minimal.toml` — `min add --task ` updates an existing task's `packages` list, it does not create the task: ```toml [tasks.lint] exec = "ruff check ." ``` Add a package to the declared task with `min add --task `: ```shell $ min add --task lint ruff ``` The task's `packages` list is updated in place: ```toml [tasks.lint] exec = "ruff check ." packages = ["ruff"] ``` ## Available packages The [Minimal Public Package Registry](https://github.com/gominimal/pkgs/tree/main/packages) contains hundreds of packages including compilers, interpreters, build tools, and common CLI utilities. --- # Software supply chain > Layered, composable model for declarative software dependencies, from public registries to organization-internal packages. Source: https://minimal.dev/docs/concepts/software-supply-chain Last updated: 2026-08-17 To enable declarative, reproducible, and understandable software in Minimal, all software exists within a layered model we refer to as the _software supply chain_. The lowest layer of the software supply chain is your codebase, within which you invoke commands in the Minimal CLI. Each layer depends on configuration and software in the layer above it, which is itself a codebase. Each codebase links to the layer above it via the `[upstream]` section in the [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) file: ```toml [upstream] repo = "" branch = "" locked_commit = "" ``` These all form the software supply chain. The most common upstream is our public packages registry: ```toml [upstream] # Source of software & tooling repo = "https://github.com/gominimal/pkgs" branch = "main" # locked_commit is filled in when you run `min update` ``` For more information, check out the [minimal.toml page](https://minimal.dev/docs/reference/minimal-dot-toml) in the References section. ## Software is configuration Software in any real environment is more complicated than binaries copied to disk, and these nuances are the basis of how Minimal operates. By building security into the way that products are created, Minimal makes software secure by default and removes pressure from the engineering staff. A piece of software is encapsulated in a [package](https://minimal.dev/docs/concepts/packages), defined at any layer of the software supply chain and usable by packages at that layer or below. Packages themselves are defined by build specifications: config-as-code that describes the detailed semantics of what the software needs to function as well as an exact, reproducible description of how to build the software. Dependencies of packages are references to other packages, an approach that enables Minimal to build exact production and development environments from simple, composable descriptions of individual packages. For readers familiar with Nix/Nixpkgs, package build-specs are similar to Nix derivations. ## Everything is (composable) configuration In addition to declaring packages, each software supply chain layer also declares [stacks](https://minimal.dev/docs/concepts/stacks). Each layer inherits the definitions from the layers above it and adds its own on top; defining the same name twice is an error. Your codebase can use packages from the public registry while defining its own stacks, and an organization's internal registry can add private packages on top of the public one. This composability means you can: - **Use the public registry as-is** for standard open-source tooling - **Add an internal layer** with organization-specific packages, stacks, or policies - **Define project-local packages** for software unique to your codebase --- # Sandboxing > How Minimal isolates all builds and tasks from the host system using hermetic sandboxes. Source: https://minimal.dev/docs/concepts/sandboxing Last updated: 2026-08-17 Sandboxing is essential to make sure all builds and tasks are insulated from the machine they run in. **In Minimal, all tasks and builds run in a sandbox**. ## Package builds Minimal packages encapsulate all tooling and software, so it's essential they are compiled in a hermetically-sealed environment that gives the rest of the ecosystem a strong foundation. As such, package builds take place in a cleanroom sandbox that shares nothing with the host machine, aside from network access when a dependency calls for it. Specifically, the cleanroom sandbox wires: - Files representing the build inputs and runtime dependencies of the package - Working directories `/build`, `/tmp`, and an empty `/state`. - The source of the package being built - Network connectivity (when called-for by a dependency) At the completion of the build, artifacts are gathered based on the outputs specified in the packages' build-spec, and are cached for later consumption when needed by a task or another package build. By default, Minimal is configured to fetch completed builds from our binary cache, to avoid a slow process building everything locally the first time it is needed. You can force builds to run locally with the build CLI's `--no-fetch` and `--no-cache` flags. ## The task sandbox When a task is invoked, its configuration is used to setup and launch a task sandbox. This sandbox wires: - Files representing the packages requested and their runtime dependencies. The packages requested for a task includes any that are explicitly defined on the task, and those defined by the repository stack (if set). - The repository's files and directories, from the repository root downward, but not above it. - A `/state` directory, which can be shared between tasks and task invocations by specifying a task `state_key`. Package managers are typically wired to cache source downloads and intermediate build artifacts in this directory. - Pinhole filesystem mappings, as declared by packages. - Network connectivity when necessary. ## The session sandbox A [session](https://minimal.dev/docs/concepts/sessions) is hosted in a sandbox of its own: the one you drop into when you attach a shell. Its contents are the union of the project's `[session]` packages, the packages the repository's `[stack]` declares, and whatever your applied [loadouts](https://minimal.dev/docs/concepts/loadouts) contribute. The session sandbox's working directory is the session's workspace, seeded with a copy of your project files at activation. --- # Sessions > What a Minimal session is, how a provider creates and hosts one, and how sessions compose from your project config plus loadouts. Source: https://minimal.dev/docs/concepts/sessions Last updated: 2026-10-07 A **session** is Minimal's isolated developer environment: a durable place to work on a project, with exactly the tools and configuration that project declares, and nothing else from your host leaking in unless you allow it. You create a session for a project, attach a shell to it, and do your work inside it; detach and the session keeps running in the background, ready to rejoin. When you are done you tear it down. Sessions are managed by `min`, the Minimal session CLI. This guide explains what a session is, the provider that hosts it, how one is created and entered, how it is composed from your project config plus loadouts, and how sessions relate to sandboxes and tasks. For the exact flags of every command mentioned here, see the [`min` command reference](https://minimal.dev/docs/reference/cli-min). ## Why sessions Working directly on your host mixes every project's tools, environment variables, and stray state into one global namespace. Minimal instead gives each project its own isolated environment: - **Reproducible.** A session's tools come from Minimal packages, built or fetched deterministically from your project's declarative config. Two people who activate the same project get the same environment. - **Isolated.** Everything that runs in a session runs in a sandbox. Your host filesystem, host environment, and other projects are not visible inside it unless the project or your policy brings them in. - **Declarative.** What a session contains is derived from files you commit (your `minimal.toml` and the packages it references), not from setup steps you run by hand and hope to remember. Declarative does not mean one-size-fits-all: **loadouts** let you bring your own tools — your editor, your shell configuration — into a session without those personal choices contaminating the project's declared environment. See the [loadouts guide](https://minimal.dev/docs/concepts/loadouts). ## The provider and session model Two roles are in play whenever you use sessions: - A **session** is the durable environment itself: a single isolated workspace tied to one project directory. - A **provider** is the daemon that creates and hosts sessions. On Linux the provider is `minimald`, an SSH server that owns session lifecycle and hosts the shells and executions that run inside each session. The `min` CLI is a thin client. It discovers the provider's socket, auto-spawns a provider if none is running, and speaks to it over SSH on a UNIX domain socket. You rarely interact with the provider directly: running a `min` command is enough, and the provider comes up on demand. On platforms where `minimald` cannot run natively (macOS, or a Linux setup that opts into it), a second daemon, `minvmd`, boots a lightweight Linux microVM and runs `minimald` inside it, bridging the connection so `min` reaches the session without needing to know a VM is involved. From the user's point of view the model is identical: a provider creates and hosts sessions, and `min` talks to it. ## Creating and entering a session You create a session by **activating** a project. From the project directory: ```console $ min session activate --attach ``` `min session activate` takes a project path that defaults to the current directory. `--attach` drops you straight into a shell in the new session once it is ready. Without `--attach`, activation prints the new session's id and returns, leaving the session ready in the background for you to attach to later. A few things happen during activation: 1. `min` resolves the project path and, if the project has no `minimal.toml` yet, offers to scaffold one on an interactive terminal; when prompts are skipped (`--no-input`, or a non-terminal stdin) it prints a notice and continues without writing anything to the directory. Either way the session still comes up with a default environment. 2. Your project files are uploaded into the session's workspace on the provider so it can read your config. You can opt out of this with `--sync none`. 3. The session is composed from your project config plus any loadouts (see the next section), and any environment values that need your approval are gated against your user policy. Useful activation options: - `--name ` gives the session a stable, human-friendly name you can use in place of its id. - `--loadout ` applies a named loadout (repeatable); `--no-loadouts` applies none. - `--no-prompt` fails instead of prompting when composition surfaces items your policy cannot auto-decide, printing a ready-to-paste policy snippet instead. This mode is selected automatically when stdout or stderr is not a terminal, so activation is safe to run from scripts and CI. To enter a session that already exists, **attach** to it by id or name: ```console $ min session attach my-session ``` Attach opens an interactive shell inside the session, with your tools on `PATH` and the session's workspace (your project files, uploaded at activation) as your working directory. That shell is bash unless a loadout asks for another one it has installed — set `SHELL` in `[vars]` to be dropped into zsh, fish, nushell, or POSIX `sh` instead ([reference](https://minimal.dev/docs/reference/loadouts#session-shell)). Exiting the shell detaches: the session keeps running in the background, and `min session attach` rejoins it later. Changes you make inside stay in the session's workspace; bring them back to the host by pushing them out with `git push min://`. Activating a project whose directory already has a session does not reuse that session: `min` warns, names the existing session, and creates a second one anyway. Once two sessions share a path, `min` can no longer resolve that directory to a single session, so a bare `min` there is ambiguous and `min session attach` opens a picker to choose between them (erroring when `--no-input` is set or stdin/stdout is not a terminal). Attach to the existing session instead of activating again when you mean to rejoin it. ## How a session is composed A session's contents are not hand-configured. They are **composed** from several declarative sources into a single result the provider applies when it mints your shell. The composition is a pipeline that spans the `min` client and the provider: 1. **Your loadouts, composed on the client.** A loadout is a reusable bundle of session contributions (environment variables, file mappings, package selections, and lifecycle hooks) kept in your user config. `min` composes the loadouts you selected, resolving values and gating them against your user policy. See the [loadouts guide](https://minimal.dev/docs/concepts/loadouts) for how to write and select them. 2. **Your project config, collected on the provider.** The provider reads your project's `minimal.toml` from the uploaded workspace: its session block, the packages the project's stack declares as build and runtime dependencies, and any environment the stack contributes. 3. **Package contributions, collected on the provider.** Packages in the project's dependency closure can contribute environment wiring and file mappings of their own. These are collected alongside the project's own contributions. The provider routes anything that needs your approval (chiefly environment values sourced from your own environment, and file patches) back to `min`, which gates them against your user policy and, when the policy cannot decide, prompts you. Approved items, plus the selections that were already decided, are assembled into the final composition. The result carries four kinds of item: - **Variables**: environment variables set inside the session. - **File patches**: files copied into the session's home when it is finalized. - **Packages**: the tools and libraries that make up the environment. - **Lifecycle hooks**: scripts declared to run at defined points in a session's life. Hooks are inert in the current release: a hook declared in a project's `[session]` block is composed but never executed, and hooks declared in loadouts are excluded from composition entirely. A key principle: **your user policy is enforced only on the client.** The provider never runs your policy. It forwards the items that need a decision, and `min` applies your allow, deny, and ignore rules, prompting you for anything left undecided. Every surviving item keeps a record of which source contributed it, so a session's contents can always be traced back to your loadout, your project, or a specific package. Environment values that carry your own environment (for example a variable your loadout inherits from your shell) are the ones your policy gates, because they move data from your host into the sandbox. Values a package or project hardcodes as literals are a matter of that declaration, not of your policy, and pass through without prompting. ## Session lifecycle List the sessions the provider is currently hosting: ```console $ min ls ``` `min ls` prints a table of session ids, names, titles, and last activity, along with the shared resource pool. `--raw` prints bare ids one per line for scripting, and `--json` prints the full list as JSON. Rename a session: ```console $ min session rename my-session backend-work ``` Destroy a session when you are finished with it: ```console $ min session destroy my-session ``` `min session destroy` terminates a single session by id or name. `min session destroy --all` tears down every session at once (add `--force` to skip the confirmation). Stop the provider itself: ```console $ min stop ``` `min stop` shuts down the active provider. Stopping the provider ends every running shell, but the sessions themselves survive: their records and workspaces persist, and a provider that comes back up re-hosts them (the extras composed from loadouts are rebuilt when you re-activate). The provider refuses to shut down while any session is busy (a live shell, or an activation in flight) unless you pass `--force`. Contrast this with `min session destroy`, which removes one session entirely and leaves the provider running to host the rest. Because `min` auto-spawns a provider on demand, the next `min` command after a stop simply brings one back up. You can inspect a session's effective networking rules, rendered as text. Pass `-o json` for the machine-readable document; see the CLI reference's [`session policy`](https://minimal.dev/docs/reference/cli-min#session-policy) section: ```console $ min session policy my-session ``` And list the loadouts available in your user config: ```console $ min loadout list ``` ## Sessions, sandboxes, and tasks A session is not itself a sandbox: it is the managed, named environment the provider hosts. But everything that runs *inside* a session runs in a **sandbox**, the low-level isolation primitive built from Linux user and mount namespaces. The first time you attach a shell, the provider composes a sandbox whose root filesystem is assembled from the package closure your session needs (each package's files hardlinked into place), mounts the session's workspace (your uploaded project files) into it, sets the composed environment variables, and launches your shell there. Detaching leaves that sandbox running; reattaching joins it. The sandbox is how the session's isolation and reproducibility are actually delivered. **Tasks** run in sandboxes of their own. A task is a command your project defines in its `minimal.toml`: a named, repeatable operation (a build step, a test run, a linter) that executes in a freshly composed sandbox built from the packages that task needs. If a needed package is not present, Minimal builds it or fetches it from a remote cache first. Loadouts contribute to the session's interactive shell only, never to tasks, so your personal tooling cannot influence a task's deterministic execution. Running `min task run ` is the non-interactive way to trigger a declared task; `min session run ` runs one against a session you already have, rather than composing a task session of its own. The through-line: a **session** is the durable, named environment a provider hosts; a **sandbox** is the isolated execution context composed from a package closure; and a **task** is a declared command that runs in such a sandbox. Your project config and loadouts decide what goes into all three. ## See also - [`min` command reference](https://minimal.dev/docs/reference/cli-min) for every command and flag. - [Loadouts](https://minimal.dev/docs/concepts/loadouts) for writing and selecting reusable session contributions. --- # Loadouts > Loadouts are per-developer, session-level bundles of packages, vars, file patches, and lifecycle hooks that layer your personal environment on top of a project's shared config. Source: https://minimal.dev/docs/concepts/loadouts Last updated: 2026-09-20 A **loadout** is your personal layer on a session. The project's [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) describes what every contributor's [session](https://minimal.dev/docs/concepts/sessions) needs to build and run the code. A loadout describes what *you* want on top of that: your editor, your terminal multiplexer, your shell prompt, your dotfiles. When you activate a session, the loadout is layered in, so the environment comes up matching your muscle memory without changing anything the rest of the team sees. Loadouts are deliberately per-developer. They live in your user config directory, not in the repository, and they never travel with the project. Two people working in the same codebase can activate the same session definition and still get their own editors, aliases, and prompts. ## How a loadout differs from `minimal.toml` It helps to keep the two roles straight: - The project's `minimal.toml` is **shared and checked in**. It is the contract for the codebase: the packages a build needs, the tasks everyone runs, the session baseline the project expects. Changing it changes the environment for every contributor. - A loadout is **personal and local**. It is never committed to the project and never affects another developer. Deleting all your loadouts changes nothing about how the project builds or what its sessions require. Both feed the same session, and the daemon merges them into one final configuration when the session comes up. The project contribution and every applied loadout compose together as peers: there is no override precedence, so a loadout adds to the project rather than silently replacing parts of it. If two contributors disagree on the same value, activation fails with a conflict. (For the exact merge and conflict rules, see the [loadout reference](https://minimal.dev/docs/reference/loadouts).) One boundary worth stating up front: loadouts apply to **sessions** (`min session activate`). They do not apply to task sandboxes, which carry their own packages and environment through the task schema. ## Where loadouts live Loadouts live in your user config directory, in either of two layouts: ``` ~/.config/minimal/loadouts/.toml # a loadout that is just a file ~/.config/minimal/loadouts//loadout.toml # a loadout you keep in git ``` On Linux this is `$XDG_CONFIG_HOME/minimal/loadouts` (falling back to `~/.config/minimal/loadouts` when `XDG_CONFIG_HOME` is unset). macOS uses the same `~/.config` location for consistency with Minimal's state and cache directories. The global `--config-dir` flag overrides the base directory when you need a non-default location. The filesystem names the loadout. A file named `dev.toml` and a directory named `dev/` holding a `loadout.toml` both define the loadout `dev` — nothing inside the file names it, so renaming the file (or the directory) renames the loadout. Defining one name both ways at once is an error rather than a precedence rule. The second shape exists so a whole loadout — its definition, the files it ships, its hook scripts — is one self-contained directory you can clone: ```console $ git clone git@example.com:you/helix-loadout ~/.config/minimal/loadouts/dev ``` Nothing else changes between the two, because a loadout's own directory is `~/.config/minimal/loadouts//` either way. The [reference](https://minimal.dev/docs/reference/loadouts#where-loadouts-live) has the details. The loadouts directory is not created for you: make it and add loadouts in either shape to get started. ## What a loadout carries A loadout can contribute four kinds of things to a session, and all four take effect inside the session when it is activated. ### Packages A list of package names brought into the session alongside the project's baseline packages. This is how you land your editor and tools in the sandbox without asking the project to depend on them: ```toml packages = ["helix", "zellij"] ``` Packages compose as a set across all contributors, so listing something the project already provides is harmless: duplicates are deduplicated. ### Vars Environment variables set in the session. A value can be a literal, or it can inherit from the host environment of the `min` process, optionally with a fallback: ```toml [vars] EDITOR = "hx" VISUAL = "hx" ``` Vars are the main lever for interactive setup: your prompt (`PS1`) and a one-time banner travel through `[vars]`, and they reach every shell. An rc file is the other lever — the shell started by `min session attach` sources the startup file you [patched](#file-patches) into the session home, bash's `.bashrc` included (plain `sh` is the one exception: its startup file is the daemon's `$ENV`). The [reference](https://minimal.dev/docs/reference/loadouts#session-shell) walks through both in detail. ### File patches Patches copy files from your host into the session, which is how your dotfiles arrive. Each entry pairs a host `source` (a path, a glob, or a list of them) with a `dest` inside the session: ```toml patches = [ { dest = ".config/helix/config.toml", source = "~/dotfiles/helix/config.toml" }, { dest = ".config/zellij/", source = "~/dotfiles/zellij/**/*.kdl" }, ] ``` A source that does not exist on your host is skipped with a warning rather than failing activation, so you can opportunistically patch a dotfile tree that may not be present on every machine. For files that exist only to serve one loadout, keep them beside it instead and name them with `$LOADOUT_ROOT` — the directory next to the loadout file, named after the loadout, which is also where its hook scripts live: ```toml patches = [ { dest = ".config/helix/config.toml", source = "$LOADOUT_ROOT/config.toml" }, ] ``` That makes the loadout and its files one movable unit, with no absolute path into your config directory to keep in sync. See the [reference](https://minimal.dev/docs/reference/loadouts#loadout_root) for the rules. ### Lifecycle hooks Hooks run inside your session, with its packages, variables, files, and network. Your own loadouts' hooks run without ceremony; a *project's* hooks require you to allow-list that project in your [user policy](https://minimal.dev/docs/reference/user-policy) first, because a hook is arbitrary code the project asked to run on your machine. `min session activate --no-hooks` turns them off for a session entirely. Hooks are scripts declared to run at session transition points: `on_activate` when the session comes up, `on_destroy` when it is torn down, `on_attach` when you connect to it, and `on_detach` when you disconnect. Declare them to warm a cache, fetch grammars, or clean up when you step away: ```toml [[lifecycle_hooks]] on_activate = { type = "inline", value = "hx --grammar fetch >/dev/null 2>&1 || true" } ``` Hooks from every applied loadout concatenate in declaration order. ### A complete example Putting the pieces together, a loadout that sets up the helix editor and the zellij multiplexer with your dotfiles looks like this: ```toml description = "helix + zellij with my dotfiles" packages = ["helix", "zellij"] patches = [ { dest = ".config/helix/config.toml", source = "~/dotfiles/helix/config.toml" }, { dest = ".config/zellij/", source = "~/dotfiles/zellij/**/*.kdl" }, ] [vars] EDITOR = "hx" [[lifecycle_hooks]] on_activate = { type = "inline", value = "hx --grammar fetch >/dev/null 2>&1 || true" } ``` Saved as `~/.config/minimal/loadouts/dev.toml` — or as `~/.config/minimal/loadouts/dev/loadout.toml` — it is ready to apply. ## Applying a loadout Name a loadout when you activate a session with `--loadout`, which is repeatable to stack several at once: ```console $ min session activate --loadout dev $ min session activate --loadout dev --loadout scratch ``` Loadouts are read and composed on the client, before the CLI contacts the daemon, so a missing or malformed loadout file fails the activation loudly and immediately rather than producing a silently-empty session. When loadouts are applied, the CLI prints `Applying loadouts: ` to stderr. Applying a loadout is also the moment its contents are captured: files are read once, inherited vars are resolved against your current host environment, and the composed result is what the session runs with. Editing a loadout file does not retroactively change sessions that already exist, so destroy and re-activate to pick up an edit. To activate with no loadouts at all, including skipping the defaults described below, pass `--no-loadouts` (which conflicts with `--loadout`): ```console $ min session activate --no-loadouts ``` ## Auto-applying with `default_loadouts` Typing `--loadout dev` on every activation gets old. Set the loadouts you almost always want in the client config at `~/.config/minimal/config.toml`, under a `[loadouts]` section: ```toml [loadouts] default_loadouts = ["dev", "fish"] ``` With this in place, a plain `min session activate` applies `dev` and `fish` automatically. The defaults are a convenience layer, and the activation flags take precedence over them: 1. `--no-loadouts` applies nothing, regardless of the config. 2. One or more `--loadout NAME` applies exactly those, and the config's `default_loadouts` are ignored for that activation. 3. Neither flag applies the `default_loadouts` list, which may be empty. So the defaults are your baseline, `--loadout` is an explicit override for a one-off session, and `--no-loadouts` is the clean-room escape hatch. ## Listing your loadouts To see which loadouts you have and what each contributes, run: ```console $ min loadout ls ``` `ls` is an alias for `min loadout list`. It enumerates every `*.toml` file in your loadouts directory, one row per file, with the name, description, and a per-kind summary of what it contributes: ``` NAME DESCRIPTION CONTRIBUTES * dev helix + zellij with my dotfiles 2 pkg / 1 var / 2 patch extra 1 pkg / 0 var / 0 patch * default (from `[loadouts].default_loadouts`) ``` Loadouts named in `default_loadouts` are marked with a leading `*`, so the listing doubles as a quick check of what a plain `min session activate` will pull in. A malformed file is listed with its parse error so you can fix it in place rather than hunting for which file is broken. Pass `--dir ` to list a loadouts directory other than the default. ## Where to go next - The [loadout reference](https://minimal.dev/docs/reference/loadouts) has the full TOML schema for every field (`description`, `packages`, `[vars]`, `patches`, `[[lifecycle_hooks]]`, and the rest), the client config keys, and the exact composition and conflict rules. - [Sessions](https://minimal.dev/docs/concepts/sessions) explains the session model that loadouts layer onto: activation, attaching, and teardown. --- # Stacks > Stacks wire Minimal to build codebases with specific languages and tools. Covers available stacks, usage, and how to define custom ones. Source: https://minimal.dev/docs/concepts/stacks Last updated: 2026-07-29 Stacks wire Minimal to operate a codebase with specific tools and commands. When you name a stack in your [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) file, build-plane commands like `mip run build` just work. Also, your [tasks](https://minimal.dev/docs/reference/tasks) inherit these familiar tools and semantics automatically. Learn more about stacks in [the reference section](https://minimal.dev/docs/reference/stack-specs). ## Available stacks The [Minimal Public Package Registry](https://github.com/gominimal/pkgs) ships stacks for most common languages and build systems. The registry is the source of truth; the highlights: | Stack | Detected by | Build command | |---------|-------------|---------------| | `go` | `go.mod` + `go.sum` | `go build` | | `rust` | `Cargo.toml` | `cargo build --release` | | `pnpm` | `pnpm-lock.yaml` | `pnpm install && pnpm build` | | `npm` | `package-lock.json` | `npm ci && npm run build` | | `bun` | `bun.lock` | `bun install && bun run build` | | `deno` | `deno.json` | `deno compile` | | `uv` | `uv.lock`, `pyproject.toml` | `uv sync && uv build` | | `pip` | `requirements.txt`, `setup.py` | `pip3 install --target ./build .` | | `gradle` | `build.gradle` | `gradle build -x test` | | `make` | `Makefile` | `./configure && make` | | `meson` | `meson.build` | `meson setup && ninja` | | `cmake` | `CMakeLists.txt` | `cmake && make` | | `zig` | `build.zig` | `zig build -Doptimize=ReleaseSafe` | ## Using a stack Stacks are declared in the [`[stack]` section](https://minimal.dev/docs/reference/minimal-dot-toml#stack) of your `minimal.toml` file. ```toml [stack] use = "" ``` Any stack defined in your codebase (or defined in any layer in your [software supply chain](https://minimal.dev/docs/concepts/software-supply-chain)) can be used. When a stack is defined, any [task](https://minimal.dev/docs/reference/tasks) inherits the packages and environment the stack declares; the stack also contributes default tasks, such as `build`. ### Declaring additional dependencies It's quite common for a specific codebase to need additional dependencies, either during the build (build packages) or on any system the built software runs (runtime packages). Both can be declared in your `minimal.toml`: ```toml [stack] use = "..." build_packages = ["perl"] runtime_packages = ["openssl"] ``` Dependencies can also be added automatically with `min add --build` or `min add --runtime` (see the [`min` reference](https://minimal.dev/docs/reference/cli-min)). ### Automatic initialization Minimal will auto-detect the stack to use for most languages and package managers. To have the Minimal CLI detect an appropriate stack and pre-fill your codebase's [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) file, run `min init` in the root directory. --- # Packages > Packages are reproducible, dependency-aware units of software in Minimal. Covers usage, definition, caching, and hermetic builds. Source: https://minimal.dev/docs/concepts/packages Last updated: 2026-08-17 Packages are the core of software production in Minimal. Packages are reproducible, maintainable, and dependency-aware building blocks designed for environment parity across the software development lifecycle (SDLC). Structurally, a package is the union of its declarative build specification and the resulting binary artifacts. A metaphor for Minimal's packages would be a recipe (the build specification) and the meal that's cooked from it (the resulting artifacts). This relationship is cemented by a [layer of metadata](#definition) that defines the package's dependencies, architecture, and provenance. In our metaphor, these would be the optional modifications made to tailor the dish so it's more to your liking. By binding the "recipe" to the "result," Minimal makes sure that you have complete control over every dish you make, verifying that every component of your environment is transparent, verifiable, and maintainable at scale. ## Using packages {#using} A Minimal package is available anywhere it is called for: - A package declared on a [task](https://minimal.dev/docs/reference/tasks) will always be installed in the environment the task runs. - A package declared as an input to a [build spec](https://minimal.dev/docs/reference/build-specs) will always be present in the build sandbox. - A package declared on a [stack](https://minimal.dev/docs/concepts/stacks) will always be present in the tasks and builds that use the stack. - A package declared on a [session](https://minimal.dev/docs/concepts/sessions) will always be present in that session. ## Defining packages {#definition} Packages are defined in a [build spec](https://minimal.dev/docs/reference/build-specs) file located at `packages//build.ncl`. The `packages/` directory is always adjacent to the [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) file at the base of the layer, but can be omitted if the layer does not define any packages. The package definition captures a variety of useful information: - The build-time and runtime dependencies of the package, defined in terms of other packages - How to build the package from source-code: including build commands, where to fetch the source, and what outputs compose the package - Runtime semantics, such as state directories or environment variables that need to be configured at runtime - Metadata, such as the current version of the software, the software license, and the authoritative source of releases. ## Caching {#caching} Built packages are cached against their _spec hash_: a cryptographic hash of the metadata of the package, including the recipe for building the package (and the recipes for building all its transitive dependencies). Using these hashes as the storage key makes sure that the exact package described in your repository or [software supply chain](https://minimal.dev/docs/concepts/software-supply-chain) is used anywhere it is requested, as well as providing a reliable key for fetching packages remotely. When Minimal needs a package, it first checks the local cache. If the package is not present locally, Minimal fetches it from the remote binary cache. If the remote fetch is disabled or the package is unavailable there, Minimal builds it locally. ## Package builds All Minimal package builds take place in a hermetic, isolated environment to prevent interference or non-reproducibility from the machine running the build. ## Sideloading additional packages Sometimes you may want to use packages defined in a separate repository, but still use the toolchains and software versions defined by your [`[upstream]`](https://minimal.dev/docs/reference/minimal-dot-toml#upstream). Sideloads allow you to do this: each sideload specified in your `minimal.toml` brings in additional packages from other repositories, while building those packages using the version of packages defined by your [`[upstream]`](https://minimal.dev/docs/reference/minimal-dot-toml#upstream). See the [`sideloading schema`](https://minimal.dev/docs/reference/minimal-dot-toml#sideload) for additional information. --- # CLI Reference > Overview of the Minimal command-line binaries (min, mip, minimald, and minvmd) and where each is documented. Source: https://minimal.dev/docs/reference/cli Last updated: 2026-07-31 Minimal's primary command-line tool is **`min`**, the session CLI: it creates, attaches to, and manages sandboxed development sessions. `min` talks to the `minimald` host daemon, which creates and supervises sessions. On macOS (and optionally on Linux), `minimald` runs inside a Linux microVM managed by the `minvmd` daemon. Underneath, Minimal has a declarative package/build engine that evaluates `minimal.toml` plus Nickel files, builds packages in clean rooms, runs tasks in sandboxes, and manages a content-addressed artifact cache. Most users interact with it only through `min` and their `minimal.toml`. Advanced users on Linux can drive it directly with the **`mip`** CLI. ## Binaries | Binary | Role | Reference | |--------|------|-----------| | `min` | Session CLI: create, attach to, and manage sandboxed dev sessions | [min](https://minimal.dev/docs/reference/cli-min) | | `mip` | Package/build CLI (advanced, Linux-only): build packages, run tasks, manage the cache directly | [mip](https://minimal.dev/docs/reference/cli-mip) | | `minimald` | Host daemon: serves sessions to `min` over SSH-on-UDS | [minimald](https://minimal.dev/docs/reference/cli-minimald) | | `minvmd` | VM daemon: boots the Linux microVM that hosts `minimald` | [minvmd](https://minimal.dev/docs/reference/cli-minvmd) | ## Command naming convention {#command-naming-convention} Commands are spelled **` `**: `min session activate`, `min session policy`, `min loadout list`, `mip package build`. The noun names the thing being acted on; the verb names the action. Three rules follow from that: 1. **Every noun accepts its singular and plural form.** `session`/`sessions`, `loadout`/`loadouts`, `package`/`packages` (which also keeps `pkg`). Both spellings are visible in `--help`, so neither is a hidden alias you have to know about. 2. **Verbs are shared across nouns, not owned by one.** `list`, `stop`, `build`, and friends mean the same thing wherever they appear, so ` list` is guessable for any noun that has a list. 3. **New surface gets a noun.** Reach for an existing noun before inventing one; a genuinely new kind of thing gets a new noun rather than a new bare verb at the top level. ### Documented exceptions `min` with no arguments prints the top-level help. A few high-traffic forms stay bare at the top level. These are deliberate ergonomic choices, not leftovers — do not "fix" them: | Form | Why it stays | |------|--------------| | `min ls` | Visible top-level alias of the canonical `min session list`; the highest-traffic command keeps its bare form. | | `min stop` | Acts on the daemon backend rather than any session, and is the daemon-lifecycle command people reach for. | | `min init`, `min add`, `min update` | Passthroughs to the `mip` commands of the same name; keeping the spelling identical across the two CLIs beats the hierarchy. | ## Platform availability - **Linux**: installs ship `min`, `mip`, and `minimald`. `minimald` runs natively by default; `min --provider local-minvmd` routes sessions through the `minvmd` microVM instead, which needs a separately obtained `minvmd` binary (a prebuilt Linux amd64 `minvmd` is attached to each GitHub Release; arm64 must build from source). - **macOS**: installs ship `min` and `minvmd` only. `minimald` always runs inside the microVM, and the package/build plane runs there with it; there is no native macOS `mip`. - `mip run` executes tasks in Linux sandboxes and is available on Linux only. ## In-sandbox helper commands Task sandboxes expose a small set of helper commands (`add`, `search`, `check`, `run`) for use from inside a running sandbox. Those are documented separately in [sandbox operations](https://minimal.dev/docs/reference/sandbox-operations). --- # min CLI > Reference for the min session CLI: create, attach to, and manage sandboxed development sessions, plus the git-remote-min helper. Source: https://minimal.dev/docs/reference/cli-min Last updated: 2026-10-14 `min` is the Minimal session CLI. It talks to the `minimald` daemon (see [minimald](https://minimal.dev/docs/reference/cli-minimald)) to create, attach to, and manage sandboxed development sessions. Most commands start the daemon automatically when it isn't running, with `bug`, `stop`, and `version` being the exceptions. The daemon starts natively on Linux (and under `--provider local-minvmd`) or inside the [`minvmd`](https://minimal.dev/docs/reference/cli-minvmd) microVM host daemon on macOS. Running bare `min` with no subcommand routes you into a session when stdin and stdout are both a terminal: it follows the smart-resolution rules of `min session attach` with no argument (cwd match → attach, only session → attach, ambiguity → picker), and when no sessions exist it creates one from the current directory and attaches. Without a terminal it instead prints a read-only state report on stderr and exits successfully. Use `min --help` to print this help. Commands are spelled `min `, and every noun accepts its singular and plural form (`session`/`sessions`, `loadout`/`loadouts`). A handful of bare verbs (`ls`, `stop`, `init`, `add`, `update`) survive at the top level as deliberate ergonomic exceptions, called out as such below; see [the CLI convention](https://minimal.dev/docs/reference/cli#command-naming-convention) for the rule and the full list of exceptions. ## Global flags These apply to every subcommand. | Flag | Short | Description | |------|-------|-------------| | `--repo-dir ` | `-C` | Use the given directory as the repository root, instead of the current working directory | | `--minimal-dir ` | | Override the base directory for minimal's state; the session store, provider instances, and other on-disk state live under `/`. Defaults to `$XDG_STATE_HOME/minimal` on Linux (or `$HOME/.local/state/minimal`); macOS also uses `$HOME/.local/state/minimal` | | `--config-dir ` | | Override the user config directory; everything under `/minimal/` (`config.toml`, `loadouts/`, ...) resolves relative to it. Defaults to `$XDG_CONFIG_HOME` on Linux (or `$HOME/.config`); macOS also uses `$HOME/.config` | | `--provider ` | | Daemon backend that hosts sessions: `local-minimald` (Linux default, `minimald` natively on the host) or `local-minvmd` (`minimald` inside the `minvmd` microVM). No effect on macOS, where `minvmd` is the only backend | | `--no-input` | | Skip interactive prompts that need a terminal (such as the session picker); ambiguous choices error with a list of candidates instead. Implied when stdin/stdout is not a terminal | ## Commands ### `session list` (aliases: `min ls`, `min session ls`) ``` min session list [--raw] [--json] ``` Lists sessions. `--raw` prints raw session IDs one per line for piping into scripts; `--json` prints the full session list as pretty-printed JSON. When the daemon reports a shared resource pool, the table is headed by a `RESOURCE POOL:` line (CPU cores, memory, and the number of sessions sharing them); `--raw` omits it. Another box at the same shared loopback address can hold a port this box declares, and this box does not forward it. The list prints one stderr line per such port after the table: `warning: : port

is held by ; not forwarded`. With `--json` the entry lists those ports under `shared_port_collisions`, each with `port` and `held_by`, and the key is absent when there are none. `min ls` is the same command kept bare at the top level — a deliberate exception to the `min ` convention, since it is the highest-traffic command in the CLI; `min session ls` is the noun-level alias. All three spellings take the same flags and produce identical output. ### `dash` ```text min dash ``` Opens a full-screen TUI for browsing, inspecting, and managing sessions across every running provider on the host (native minimald and the minvmd microVM). The left pane lists sessions grouped by provider; the right pane stacks the focused session's Info, networking Policy, and a read-only live Preview of its terminal screen (no attach, no PTY resize). Requires a terminal. Keys: `↑`/`↓` (or `k`/`j`) move, `/` fuzzy-filters by name, ID, and project path, `enter` attaches to the focused session (suspend TUI → ssh → resume on `ctrl-]` then `d` detach) or collapses a provider group, `d` destroys (with confirmation — also cancels an in-flight create/upload), `r` renames, `n` creates a session through the full activate flow (project upload, loadout compose, finalize), `q` quits. The cursor's last position is restored on the next launch from `/dash-state.json`; TUI diagnostics go to `/dash.log`. ### `session activate` ``` min session activate [OPTIONS] [PATH] ``` Activates (creates) a new session for the project at `PATH` (defaults to the current directory). | Flag | Short | Description | |------|-------|-------------| | `--name ` | `-n` | Optional session name | | `--sync ` | | How to load project files into the session: `tarball` (default: stream a tarball of your project and unpack it) or `none` (do not populate the worktree. The session starts from a default project configuration and does not apply the project's `minimal.toml`) | | `--network ` | | Network mode for the session: `none` gives it no network (every socket it opens to a destination outside itself fails), `host_ip` shares the host's network namespace (the default), and `own_ip` gives it its own IP address so `--ingress` can publish ports. An `own_ip` box with no egress flags reaches no external address by default (see below). The old hyphenated spellings `no-net`, `host-net`, and `own-ip` still work for one release, with a one-line hint naming the current spelling. On a VM-backed host an `own_ip` box registers with the VM host daemon before the daemon creates the box. An activation that cannot reach the VM host daemon exits 7. One that daemon refuses because it has no free addresses exits 8 | | `--ingress ` | | Static ingress port mapping `EXT:INT[/PROTO]` (PROTO = tcp or udp, default tcp). Repeatable. Requires `--network own_ip` | | `--dynamic-ingress ` | | Sets the stance that decides the box's own requests to publish a port (`min net expose`, or a listen inside `--dynamic-range`). `allow` publishes them, `ask` asks the attached person, and `deny` refuses every one, the same as leaving the flag unset. Setting it declares ingress even with no `--ingress` mapping. Requires `--network own_ip`. A VM-backed host refuses `allow` and `ask` before it creates the box, until the host side can admit a publish the box requests ([gominimal/minimal#1897](https://github.com/gominimal/minimal/issues/1897)). Every macOS host is VM-backed | | `--dynamic-range ` | | Inclusive host port range, such as `8000-8443`, inside which `--dynamic-ingress allow` publishes. A port outside it gets the out-of-range refusal. Requires `--dynamic-ingress`. Setting it declares ingress even with no `--ingress` mapping. The flag refuses a range that starts below 1024, a privileged port | | `--loadout ` | | Apply the named loadout from `/minimal/loadouts/.toml` or `/minimal/loadouts//loadout.toml`. Repeatable; if given, config-file `default_loadouts` are ignored | | `--no-loadouts` | | Apply no loadouts at all (also skips the config's `default_loadouts`). Conflicts with `--loadout` | | `--no-hooks` | | Run none of the session's [lifecycle hooks](https://minimal.dev/docs/reference/loadouts#lifecycle_hooks---scripts-at-session-transition-points), from either the loadouts or the project's `minimal.toml`. Recorded on the session, so it applies to the later attach, detach, and destroy transitions too | | `--no-prompt` | | Fail instead of prompting when the daemon surfaces items user policy can't auto-decide; implied when stdin/stderr isn't a TTY | | `--attach` | | Automatically attach after creation | | `--allow-subnets ` | | Destination subnet the box may reach, in CIDR form (e.g. `10.0.0.0/8`). Repeatable. Inside a declaration, an unset flag grants nothing once the daemon resolves the policy: write allow-all out as `0.0.0.0/0` and `::/0`. Boxes have no IPv6 path yet, so `::/0` is the forward-compatible spelling and grants nothing today. The opt-out below keeps the old allow-all meaning of an unset flag. An own-address (`--network own_ip`) or host-address (`--network host_ip`) box accepts the rule flags, and only an own-address box enforces them. A host that decides egress per box refuses to create a host-address box that carries them. Any other host runs that box with the rules unenforced. A `--network none` box rejects the whole egress declaration | | `--allow-dns-hosts ` | | Destination DNS hostname the box may resolve and reach (e.g. `github.com`). Repeatable. Inside a declaration, an unset flag grants nothing once the daemon resolves the policy. The opt-out below keeps the old allow-all meaning of an unset flag | | `--allow-protocols ` | | Outbound transport protocol the box may use: `tcp`, `udp`, or `icmp`. Repeatable. Unset lets every protocol through: the flag filters the reach the two allow lists grant | | `--deny-subnets ` | | Destination subnet the box may not reach, in CIDR form — subtracted from what the allow flags admit. Repeatable; unset means nothing is denied. A declaration with only deny flags admits nothing: there is no allow list to subtract from. Add `--allow-subnets 0.0.0.0/0 --allow-subnets ::/0` to deny a range out of allow-all | | `--deny-all-egress` | | Declare deny-all egress: the box reaches no external address. Writes the deny-all `egress` section, every allow list present and empty. A box declared by flag reads in the record exactly like one whose `minimal.toml` carries the section. On a host-address box the host's classifier decides a declared deny-all per box. An own-address box without egress flags already gets deny-all by default (see below). Conflicts with every `--allow-*`/`--deny-*` rule flag | Together the four `--allow-*`/`--deny-*` rule flags form the box's `egress` declaration. Naming one of them stores it on the session, and `min session policy` shows what the session ended up with. `--deny-all-egress` declares the whole section in one flag and cannot combine with them. An unset `--allow-subnets` or `--allow-dns-hosts` inside a declaration grants nothing, whatever the box's network mode. This changes what a declaration with no allow list does. `--allow-dns-hosts github.com` alone reaches `github.com` and no address directly. `--deny-subnets` alone reaches nothing. Both used to reach every address the deny left, and the operator opt-out named below restores that earlier meaning. An own-address box (`--network own_ip`) without these flags gets the deny-all default: it reaches no external address. It can still resolve names in the box zone, `host.min.internal` included. Activate prints one line saying so: ``` This box can reach no external address: a --network own_ip box with no egress rules gets the deny-all default. To allow outbound traffic, activate a new session with --allow-subnets or --allow-dns-hosts, for example --allow-dns-hosts github.com. ``` To give such a box reach, declare it with the allow flags, such as `--allow-dns-hosts github.com` or `--allow-subnets 0.0.0.0/0`. To keep the earlier allow-all default for every undeclared box on a host, the operator opts out. A native Linux host opts out with `minimald run --egress-deny-all-opt-out`, or with `MINIMALD_EGRESS_DENY_ALL_OPT_OUT=1` in the environment that starts `minimald`. `min` starts the daemon without the flag when it is not running, so export the variable to keep the opt-out after such a restart (see [minimald](https://minimal.dev/docs/reference/cli-minimald)). A VM-backed host opts out with `MINVMD_EGRESS_DENY_ALL_OPT_OUT=1` in the environment that starts `minvmd` (see [minvmd](https://minimal.dev/docs/reference/cli-minvmd)). A host-address box with no section keeps allow-all either way. The opt-out also keeps the earlier meaning of an unset allow flag inside a declaration. On an opted-out host, `--allow-dns-hosts github.com` alone keeps allow-all addresses: the box reaches every address directly, and the names bound nothing. Activation does not warn about it. Add `--allow-subnets` to bound the addresses there. Activating a path that already has a session is allowed, but warns: `min` names the existing session and creates a second one anyway. With two sessions on one path, resolving that directory to a session is ambiguous, so a bare `min` there can no longer pick one and `session attach` falls back to its picker (erroring when `--no-input` is set or stdin/stdout is not a terminal). Attach to the existing session instead when you mean to rejoin it. When `PATH` has no `minimal.toml`, activation still succeeds and the session comes up with a default environment. On an interactive terminal `min` first offers to scaffold a `minimal.toml`; accepting writes one into `PATH`, while declining leaves your directory untouched. When prompts are skipped (`--no-input`, or a non-terminal stdin) `min` prints a notice and continues without writing anything to `PATH` — the daemon fabricates a default config inside the session's own workspace instead. `--loadout` is resolved before any of this, so an unknown loadout name errors even in a directory with no config. ### `session attach` ``` min session attach [SESSION] ``` Attaches to an existing session, identified by UUID, unique id prefix, or session name. An exact session name wins over an id prefix. A prefix that matches more than one session fails with an error that names the candidates. When `SESSION` is omitted, `min session attach` resolves a session from the current working directory (or the only existing session) and opens an interactive picker if the choice is ambiguous (`--no-input` errors instead). `min session attach` exits 0 when you detach or the session's shell exits. It prints a one-line notice and exits 254 when the daemon ends the attach. That happens when someone destroys the session, another connection attaches to it, or the daemon shuts down. It exits 255 when the connection fails, or when the daemon disconnects a terminal that stopped keeping up with output. ### `session exec` ``` min session exec ... ``` Runs a command in an existing session, non-interactively, relaying its stdout, stderr and exit code. When a daemon shutdown stops the command, `min` prints `minimald is shutting down; the command was stopped` on stderr and exits with the command's own status. How `COMMAND` is read depends on how many arguments you give it: - **One argument is a shell command**, run by the session's shell with its pipes, globs and `$VAR` intact — the `ssh host ''` form. ``` min session exec web 'echo $PWD' ``` - **Several arguments are an argv**, carried as data. No shell reassembles them, so a word keeps its spaces and its metacharacters stay literal. ``` min session exec web sh -c 'echo A B C' ``` The argv form matters because ssh has no argv on the wire — it joins its trailing arguments with single spaces and the far side reshells the result. Passing words through one by one would let the session's shell re-split them, which is how `sh -c 'echo A B C'` once lost its first word to `sh`'s `$0`. Nothing about a command's *text* routes it: a command is the session's however it happens to start, so the session's own `min` binary is reachable here. The daemon's own operations are named explicitly instead — see `session run`. #### Backgrounding a command `session exec` returns when `COMMAND` itself exits, and relays output up to that point. A process the command backgrounds keeps running in the session, but what it writes *after* the command has exited is not yours to rely on: a short drain catches whatever was already in flight, and past that its output is read and discarded. Nothing it writes is ever lost to a broken pipe — the process is not killed — but you will not see it. ``` min session exec web 'sleep 20 & echo STARTED' # returns immediately ``` So background a long-running process with its output redirected somewhere you can retrieve it, rather than expecting it on the wire: ``` min session exec web 'nohup ./server >server.log 2>&1 &' min session exec web 'tail -n 50 server.log' ``` `nohup ... >/dev/null 2>&1 &` is the fully detached form: it hands the process its own stdout and stderr and drops the ones it inherited, so nothing about it depends on the exec channel at all. The same applies to `session run` and `task run`, which relay over the same channel. If the client goes away while the command is still running, the exec ends its whole process group: SIGTERM, a grace period, then SIGKILL. A `nohup`'d job stays in that group, so it ends too. Start a job that must outlive the client with `setsid`, which puts it in a session and group of its own: ``` min session exec web 'setsid nohup ./server >/dev/null 2>&1 &' ``` A command that exits by itself ends nothing: its background jobs keep running. ### `session run` ``` min session run ``` Runs a task declared in the session project's `minimal.toml`, in that session, relaying its output and exit code. This is the session-scoped counterpart to [`min task run `](https://minimal.dev/docs/start/6-tasks), which composes a task session of its own. Use `session run` when you want the task to run against a session you already have. Because the task is named as a task rather than inferred from a command string, a task may share a name with a daemon subcommand or with a program on the session's `PATH`. ### `session destroy` ``` min session destroy [--all] [-f|--force] [SESSION] ``` Destroys a session and ends its processes. `--all` destroys all sessions. `-f/--force` skips the confirmation. Before it destroys one session, the command asks for confirmation if the session holds uncommitted changes or commits that no remote has. It also asks if the daemon cannot report that state. The command destroys a clean session without a prompt. Without a terminal, or under `--no-input`, nobody can answer, so the command refuses unless you pass `-f`. `--all` without a terminal also refuses unless you pass `-f`. ### `session rename` ``` min session rename ``` Renames an existing session. ### `session policy` ``` min session policy [-o json] ``` Prints the effective networking rules for `SESSION` (a UUID, unique id prefix, or session name). Resolved from the daemon, which answers from the policy stored at activation; each rule line shows what the session ended up with, not just what was typed. The egress block resolves every dimension of the box's `egress` declaration to its rule or its default: ``` egress subnets 10.0.0.0/8 dns hosts allow-all protocols tcp, udp deny subnets 169.254.169.254/32 ``` `subnets`, `dns hosts`, and `protocols` each read `allow-all` when the matching flag was not given. `deny subnets` reads `(none)` when the declaration denies nothing. A deny-all declaration, the `egress` section with every allow list present and empty, prints as the one row `deny-all`. A box without an `egress` section prints the default the daemon resolved the absence to, by name and marked as what it is. `deny-all (default)` marks an own-address box, which gets the deny-all default. `allow-all (default)` marks a box on a host that opted out of that default, or one that shares its host's network namespace. The `(default)` mark distinguishes a verdict the box declared from the same verdict the default gave it. A box activated with `--credentialed-upstream` prints one more row in the egress block: ``` egress deny-all (default) credentialed upstream allowed to reach the Box Egress Proxy ``` The row names the lane the declaration opened. The gate admits the box's frames to the Box Egress Proxy's listener, and the proxy checks their credential. The rows above never decide that destination. The listener is the lane's whole reach. The gate still refuses everything else the box sends to the proxy's address. A box without the lane omits the row, so the missing row means the box runs without a lane. The gate that admits the lane sits only in front of own-address boxes. A box in any other mode still prints the row it declared, marked as not in effect, with the box's mode named: ``` egress allow-all (default) credentialed upstream allowed to reach the Box Egress Proxy (not in effect: host_ip box) ``` The ingress block lists the published port mappings the session's `--ingress` flags declared (or `deny-all` when the box leaves ingress undeclared). The `dynamic ports` row joins them when the box declared a `--dynamic-range`. The `dynamic ingress` row always prints, with the stance that decides the box's own publish requests. A box that set no `--dynamic-ingress` reads `deny (default)`, the deny the absence evaluates to, marked the way the egress block marks a default. A box that declared `deny` reads plain `deny`: ``` ingress tcp :8080 → :80 dynamic ports 8000–8443 dynamic ingress allow ``` A host-address (`--network host_ip`) session prints no ingress block at all: it shares its host's network namespace, so minimald applies no per-session ingress to it and there is no rule to state. The `live ingress` block lists the ports the box published at runtime with `min net expose`. Each row shows the address the forward binds on and the in-box port it delivers to: ``` live ingress (published at runtime) tcp 127.0.64.21:3000 → :3000 ``` A listed runtime publish is reachable. The daemon admits the port at the box's relay gate when it binds the forward. If nothing in the box listens on the port yet, the box itself refuses a connection to it. A row from an older daemon whose gate did not admit runtime publishes reads `(pending; not yet reachable)`. A row from a daemon older than the `pending` field reads `(reachability unknown: the running daemon is older than this min)`. The CLI never shows either row as reachable. The daemon publishes a port the box listens on only after it writes the decision to its audit log. When the daemon cannot write the log, the port stays unpublished, and the command prints one line per port after the live rows: ``` warning: port 3000 is allowed but not reachable yet: Minimal could not write the audit log /path/to/audit/decisions.log. The port is published once that log can be written. ``` The line clears once the log takes records again and the port publishes. In `-o json` output the same ports appear as `unaudited_listen_ports`, a list the document leaves out when it is empty. `-o json` (`--output json`) prints one `min/v1/session-policy` document on stdout instead of text. Each block the text output prints becomes a key: `network`, `egress`, `ingress`, and `live_ingress`. The `egress` object carries the verdict the gate enforces as `effective` and its origin as `source`. `effective` reads `deny-all`, `allow-all`, or `rules` when the declaration's own lists say the verdict. `source` reads `default` for the rollout's resolution of an absent section, `declared` for the box's own. A declared section appears under `rules`, its lists as the record holds them. The fields match the text output's `(default)` mark, so a client never recomputes the default rule to tell a declaration from a default. Each `live_ingress` row is the daemon's mapping object, with its `pending` state (`true`, `false`, or `null` for a daemon older than the field). The document leaves out the blocks the text output leaves out. A host-address session has no `ingress` key, and a `--network none` box has only `schema` and `network`, plus `credentialed_upstream` when it declared a lane. The `ingress` block has a `kind` tag, `deny_all` or `declared`, so a client reads one field to branch. Both kinds carry `dynamic_ingress`, the resolved stance: `allow`, `ask`, or `deny`, never `null`. Both also carry `dynamic_ingress_source`. It reads `declared` when the box set `--dynamic-ingress`, and `default` when the stance is the deny an absent setting gives. Both keys are new in the `min/v1/session-policy` shape. A client written against the earlier document ignores them. A client that reads them finds a value in every `ingress` object. The document also carries `credentialed_upstream`, an object, when the box declared a lane, in any network mode. Its `effective` field is `true` for an own-address box and `false` for any other, the text row's `(not in effect: …)` mark. The document leaves the key out when the box did not declare a lane, so a missing key means the box runs without one. Boxes handed one shared loopback address can declare the same port. The box that published it first holds it, and a later box does not forward it. Activation prints a `warning:` line on stderr for each such port, naming the box that holds it. The text output marks the declared row `(held by )`, and the document lists the rows under `shared_port_collisions`, each with `port` and the holding box as `held_by`. The key is absent when another box holds none of the box's ports. With `-o json`, a failed run writes one `min/v1/error` object on stderr and exits non-zero, with no plain-text error line. The `code` field names the failure: `not_found` for a missing session, `daemon_unreachable`, or `policy_unavailable`. The code is `output_failed` when the CLI cannot write its own document to stdout, such as on a full disk. Any other failure has the code `unspecified`. The `message` field holds the text mode's error chain, and `hint` says what to do next. An `unspecified` object has no `hint`. When the reader closes stdout early, the run writes nothing and exits 141, the shell's SIGPIPE convention. ### `session hooks` ``` min session hooks [--json] ``` Lists the [lifecycle hooks](https://minimal.dev/docs/reference/loadouts#lifecycle_hooks---scripts-at-session-transition-points) composed into `SESSION` (a UUID, unique id prefix, or session name), one row per script, with the transition it runs on, whether it is inline or external, its timeout, and the loadout or project that declared it. This shows what will actually run, not what was asked for: the daemon answers from the session's composition, which holds only the hooks that survived your [user policy](https://minimal.dev/docs/reference/user-policy). A session activated with `--no-hooks`, or one whose project you never allow-listed, lists nothing. Rows are in setup order — the project first, then loadouts in the order they were applied; teardown runs the reverse. Inline bodies are collapsed to their first line; `--json` emits the full records. Answered from the persisted composition, so it works after a daemon restart and for a session nobody is attached to. ### `net forward` ``` min net forward : ``` Forwards a port from a session's box to the laptop: binds `localhost:` and relays every accepted connection over the session's SSH channel to `127.0.0.1:` inside the box, so a service running in the session answers on the laptop with nothing else installed or configured on the remote side. `min net forward web 8080:3000` puts the box's port 3000 on `localhost:8080`. A `` of `0` binds a free port the OS picks, and the forward prints the port it bound. `` must be 1-65535. Each accepted connection gets its own SSH channel, and the daemon dials `127.0.0.1:` on the box's side of the session: inside the box's own network namespace when it has one (`--network none`, `--network own_ip`), or the namespace it shares with the daemon when it does not (`--network host_ip`). Either way the port reached is the one the box's service binds, so the command works for every network mode a session can have. The forward follows the session, not the box's process: a session record that outlives its box is the normal state after [`stop`](#stop), which keeps records, and a forward that refused such a session would be stranded against every session that survived a daemon restart. Where the dial has to run inside the box — an isolated session's own network namespace — the daemon brings a box that isn't running up for the dial, exactly as [`session exec`](#session-exec) brings one up for a command. A `--network host_ip` box shares the daemon's namespace, so its dial needs no box up: a session nothing has started answers with a refused connection until something starts the box. The forward stays in the foreground and ends on `Ctrl-C`, when the session is destroyed, or when the daemon goes away; the listener and every open relay close with it. `stop` is not a destroy: it ends a forward only because the daemon's exit does, and it keeps every session record, so a later `min net forward` reaches those sessions again. What the tree proves today is the host-address mode (`--network host_ip`) end to end: the `net_forward_*` tests drive a real daemon and relay bytes through a live laptop-side listener. The in-box dial that the isolated modes (`--network none`, `--network own_ip`) take — a `socat` relay injected into the box's namespaces — is exercised by the daemon's harness test with a host-side stand-in relay rather than a real box; a root-integration proof of that leg (`just test-root-integration`) is still owed to the root lane. ### `finalize-install` ``` min finalize-install [--show [--script | --json]] [--undo] ``` The installer runs without privilege. Anything on this host that needs root is left for this command, which runs it in one `sudo` call. Until it runs, the hostname proxy serves box names. The host line that `min session activate` prints says where the box name resolves: `names: .min.internal opens through the HTTP proxy at 127.0.0.1:` while the proxy serves the names, or `names: .min.internal resolves on this machine; open it through the HTTP proxy at 127.0.0.1:` once this host resolves the zone natively. Native names resolve to a reserved local address. A browser opens the box through the hostname proxy, which keeps serving. While any item is not done, the line ends with `; finish setup: min finalize-install`. When the proxy failed to bind, the cause replaces the resolution and the box stays named: `names: .min.internal: the hostname proxy is not serving: `, with the same clause after it when it applies. A session start never prints or runs the privileged step. The script holds only what this host is missing from: - the AppArmor profile that allows user namespaces for the sandbox - names: the resolver link and the box-name service, plus the reserved local range on macOS - the classifier tree that enforces a host-address box's egress declaration. The step keeps a root-owned copy of itself under `/usr/local/lib/minimal`. A systemd boot unit re-installs the tree after a reboot. A path unit places the daemon's listener in its leaf on every daemon start. An un-enrolled host does not need a source identity for it. The classifier translates nothing until an association adds the reserved addresses - membership of the `kvm` group, only when the configured provider is the Linux VM provider On macOS the step installs the box-name service's program in an app bundle, `/Library/PrivilegedHelperTools/Minimal.app`, that root owns. The service and the job that reserves the local range both run that program. A release signs the bundle, so System Settings lists the two jobs under the bundle's name, Minimal, in General, then Login Items & Extensions. A host that ran the step from an earlier release has a bare `minzoned` program in the same directory and a script for the local range. The next `min finalize-install` replaces both with the bundle. `min finalize-install` prints the summary of what is missing. It writes the script to a temporary file that only you can read (mode `0600`) and runs it with `sudo sh`, so `sudo` asks for your password once. It then removes the file and exits with the script's status. A completed run that installed anything other than the `kvm` group membership ends with `Running boxes pick this up on their next start.`. A run that added you to the `kvm` group ends with `KVM group membership starts at your next login: log out and back in, or restart the daemon from a new login.` When both apply, both lines print, the login line last. On a host that needs nothing it prints `Every part of the install is finished on this machine; there is nothing to run.` and exits 0. It lists a fact that no script can change, such as cgroup2 mounted without `nsdelegate`, under `can't do on this machine:`. The other items still run. When a host fact blocks every missing item, it runs nothing and exits non-zero. Without a terminal, it tries `sudo -n`. If `sudo -n` cannot run the script without a prompt, it prints the summary and `f=$(mktemp) && min finalize-install --show --script > "$f" && sudo sh "$f"`, runs nothing, and exits 1. The script is plain POSIX `sh` and stops at the first statement that fails. Its header says what it configures and that it must run as root. A comment introduces each step. | Flag | Description | |---|---| | `--show` | Print the summary of what this host is missing, with no privilege prompt, and run nothing. Exits 0 whatever the items' states. | | `--show --script` | Print the script to stdout and the summary to stderr, with no privilege prompt. Run it later with `sudo sh `. Exits 0 whatever the items' states. | | `--show --json` | Print the summary as one JSON document under the schema `min/v1/finalize-install`. Each item has a stable id and a state: `done`, `missing`, `waiting`, or `cannot`. Exits non-zero while any item is not `done`. | | `--undo` | Remove everything the step installed on this host. It accepts only `--show --script`, which prints the removal script instead. | The names step points at the port the daemon's zone answerer listens on, so it needs a running daemon that has bound its answerer. It does not start one. With no daemon reachable, it lists the names item as `waiting on a daemon` under the summary and runs the remaining items. The box-name service runs as one user for the whole machine. If another user already installed it, the command refuses before running anything, names that user, and exits 1. `--show` still prints. `--undo` works without a daemon, and it succeeds on a host that holds none of the install. It removes the user-namespace profile, the classifier tree with its units and step copy, and the group membership the step added. A profile or a classifier tree installed another way stays, with a note. It also removes the box-name service and its program copy, the resolver hook, and on macOS the local range unit. On macOS the program copy is the app bundle, and `--undo` also removes the bare program and the range script that an earlier release installed. The local range addresses on macOS stay on the loopback until the next boot. `install.sh --uninstall` points at `min finalize-install --undo` while any of these host files remain. When the installer finishes on a terminal and the host lacks a step, it shows the summary and asks `Finish setup now? This runs one sudo command. [Y/n]`. The default is yes. It does not ask on a host that needs nothing. If you decline, or there is no terminal, it prints ``run `min finalize-install` when you're ready``. ### `stop` ``` min stop [-f|--force] ``` Shuts down the `minimald` daemon. `--force` shuts down even if active sessions exist. On a VM backend, `--force` also stops the VM from the host when the in-guest daemon does not answer. A guest that does not acknowledge the shutdown is killed: its running sessions get no clean stop, and data not yet flushed inside the VM can be lost. Without `--force`, an unanswering VM fails with exit code 7. This stops the daemon backend that hosts sessions, and the sessions themselves survive it (contrast [`session destroy`](#session-destroy), which removes one session and leaves the daemon running). `stop` stays bare at the top level — a deliberate exception to the `min ` convention: it acts on the daemon, not on any session. ### `loadout list` (alias: `ls`) ``` min loadout list [--dir

] ``` Lists loadouts from the user's config directory, in both layouts — `.toml` and `/loadout.toml`. `--dir` overrides the loadouts directory (default: `/minimal/loadouts`, e.g. `~/.config/minimal/loadouts` on Linux). A loadout that fails to load is reported on stderr and makes the command exit non-zero, leaving the table of valid loadouts intact. That covers a malformed file and a name defined in both layouts at once, which is [an error rather than a precedence rule](https://minimal.dev/docs/reference/loadouts#where-loadouts-live). ### `dirs` ``` min dirs ``` Prints important directories and file paths for debugging. ### `bug` ``` min bug [-o ] [--upload [--context ] [--endpoint ]] ``` Collects a diagnostic bundle (logs, state, config) to send to the minimal dev team. Writes `minimal-diag-.tar.zst` to the current directory; `-o/--output` overrides the path. The archive contains host system facts, log tails, redacted config, and state listings, plus a `manifest.json` recording any collector that failed or timed out; a broken install still yields a valid archive that explains what is missing. Because sessions are interactive, the bundle also records the terminal `bug` itself ran on (`host/terminal.json`): whether stdin, stdout, and stderr are ttys — the same condition `attach` gates on, so a run under a pipe or CI is distinguishable from a real terminal — and, for each that is, its device and its `TIOCGWINSZ` geometry (rows, columns, and the `xpixel`/`ypixel` size emulators report), which is what makes a garbled TUI or wrong wrapping diagnosable. The bundle is scoped to a project, so it can be attributed to one. Every other collector describes a machine, and a machine hosts many projects: two bundles taken on one host from two checkouts otherwise read identically. `manifest.json` therefore opens with the project's name, root, and which config file defines it (`minimal.toml` or `.minimal/minimal.toml`), repeated in `project/project.json` along with the directory `bug` was actually run from. Run outside a project, the manifest records that as a finding with its reason rather than falling silent. Only the project's identity is recorded there — its name, its root, its config file's relative path, and where `bug` ran from. No configuration *values*; the redacted config is collected separately, under the allowlist policy below. Diagnosing a wedged system must not change it: `bug` mutates no state and never starts a daemon; it works even when none are running. Secret-shaped values (env vars, tokens) are redacted before they enter the archive: only a small allowlist of env names (`RUST_LOG`, `HOME`, `SHELL`, `TERM`, `TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `COLORTERM`, `PATH`, and the `XDG_*` / `MINIMAL_*` / `MINVMD_*` / `MINIMALD_*` prefixes) have values captured verbatim (a sensitive-shaped name always loses to the allowlist), and every other env var is reported by name only. Session and project file contents are never included, only name/size listings. Review the archive before sharing. `--upload` sends the bundle to the diag portal after `bug` writes it to disk. A failed upload leaves the archive in place. The upload sends nothing to identify you: it sends the bundle and the `--context` text. `--context` is a sentence on what went wrong. The portal shows it beside the report and gives it to the agent that reads the bundle. `--endpoint` names another portal. It must be HTTPS, or plain HTTP to a loopback host. The portal takes bundles up to 64 MiB. After an upload, `bug` prints: ``` Report: https://agents.minimal.farm/diag/ The bundle is stored for the minimal team for 7 days. It is not diagnosed until someone signs in at the report URL and starts the diagnosis. Expires: Delete: min diag delete ``` The portal keeps the bundle for 7 days and then deletes it. The portal starts a diagnosis only when someone signs in on the report page and starts it there. The report URL does not contain the delete token, so you can share the URL. To remove the bundle sooner, run the `Delete:` line. See [`diag`](#diag). ### `diag` ``` min diag collect [] min diag upload [--context ] [--endpoint ] min diag delete [--endpoint ] ``` `diag collect` is [`bug`](#bug) under another name and takes the same options. `diag upload` sends a bundle that you collected earlier. Use it for a bundle someone gave you, or for one you collected while the portal was unreachable. It prints the same lines as `bug --upload`. `diag delete` deletes an uploaded bundle before it expires. Give it the id and the delete token that the upload printed. The portal deletes the bundle and any diagnosis made from it, and the command prints `Deleted `. It fails with a plain error when the portal has no such bundle, or when the token does not match the bundle. A bundle is missing when nobody uploaded it, it expired, or someone already deleted it. `--endpoint` follows the same HTTPS rule as the upload. ### `init`, `add`, `update` ``` min init [-y|--yes] min add <--session|--runtime|--build|--task > ... min update ``` Project-configuration conveniences mirroring the corresponding `mip` commands: initialize minimal configuration from your source tree, add a tool or dependency, and refresh local checkouts of upstream packages and the embedded standard library. See the [mip reference](https://minimal.dev/docs/reference/cli-mip) for details. `min update` is not a self-update. It re-pins the project's `[upstream]` link (and any sideloads) in `minimal.toml` to the current head of each tracking branch — rewriting `locked_commit` and leaving the file modified in your working tree — then refreshes the local checkouts to match. The standard library is embedded in the `min` binary and is only refreshed or verified locally — its commit is not re-pinned. The next `min session activate` materializes the new closure, which can take several minutes on the first activate. When no pin has moved it reports that and leaves `minimal.toml` unchanged. To update the `min` binary itself, reinstall it with the installer. These three are deliberate exceptions to the `min ` convention: they are passthroughs to the `mip` commands of the same name, and keeping the spelling identical across the two CLIs is worth more than the hierarchy. ### `version` ``` min version ``` Prints CLI and daemon version information. ### `completions` (alias: `completion`) ``` min completions print min completions install [...] ``` `print` writes a shell tab-completion script to stdout. Supported shells include `bash`, `zsh`, `elvish`, `fish`, `powershell`. Usage: `source <(min completions print bash)`. `install` writes that script into the shell's completion directory instead, for the three shells with a conventional per-user completion path. With no `SHELL` argument it installs for all three. | Shell | Path | |-------|------| | `bash` | `$XDG_DATA_HOME/bash-completion/completions/min` (default `~/.local/share/...`) | | `zsh` | `$XDG_DATA_HOME/zsh/completions/_min` (default `~/.local/share/...`) | | `fish` | `$XDG_CONFIG_HOME/fish/completions/min.fish` (default `~/.config/...`) | Each file is written atomically — a temporary sibling, then a rename — so a half-written completion file never reaches a shell. `install` prints every path it wrote to stdout, one per line. That is a contract rather than a convenience: `scripts/install.sh` feeds exactly those paths into its install record so `uninstall` can remove them, and derives no paths of its own. The bookkeeping therefore has one implementation, reachable by everyone rather than only by users who installed via `curl | sh`. A completion directory that exists but is not writable — these are shared, user-owned locations that can pre-exist root-owned — produces a warning on stderr, not a failure: the other shells still install and the exit status is still 0. For `zsh`, a stale `compinit` dump is dropped after a (re)install, since it can otherwise keep trusting its cached contents after the completion file underneath has changed. What it emits is a short *registration* shim, not a completion table: it teaches the shell to ask `min` itself what to offer. That indirection is what makes session arguments completable — `min session attach ` lists live session names, and `min session attach 019` lists session IDs, neither of which exists at the time a static script would be written. Every argument documented as "UUID, unique id prefix, or session name" completes this way: `session attach`, `session exec`, `session run`, `session destroy`, `session rename`, and `session policy`. Session completion is best-effort by design. It never starts a daemon — with none running there is nothing to list, and booting a VM on a keystroke would be a poor trade — and it gives up rather than make you wait if the daemon does not answer promptly. In both cases the shell simply offers nothing. To see the candidates without a shell in the loop, or to check completion against a non-default backend: ``` min complete-session-str [] # valuedescription per line min --provider local-minvmd complete-session-str # honours global args ``` The in-process completer cannot see global args (clap hands a value completer only the word being typed), so it always resolves the default backend; this hidden command is the way to check any other. ## `git push min://` - the git remote helper Installs of `min` lay down a `bin/git-remote-min` symlink pointing at the `min` binary. The binary dispatches on `argv[0]`: when invoked with the basename `git-remote-min` (which is how git invokes remote helpers for `min://` URLs), it speaks the [gitremote-helpers](https://git-scm.com/docs/gitremote-helpers) line protocol on stdio instead of the normal CLI. This lets you add a session's workspace as a git remote and push to or fetch from it directly: ``` git remote add session min:// git push session ``` Only the `connect` capability is implemented: on `connect`, the helper opens an exec channel to `minimald` over its Unix domain socket (the same transport the rest of the CLI uses) and bridges git's pack-protocol conversation across it; no external `ssh` or `socat` involved. Only the two pack services (`git-upload-pack`, `git-receive-pack`) are accepted. If the daemon is not running, the helper starts it, using default global flags (git invokes helpers without any of `min`'s own flags). --- # minimald daemon > Ops reference for the minimald host daemon: serves sandboxed sessions to the min CLI over SSH-on-UDS. Source: https://minimal.dev/docs/reference/cli-minimald Last updated: 2026-10-10 `minimald` is the Minimal host daemon. It creates and supervises the sandboxed sessions that the [min CLI](https://minimal.dev/docs/reference/cli-min) connects to, speaking SSH over a Unix domain socket (or vsock when hosted inside the `minvmd` microVM). End users normally never run it by hand: `min` auto-starts a native daemon on Linux, and [minvmd](https://minimal.dev/docs/reference/cli-minvmd) supervises it inside the microVM. ## Global flags | Flag | Short | Description | |------|-------|-------------| | `--minimal-state-dir ` | | Override the directory where state is stored (default: `$XDG_STATE_HOME/minimal`) | | `--minimal-cache-dir ` | | Override the directory where artifacts are cached (default: `$XDG_CACHE_HOME/minimal`) | ## Commands ### `run` ``` minimald run [OPTIONS] ``` Runs the minimald server in the foreground. | Flag | Description | |------|-------------| | `--instance-num ` | Instance number for this minimald; determines client-relevant paths under `/providers/local-minimald`. The SSH socket is accessible as `ssh.sock`. Default: `0` | | `--vsock` | Host the SSH socket over vsock instead of UDS; the vsock port is the default port base plus `instance_num` | | `--hostname-proxy-port ` | Port the host-side hostname proxy must listen on, when this deployment pins one — the port clients point `HTTP(S)_PROXY` at, whose documented default is 7654. Unset (the default) tries that default first and only when it is busy asks the OS for a free port, which the daemon reports wherever a client needs it: `min ls` prints it, and a second daemon on the same machine gets its own port instead of silently losing hostname routing. A pinned port that is busy stays a hard failure. | | `--zone-answerer-port ` | Port the box-zone answerer must listen on (UDP), when this deployment pins one — the port the host's resolver is pointed at to answer `*.min.internal`, whose documented default is 7656. Unset (the default) gives it the same try-the-default-then-select treatment `--hostname-proxy-port` documents. | | `--detach` | Daemonize: spawn minimald in a new session (setsid) and return once the SSH socket accepts connections, or an 8s timeout elapses. Used by the `min` CLI to auto-start a native daemon on Linux | | `--gvproxy-bin ` | Path to the gvproxy ("gvisor-tap-vsock") binary used for networking. Defaults to the installed location: the user-local `bin/gvproxy-min` the installer stamps, else the system install path. | | `--egress-deny-all-opt-out` | Keep the earlier allow-all egress default for a box that declares no `egress` section. Without it, an own-address box with no egress declaration reaches no external address and shows `deny-all (default)` in `min session policy`. The flag does not change a box that declares its own egress section. The daemon reads the flag at start, so restart it to change the setting. Setting `MINIMALD_EGRESS_DENY_ALL_OPT_OUT=1` in the environment has the same effect as the flag. Only `1`, `true`, `yes`, or `on` opt out, in any letter case. A daemon that `min` starts automatically gets no flags but inherits the environment, so export the variable to keep the opt-out across those restarts. The VM-backed counterpart is `MINVMD_EGRESS_DENY_ALL_OPT_OUT` (see [minvmd](https://minimal.dev/docs/reference/cli-minvmd)) | `MINIMALD_USERNS_GATE=off` in the daemon's environment skips the user-namespace check that refuses every `CreateSession` on a host that blocks unprivileged user namespaces. The daemon logs the skip at `warn`. When the daemon brings its host-side listeners up it logs **two** serving lines, one per listener: the hostname proxy (`component=dns-proxy`, TCP) and the box-zone answerer (`component=zone-answerer`, UDP). Each names the port it came up on and a `port_source` field saying how the port was chosen — `configured` (pinned by flag), `default` (the documented default was free), or `selected` (the default was busy, a publish was refused, or the bind asked the OS outright). Both ports also travel to clients: `min ls` prints a line for each, with the answerer's marked `(UDP)` since it is the one a host resolver is pointed at rather than an `HTTP(S)_PROXY` export. ### `completions` ``` minimald completions ``` Generates a shell tab-completion script for `minimald`. Supported shells include `bash`, `zsh`, `elvish`, `fish`, `powershell`. Usage: `source <(minimald completions bash)`. ## Known issues - `minimald` accepts but ignores the hidden `--stdlib-dir` flag. --- # minvmd daemon > Ops reference for the minvmd VM daemon: boots and supervises the Linux microVM that hosts minimald. Source: https://minimal.dev/docs/reference/cli-minvmd Last updated: 2026-10-09 `minvmd` is the host daemon that brings up a Linux microVM via libkrun (macOS/HVF or Linux/KVM) and supervises the [minimald](https://minimal.dev/docs/reference/cli-minimald) instance running inside it. On macOS it is the only session backend. On Linux, select it with `min --provider local-minvmd` (see [min](https://minimal.dev/docs/reference/cli-min)). ## Global flags | Flag | Description | |------|-------------| | `--minimal-state-dir ` | Override the state dir base (default: `$XDG_STATE_HOME/minimal`). Runtime files live under `/providers/local-minvmd0/` | ## Commands `run` and `boot` bring the VM up. `run` (alias `start`) is the lifecycle-managed supervisor: it drives the daemon's state transitions and supports `--detach` for background operation, so it is the usual entry point. `boot` is a lower-level bring-up that skips lifecycle state, used mainly for diagnostics. ### `boot` ``` minvmd boot [--foreground] ``` Boots the microVM and waits until the guest is up. `--foreground` stays in the foreground until the VMM child exits. ### `run` (alias: `start`) ``` minvmd run [--detach] [--timeout ] ``` Starts the microVM supervisor (foreground by default). | Flag | Description | |------|-------------| | `--detach` | Spawn the supervisor in the background and return once the host UDS is accepting connections | | `--timeout ` | Timeout in seconds to wait for the host UDS when using `--detach` (default: `8`) | The VM's hostname proxy takes port `7654` when no other process or VM holds it, and a free port otherwise. To pin the port, set `MINVMD_NODE_PROXY_PORT` for `minvmd run` or for the `min` command that starts it. If another process holds a pinned port, the start fails. An own-IP box that declares no `egress` section gets the deny-all egress default: it reaches no external address. To keep the earlier allow-all default for such boxes, set `MINVMD_EGRESS_DENY_ALL_OPT_OUT=1`. This is the VM counterpart of `minimald run --egress-deny-all-opt-out`. Only `1`, `true`, `yes`, or `on` opt out, in any letter case. Any other value, or leaving it unset, keeps the deny-all default. `minvmd` reads the variable only when it starts, and hands it to the guest at boot. Set it in the environment of the process that starts `minvmd`: `minvmd run`, or the first `min` command that starts the VM for you. Setting it for a later `min` command against a running VM changes nothing. To apply a change, stop `minvmd` (`minvmd stop`) and start it again with the new environment. ### `status` ``` minvmd status [--json] ``` Prints daemon status. Exit code: `0` if running, `1` if stopped, `2` on lock contention. `--json` prints status as a JSON object. ### `config show` ``` minvmd config show [--json] ``` Prints the effective per-VM resource configuration and each value's source. `--json` prints it as a JSON object. ### `config set` ``` minvmd config set [--vcpus ] [--ram-mib ] ``` Checks and persists resource parameters, applied on the next boot. | Flag | Description | |------|-------------| | `--vcpus ` | Number of virtual CPUs | | `--ram-mib ` | Guest RAM in MiB | ### `stop` ``` minvmd stop ``` Stops the running daemon gracefully. ### `completions` ``` minvmd completions ``` Generates a shell tab-completion script. Supported shells include `bash`, `zsh`, `elvish`, `fish`, `powershell`. --- # mip CLI > Reference for the mip package/build CLI: build packages, run tasks, materialize outputs, and manage the local cache. Source: https://minimal.dev/docs/reference/cli-mip Last updated: 2026-09-17 `mip` (short for "Minimal in-process") is the Minimal package/build CLI. It reads a project's [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml), builds packages in clean rooms, runs [tasks](https://minimal.dev/docs/reference/tasks) in sandboxes, and manages the content-addressed local cache. > **Advanced tool.** `mip` is Linux-only and aimed at advanced users driving > the package/build plane directly. Most workflows are better served by the > [`min` session CLI](https://minimal.dev/docs/reference/cli-min). ## Global flags These apply to every subcommand. | Flag | Short | Description | |------|-------|-------------| | `--repo-dir ` | `-C` | Use the given directory as the repository root instead of searching from the current working directory | | `--minimal-dir ` | | Override the base directory used for operations (default: `~/.cache/minimal`) | | `--no-cache` | | Ignore locally-available binary artifacts (results in rebuilds unless present in a remote cache) | | `--no-fetch` | | Do not fetch binary artifacts from the internet | | `--offline` | | Use only what's already in the local cache for sources, VCS checkouts, and the remote artifact cache; fail with a clear error on cache miss instead of attempting any network call. Implies the remote-artifact-cache half of `--no-fetch`; orthogonal to `--no-cache`/`--rebuild` | | `--num-parallel-builds ` | `-n` | Configure the number of parallel builds | ## Commands ### `run` {#run} Runs a task, such as one specified in `minimal.toml`. *(Linux only)* ``` mip run [OPTIONS] [task_args]... mip run [OPTIONS] --upstream --task-spec [task_args]... ``` | Argument / flag | Description | |-----------------|-------------| | `` | Name of the task to run (from `minimal.toml`) | | `[task_args]...` | Additional arguments to pass to the task | | `--upstream ` | JSON stanza specifying the software supply chain; must be used with `--task-spec` | | `--task-spec ` | JSON stanza specifying a task inline; must be used with `--upstream` | ### `update` ``` mip update ``` Refreshes local checkouts of upstream packages and the standard library. ### `add` {#add} ``` mip add <--runtime|--build|--task > ... ``` Add a new tool or dependency. Exactly one placement flag is required: | Flag | Description | |------|-------------| | `--runtime` | Add as a runtime dependency: your program needs this package anywhere it runs | | `--build` | Add as a build dependency: your program needs this package to build | | `--task ` | Add to a task's package list | ### `init` ``` mip init [-y|--yes] ``` Automatically initialize minimal configuration based on your source tree. `--yes` skips confirmation and writes configuration based on auto-detection. ### `status` ``` mip status ``` Shows the status of Minimal in this codebase. ### `materialize` {#materialize} ``` mip materialize --output [--arch ] ``` Materializes an output specified in an [`[outputs.]`](https://minimal.dev/docs/reference/minimal-dot-toml#outputs) section of `minimal.toml`. | Flag | Short | Description | |------|-------|-------------| | `--output ` | `-o` | **(required)** The output file to write | | `--arch ` | | Override the architecture used when building the output (e.g. `amd64`, `arm64`); takes precedence over the `arch` field in `minimal.toml` and the host default | Supported output types include `oci-image`, a Linux OCI image archive containing the configured packages, suitable for `docker load` or pushing to a registry. ### `package` (aliases: `pkg`, `packages`) Package-level operations. Takes a subcommand; see `package build` and `package dep` below. #### `package build` ``` mip package build [OPTIONS] [PACKAGES]... ``` Builds the specified package(s) in a clean room, making them available in the local cache. | Flag | Short | Description | |------|-------|-------------| | `--verbose` | `-v` | Log stdout/stderr during the build | | `--rebuild` | | Always build the specified packages, even if they are already available | #### `package dep` ``` mip package dep [OPTIONS] [PACKAGES]... ``` Generates Graphviz source code of the dependency graph, e.g. `mip package dep --input-deps-depth=0 | dot -Tpng > deps.png`. | Flag | Short | Default | Description | |------|-------|---------|-------------| | `--excludes ` | `-e` | | Packages left out of the graph and not traversed | | `--build-spec-deps ` | | `true` | Include build spec `build_deps` (does not affect runtime deps) | | `--source-deps ` | | `false` | Include source code `build_deps` | | `--local-deps ` | | `false` | Include local (`build.sh`) input deps | | `--needs ` | | `false` | Include "Needs" nodes and edges | | `--provides ` | | `false` | Include "Provides" edges and "Needs" nodes | | `--bootstrap ` | | `false` | Include replace-on-cycle/prebuilts/bootstrap | | `--subtrees-only ` | | `false` | Require non-zero subtree deps for runtime/input deps | | `--input-deps-depth ` | | `-1` | How deeply input dependencies are followed (`-1` = all) | | `--runtime-deps-depth ` | | `-1` | How deeply runtime dependencies are followed (`-1` = all) | | `--prune-edgeless ` | | `false` | Discard graph nodes with no edges | | `--output-format ` | | `dot` | Output format: `dot` or `mermaid` | ### `cache clean` ``` mip cache clean [--older-than ] ``` Removes stale entries from the local cache: cached build artifacts whose last recorded use is older than the cutoff, or that have no recorded use at all. Packages needed by the current project's tasks and stack are always kept. Also removes sandbox, task, and temporary build directories whose owning process is no longer running. `--older-than` takes a whole number of days, hours, or minutes (e.g. `30d`, `12h`, `45m`); the default is `14d`. ### `cache notices` ``` mip cache notices ``` Prints the upstream attribution files (`NOTICE` / `COPYRIGHT` / `COPYING`) recorded when the source archive with the given sha256 was last extracted, as pretty JSON. The sha256 is the one declared in the build spec. | Argument | Description | |----------|-------------| | `` | The archive's sha256, as declared in the build spec | Errors if no notices are recorded for the given sha256 — extract the source first. ### `check` {#check} ``` mip check [OPTIONS] [FILTER_NAMES]... ``` Validates minimal configuration including packages & stacks. | Flag | Short | Description | |------|-------|-------------| | `--fix` | `-f` | Attempt to fix any issues | | `--packages` | | Check packages defined in the codebase | | `--stacks` | | Check stacks defined in the codebase | | `--skip-checkers ` | `-s` | Checker names to skip, comma-separated | If no type flags are specified, all types are checked. If filter names are given, any package or stack matching a name is checked. ### `completions` ``` mip completions ``` Generates a shell tab-completion script for `mip`. Supported shells: `bash`, `zsh`, `elvish`, `fish`, `powershell`. Usage: `source <(mip completions bash)`. ## Environment variables | Variable | Description | |----------|-------------| | `RUST_LOG` | Controls logging level (default: `info`) | ## Hidden commands Additional unsupported/internal commands are hidden behind the `MINIMAL_SCIENCE_MODE` environment variable. They are experimental, carry no stability or consistency guarantees, and are not documented here. --- # minimal.toml > Schema reference for the minimal.toml configuration file: upstream, stack, defaults, tasks, and outputs sections. Source: https://minimal.dev/docs/reference/minimal-dot-toml Last updated: 2026-10-11 The `minimal.toml` file defines the configuration for Minimal in a codebase. This file must be present at the base of the repository (i.e. `./minimal.toml`), or in a `.minimal` directory at the base of the repository. Unless [`-C`](https://minimal.dev/docs/reference/cli-mip#global-flags) is specified, the [mip CLI](https://minimal.dev/docs/reference/cli-mip) searches the directory tree backwards from the current directory till a `minimal.toml` file is found. This behavior allows minimal to be invoked in project directories. ## Example ```toml [upstream] repo = "https://github.com/gominimal/pkgs" branch = "main" locked_commit = "d39aaaa581f983d6b3ba5eaaf383485a602f37f0" [stack] use = "pnpm" build_packages = ["railway"] [defaults] state_key = "dev" [session] packages = ["base", "git", "nano"] [tasks.dev] exec = "pnpm run dev" [tasks.preview] env_vars.PORT = "8080" bash = "pnpm run build && pnpm run start" [tasks.deploy] packages = ["railway"] exec = "railway up" [tasks.shell] interactive = true packages = ["base", "git", "nano"] exec = "bash -l" ``` ## Schema ### `[upstream]` - Where software comes from {#upstream} The `[upstream]` section defines the precise source of packages & stacks. This represents the preceding link in the [software supply chain](https://minimal.dev/docs/concepts/software-supply-chain). ```toml [upstream] repo = "" branch = "" locked_commit = "" ``` `locked_commit` is automatically updated when [`mip update`](https://minimal.dev/docs/reference/cli-mip) is run: it re-resolves `branch` to its current HEAD and rewrites the `locked_commit` of the upstream (and of every sideload) in `minimal.toml` in place; expect a diff on these fields after running it. #### `[[upstream.sideload]]` - Additional software sideloaded into your supply chain {#sideload} Sideload entries let you load in additional packages or stacks from a separate repository, but those packages are built using the version of packages from your upstream. Each sideload entry is loaded in order from the specified repository, and follows the same schema as `[upstream]` (the canonical table name is `sideloads`; `sideload` is an accepted alias): ```toml [[upstream.sideload]] repo = "" branch = "" locked_commit = "" # Updated via `mip update` ``` Sideload repositories have the same layout as an upstream: that is having a `minimal.toml` file, and `packages/` / `stacks/` directories as needed. ### `[stack]` - How to build code in your repo {#stack} ```toml [stack] use = "" build_packages = [""] # optional runtime_packages = [""] # optional ``` The `[stack]` section configures the [stack](https://minimal.dev/docs/concepts/stacks) to use for building code, if any. See [stack specs](https://minimal.dev/docs/reference/stack-specs) for how stacks themselves are defined. `use` is an accepted alias for the canonical `name` key, and both parse. `[harness]`, the name of this section before v0.5.0, is no longer accepted: a file that still uses it fails to load, naming the rename, so rename the section to `[stack]`. The environment variables and packages configured on a stack are inherited on all tasks in this repository. `build_packages` and `runtime_packages` are optional fields that allow you to declare additional package dependencies for build time or run time respectively. ### `[defaults]` - Settings for all tasks {#defaults} ```toml [defaults] state_key = "" # optional ``` When set, `defaults.state_key` will set a state key on all tasks which do not set `state_key`. ### `[session]` - What every contributor's session gets {#session} Contributes to [sessions](https://minimal.dev/docs/concepts/sessions) activated on this project (`min session activate`). It carries the same primitives as a [loadout](https://minimal.dev/docs/reference/loadouts) (packages, vars, patches, lifecycle hooks), scoped to the project rather than the developer: where a loadout says "what this developer wants everywhere", the session block says "what every session working on this codebase needs". There is at most one per `minimal.toml`, and it has no name or description. ```toml [session] # Toolchain every contributor's session gets. packages = ["rustc", "cargo", "postgresql-client"] # Config that lives in the repo, patched into the session's home. patches = [ { dest = ".cargo/config.toml", source = "config/cargo.toml" }, { dest = ".psqlrc", source = "config/psqlrc" }, ] [session.vars] # Applies uniformly to every developer's session. RUST_LOG = "info" CARGO_TERM_COLOR = "always" # Inherit from the developer's environment if set, else the default. DATABASE_URL = { inherit = true, default = "postgres://localhost/dev" } # Inherit with no fallback: a developer who has not set this cannot # activate a session on this project. GITHUB_TOKEN = { inherit = true } # Declared to warm the compile cache when a session comes up. [[session.lifecycle_hooks]] on_activate = { type = "inline", value = "cargo check --workspace >/dev/null 2>&1 || true" } ``` - **`packages`**: Packages brought into every session on this project, alongside whatever the developer's loadouts contribute. - **`vars`**: Environment variables. A string sets a fixed value; `{ inherit = true }` passes the developer's own value through. Here that inheritance is **required**: if the developer's environment does not have the variable set, activation fails rather than proceeding without it. ```console $ min session activate error: Composition gating failed: could not resolve pending var `GITHUB_TOKEN`: environment variable not found ``` Adding `default = "..."` supplies a fallback and makes the variable optional again. A loadout spells the bare form the same way but treats it as optional — see [Where `{ inherit = true }` differs](#inherit-divergence). One name is read specially: setting `SHELL` to a supported, installed shell makes the session's attach shell that shell instead of bash. See [`SHELL` — Which shell an attach starts](https://minimal.dev/docs/reference/loadouts#session-shell) for the supported shells and the fallback behavior. - **`patches`**: `{ source, dest }` rows copying files into the session. `dest` is relative to the session user's home directory; `source` resolves on the host, typically inside the repo. The one expansion a loadout has and a project does not is `$LOADOUT_ROOT`, which names a loadout's own directory; referencing it here fails the activation. - **`lifecycle_hooks`**: Scripts declared for session transition points (`on_activate`, `on_destroy`, `on_attach`, `on_detach`), run inside the session under POSIX `sh` — or under whatever a leading shebang names, for a hook you would rather write in fish or Python. A project's hooks require the developer to allow-list the project in their [user policy](https://minimal.dev/docs/reference/user-policy) before they will run — a hook is arbitrary code, so the developer must opt in. The field shapes and composition semantics (conflicts, policy gating) are the same as loadouts; see the [loadout reference](https://minimal.dev/docs/reference/loadouts) for the exact rules. Variable *resolution* is the exception: a bare `{ inherit = true }` that the host has not set fails activation here, where a loadout drops it with a warning, so the loadout page's rule for that one case does not carry over. #### Where `{ inherit = true }` differs {#inherit-divergence} Three surfaces accept `{ inherit = true }`, spelled identically on each. They agree that a variable the host *has* set is passed through, and they disagree about everything else: | Surface | Host variable unset | `default = "..."` | |---|---|---| | This file's `[session.vars]` (and `[[session.vars_lenient]]`) | Activation fails: `could not resolve pending var ` | Supported; the default is used | | A loadout's [`[vars]`](https://minimal.dev/docs/reference/loadouts#vars) and [`[[vars_lenient]]`](https://minimal.dev/docs/reference/loadouts#vars_lenient) | Dropped, with a warning on the terminal; activation continues | Supported; the default is used | | A task's [`env_vars`](https://minimal.dev/docs/reference/tasks#env_vars) | The run fails before the session is built | Rejected when the file is parsed — a task's `inherit` takes no `default` | Two things narrow the task row. An `echo` task never builds an environment, so it never reads — or fails over — an `env_vars` declaration. And `min run ` from inside a session, along with `min session run `, resolves `inherit` against the daemon's environment rather than your shell. Both are covered in [Tasks](https://minimal.dev/docs/reference/tasks#env_vars). ### `[tasks.*]` - Run tasks, scripts, & dev tooling {#tasks} See: [tasks](https://minimal.dev/docs/reference/tasks). ### `[outputs.*]` - Artifacts produced by `mip materialize` {#outputs} Each `[outputs.]` section defines an artifact that can be produced with [`mip materialize -o `](https://minimal.dev/docs/reference/cli-mip#materialize). ```toml [outputs.] type = "" # optional; defaults to "oci-image" packages = ["", ...] # optional; defaults to ["base"] when empty arch = "" # optional; e.g. "amd64", "arm64" path = "" # raw-file only; required there, invalid for oci-image entrypoint = "" # oci-image only; string or list of strings cmd = ["", ...] # oci-image only; string or list of strings vars = { KEY = "value" } # oci-image only; alias: `env_vars` ``` #### Output types | `type` | Description | |--------|-------------| | `oci-image` | A Linux OCI image archive built from `packages`. Compatible with `docker load` and OCI-compatible registries. | | `raw-file` | A single file extracted from `packages` at the given `path`. Useful for pulling one artifact (a kernel image, a rootfs image, …) out of a package's file tree. | #### Fields - **`type`**: The output kind, either `oci-image` or `raw-file`. Defaults to `oci-image` when omitted. - **`packages`**: Packages to include in the materialized output. When omitted or empty, defaults to `["base"]`. - **`arch`**: Target architecture for OCI images. Common values: `amd64`, `arm64`. The CLI flag [`--arch`](https://minimal.dev/docs/reference/cli-mip#materialize) overrides this; if neither is set, the host architecture is used. - **`path`**: _(`raw-file` only)_ Path, relative to the package file tree, of the single file to extract. Required for `raw-file` outputs; supplying it on an `oci-image` output is an error. - **`entrypoint`**: _(`oci-image` only)_ OCI image entrypoint. May be a string (`"/app/server"`) or a list (`["/bin/sh", "-c"]`). - **`cmd`**: _(`oci-image` only)_ OCI image default command. Same string-or-list shape as `entrypoint`. - **`vars`** (alias: `env_vars`): _(`oci-image` only)_ Environment variables baked into the image as a `KEY = "value"` table. Setting an `oci-image`-only field (`entrypoint`, `cmd`, `vars`) on a `raw-file` output, or setting `path` on an `oci-image` output, is rejected when the `minimal.toml` is parsed. #### Examples A minimal OCI image of just the `base` packages, useful for ad-hoc shells: ```toml [outputs.base-image] type = "oci-image" ``` ```sh mip materialize base-image -o ./base.tar ``` A server image with extra packages, an entrypoint, and an env var: ```toml [outputs.app] type = "oci-image" arch = "arm64" packages = ["base", "openssl"] entrypoint = "/app/server" vars = { PORT = "8080" } ``` ```sh mip materialize app -o ./app.tar ``` Override the architecture at the command line: ```sh mip materialize app --arch amd64 -o ./app-amd64.tar ``` A `raw-file` output extracts a single file from a package. Here the kernel `Image` is pulled out of the `virtio-kernel-raw` package: ```toml [outputs.virtio-kernel] type = "raw-file" packages = ["virtio-kernel-raw"] path = "usr/share/virtio-linux/Image" ``` ```sh mip materialize virtio-kernel -o ./Image ``` ### `[params]` - Repo-wide parameters {#params} Declares named parameters available to tasks across the repository, using the same `{type, help, default}` shape as per-task [`args`](https://minimal.dev/docs/reference/tasks). Every `[params]` entry must declare a `default`. Each parameter acts as a defaulted arg on every task: a command reads it as `%{name}`, and `--name ` overrides it for one run. A task that declares an arg of the same name uses its own arg instead. ```toml [params] greeting = { type = "string", default = "hi" } [tasks.hello] bash = "echo %{greeting}" # `hi`, or `bye` with `--greeting bye` ``` ### `[cache]` - Artifact cache behavior {#cache} ```toml [cache] index_source = "auto" # optional: "auto" (default), "pinned", or "root" fetch_retries = 2 # optional: retry count for remote fetches ``` ### `[stdlib]` - Standard library requirements {#stdlib} ```toml [stdlib] minimum_version = "" # optional; alias: min_version ``` Refuses to operate when the upstream's standard library is older than the declared minimum. --- # Tasks > Full task schema reference: packages, exec/bash commands, state_key, env_vars, patches, args, and interactive mode. Source: https://minimal.dev/docs/reference/tasks Last updated: 2026-10-08 Tasks are defined in a [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) file, and describe a runtime environment + command invocations to be executed using [`mip run `](https://minimal.dev/docs/reference/cli-mip#run) ## Tasks schema Tasks are defined in a `[tasks.]` block in your minimal file. ### `packages` - Packages to be installed in the runtime environment _Optional_ `packages` lists additional [packages](https://minimal.dev/docs/concepts/packages) which will be installed in the tasks' runtime environment. Packages listed here are in addition to any installed due to the stack. ```toml [tasks.my_task] packages = ["python"] # Additionally install python ``` ### `exec` or `bash` - What the task should do `exec` describes a command or invocation that should run when the task is launched. `exec` can be defined either as an argv string: ```toml [tasks.my_task] exec = "pnpm build" ``` or as a program and its list of arguments: ```toml [tasks.my_task] exec = ["pnpm", "build"] ``` When `exec` names a command without an absolute path (or a `./` prefix), the command is resolved to `/bin/` inside the sandbox; in the examples above, `pnpm` runs as `/bin/pnpm`. `bash` describes a bash command that should run when the task is launched. ```toml [tasks.my_task] bash = "echo \"hello\" > hello.txt" ``` A third action, `echo`, prints a fixed string without composing a sandbox at all; useful for pointers and reminders: ```toml [tasks.docs] echo = "Docs live at https://docs.minimal.dev" ``` Each task sets exactly one action. If a task sets two, such as `exec` and `bash`, the file fails to load with an error that names the task and both keys. When [args](#args) are set on the task, arguments can be substituted into the invocation using Nickel's string interpolation [syntax](https://nickel-lang.org/user-manual/syntax/#strings): ```toml [tasks.greet] args.name = "string" bash = "echo \"Hello %{name}!\"" ``` Command strings are always evaluated as Nickel strings, whether or not the task defines any args, so `%{` is significant even in a command that references no argument. A shell format specifier containing `%{` — most commonly curl's `-w "%{http_code}"` — is otherwise read as interpolation of an undefined variable and fails with `nickel eval failed for string`. To pass a literal `%{` through, escape it as `%{"%"}{`: ```toml # Fails: `%{http_code}` is read as interpolation of an undefined `http_code`. [tasks.probe_broken] exec = 'curl -s -o /dev/null -w "%{http_code}" https://example.com' # Works: `%{"%"}{` yields a literal `%{`. [tasks.probe] exec = 'curl -s -o /dev/null -w "%{"%"}{http_code}" https://example.com' ``` ### `description` - Describe the task _Optional_ `description` is a free-text description of the task, shown alongside the task name in [`mip status`](https://minimal.dev/docs/reference/cli-mip). ```toml [tasks.my_task] description = "Run the dev server with hot reload" ``` ### `state_key` - Persist state between invocations _Optional_ `state_key` controls caching of build artifacts and files between runs. ```toml [tasks.my_task] state_key = "dev" # Cache build artifacts under 'dev' ``` `min task run`, and `min run` inside a session, run the task in the session layout. The working directory is the project tree at `/workbench`, and `$HOME` is the session home at `/home`. There, `state_key` backs only `XDG_CACHE_HOME` (`/state/cache`). `XDG_CONFIG_HOME`, `XDG_DATA_HOME` and `XDG_STATE_HOME` point into the shared session home: `/home/.config`, `/home/.local/share` and `/home/.local/state`. `PATH` adds `/home/.local/bin`. The locale is `LANG=C.UTF-8` with no `LC_ALL`, as in the interactive session. `mip run` keeps the host working directory, the `/state`-backed `XDG_*` directories and `LANG`/`LC_ALL=en_US.utf8`. ### `env_vars` - Environment variables to set {#env_vars} _Optional. `env_vars` is an alias of the canonical `vars` key; both parse_ `env_vars` sets environment variables in the tasks' runtime environment. Variables set here take precedence over any inherited from the stack. ```toml [tasks.my_task] env_vars.CC = "gcc" env_vars.AWS_PROJECT = "zest" ``` Environment variables can also inherit their value from the parent process. To do this, declare the variable with the value `{ inherit = true }`: ```toml [tasks.my_task] env_vars.TOKEN = { inherit = true } ``` The parent process is the shell you run `min task run` in: the value is read on the client, at the moment you invoke the task, and carried to the session — the same way `[session.vars]` resolves at activation. So exporting the variable in your shell is enough, and the daemon's own environment is never consulted. A variable declared `inherit` but not set in that shell is an error, reported before the task's session is created — unless your policy ignores the name, in which case it is dropped rather than looked up ([below](#policy)): ```console $ min task run deploy error: task 'deploy' declares `env_vars.TOKEN = { inherit = true }`, but TOKEN is not set in this shell; export it before running the task ``` This applies to `min task run`. Two other ways of running a declared task still resolve `inherit` against the daemon's environment: - `min run `, from *inside* a session, has no invoking client to read from. - `min session run ` runs against a session you already have, over the attach transport, which forwards only `MINIMAL_SESSION_ID` and your locale. So prefer an explicit value for tasks meant to be run either of those ways. ### Environment variables and your policy {#policy} Resolving on the client means reading *your* shell. A project's `minimal.toml` naming `env_vars.AWS_SECRET_ACCESS_KEY = { inherit = true }` is that project asking for your credential, so every name a task declares — inherited or literal — is checked against the `[vars]` section of your `user_policy.toml` **before its value is read**: - a name matching **`deny`** fails the run, naming the rule's file. - a name matching **`ignore`** is dropped and the task runs without it. The name is removed from the task's declarations outright, so this holds for a literal value as much as an inherited one, and an ignored variable does not have to be set. - a name matching **`allow`** is carried. - anything else is **not carried**. On a terminal you are asked, once per name, and can record the answer as a rule; anywhere else the run stops and prints the rules to add. This is the same gate `[session.vars]` from the same file has always passed through, with the same precedence — `deny`, then `ignore`, then `allow` — and `allow` is required for the same reason: a project you have not read should not be able to name a variable and receive its value. An `echo` task is the one exception, and it is not a hole: its output comes straight from the declaration without an environment being built, so its `env_vars` are read by nobody. Nothing is looked up, so there is nothing to gate — an `echo` task never prompts, and never fails over a variable it has no use for. ```toml # user_policy.toml [vars] allow = ["ZZ_TASK_*", "RUST_*"] deny = ["AWS_*"] ``` A denied name: ```console $ min task run deploy error: task 'deploy' declares `env_vars.AWS_SECRET_ACCESS_KEY`, but AWS_SECRET_ACCESS_KEY is denied by `[vars] deny` in ~/.config/minimal/user_policy.toml; remove the declaration, or the deny rule if this task should see it ``` An unlisted name in CI, where there is no one to ask: ```console $ min task run deploy error: task 'deploy' declares 1 environment variable that your policy does not allow, and stdin/stderr is not a terminal. Add the following to ~/.config/minimal/user_policy.toml: [vars] allow = ["DEPLOY_TARGET"] Then re-run this command. ``` ### `interactive` - TUI apps and shells _Optional, Default `false`_ `interactive` indicates that this task must be run interactively (that is, with standard input and a tty connected). ```toml [tasks.my_task] interactive = true ``` ### `patches` - Map in files/directories from the system _Optional. `patches` is an alias of the canonical `patch` key; both parse_ `patches` configures files and directories to be mapped into the tasks' runtime environment. ```toml [tasks.my_task] patches.dir."~/.claude" = "read-write" patches.file."~/.claude.json" = "read-write" ``` `patches` is a structure with two optional fields, `dir` & `file`, each of which contains a mapping of file paths to be mapped in, and the corresponding map mode. The map mode may only be the string "read-only" / "ro" for read-only mappings, or the string "read-write" / "rw" for writeable mappings. If a mapped file or directory does not exist on the host, a read-write mapping creates an empty file or directory. A read-only mapping never creates its source: the path must already exist, and a missing read-only source is an error naming the task and the patch declaration. Mapped paths must be absolute or start with `~/`, in which case the tilde is expanded to the user's home directory. In a task the daemon runs in the session layout, `~/` expands to the session home. The daemon mounts a mapping inside that home or the project tree at the matching path under `/home` or `/workbench`. It mounts the mapping on top of the read-write session mounts, so a read-only mapping stays read-only. It mounts any other absolute path at that same path. ### `inherit_cwd` - Use parent working directory instead of repository root _Optional, Default `false`_ `inherit_cwd` configures minimal to setup the task in the current working directory, instead of the repository root. ```toml [tasks.my_task] inherit_cwd = true ``` Under `min task run`, the task starts at the matching path under `/workbench`: run from `sub/inner`, it starts at `/workbench/sub/inner`. If that directory is not in the uploaded tree, the task starts at `/workbench` and prints a one-line notice on stderr. ### `args` - Pass arguments to tasks {#args} _Optional_ `args` configures named arguments and their datatype, which can be substituted into the command being executed by the task. ```toml [tasks.greeter] args.name = "string" args.greeting = "string" exec = "echo %{greeting} %{name}" ``` Arguments without a default become mandatory for invoking the task. In the example above, running the task `greeter` without its two arguments will trigger an error: ```shell $> mip run greeter error: the following required arguments were not provided: --name --greeting Usage: mip run greeter --name --greeting ``` `min task run` takes the same flags after the task name: ```shell $> min task run greeter --name Alice --greeting hi ``` `min task run` reads its own options (`--path`, `--keep`, `--help`) before the task's. To pass a task argument with one of those names, put it after `--`, as in `min task run deploy -- --path prod`. The project directory is the `--path` option, not a positional argument. Each argument's datatype may be: - a scalar: `"string"`, `"number"`, or `"boolean"` (alias `"bool"`); - an array of a scalar type: `"Array string"`, `"Array number"`, `"Array boolean"`; - an enum of permitted values, written either as the string `"[a, b]"` or as a TOML array `["a", "b"]`. Instead of a bare datatype string, an argument can be declared as a table with a `type` field plus optional `help` (a human-readable description) and `default` (making the argument optional): ```toml [tasks.greeter] args.name = { type = "string", help = "who to greet", default = "world" } exec = "echo Hello %{name}" ``` ## Networking {#networking} A task has no `--network` flag of its own. It uses the network of the session that runs it, which the session's [`--network` mode](https://minimal.dev/docs/reference/cli-min#session-activate) sets: - `min task run` composes an ephemeral session with the default mode, `host_ip`. - `min run ` inside a session and [`min session run `](https://minimal.dev/docs/reference/cli-min#session-run) run the task in a session that already exists. The task takes that session's mode. `host_ip` (the default) shares the host's network namespace. The task reaches everything the host reaches. This includes the host-side hostname proxy, the port clients point `HTTP(S)_PROXY` at. `min ls` prints the port in use. On a native Linux host the proxy listens on `127.0.0.1:7654` by default, and picks a free port when 7654 is busy. On a native host only, [`minimald run --hostname-proxy-port`](https://minimal.dev/docs/reference/cli-minimald#run) pins it. On a host where Minimal runs in a VM (macOS), the task shares the VM's network, not your computer's. `127.0.0.1` inside the task is the VM's loopback. A service listening on your computer's loopback is not reachable at that address. The VM helper assigns the proxy port there, so read it from `min ls` rather than assuming 7654. `own_ip` gives the task its own address on the host's switch, in its own network namespace. This namespace is a second box beside the session's box, not the session's namespace. `127.0.0.1` inside the task is the task's own loopback, so a connection to it does not reach the hostname proxy on the host. Outbound traffic goes through the switch, so a task that fetches from the network does not need the proxy. The task also follows the session's egress policy: when the session cannot reach a destination, the task cannot reach it either. On a VM-backed host, which includes every macOS host, a task in an `own_ip` session has no outbound reach yet. The task takes a switch address that the host's table of boxes does not hold. The switch drops traffic from such an address. Run a task that needs the network in a `host_ip` session there. `none` gives the task no network. Every connection to a destination outside the task fails. When a task needs the network, give its session a mode that has one. --- # Loadouts > Per-developer loadout reference: the loadout TOML schema, config-directory layout, min CLI flags, client config, and how loadouts compose into sessions. Source: https://minimal.dev/docs/reference/loadouts Last updated: 2026-10-04 A loadout is a per-developer bundle of packages, environment variables, file patches, and lifecycle hooks that the [`min` session CLI](https://minimal.dev/docs/reference/cli-min) layers into the sessions it activates. The project's [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) describes what every contributor's session needs; a loadout carries what *you* want on top (your editor, terminal multiplexer, shell config, and dotfiles) so each development environment comes up matching your muscle memory. Loadouts apply to sessions (`min session activate`); they are not used by task sandboxes, which have their own `packages`/`env_vars`/`patches` schema described in [Tasks](https://minimal.dev/docs/reference/tasks). ## Where loadouts live Loadouts live under `/minimal/loadouts/`, in either of two layouts: ``` /minimal/loadouts/.toml # a loadout that is just a file /minimal/loadouts//loadout.toml # a loadout you keep in git ``` `` is the platform user config directory: `$XDG_CONFIG_HOME` on Linux (or `$HOME/.config` when unset); macOS also uses `$HOME/.config` for consistency with Minimal's state and cache dirs. The global [`--config-dir`](https://minimal.dev/docs/reference/cli-min#global-flags) flag overrides the base, and `min dirs` prints the resolved loadouts directory. The two layouts are equivalent in every way but where the bytes sit. Pick the second when you want the whole loadout — its definition, the files it [ships](#loadout_root), its [hook scripts](#lifecycle_hooks) — to be one self-contained directory you can put under version control: ```console $ git clone git@example.com:you/helix-loadout ~/.config/minimal/loadouts/dev $ min session activate . --loadout dev ``` Nothing else changes between them, because a loadout's own directory is `/minimal/loadouts//` under both — see [`$LOADOUT_ROOT`](#loadout_root). To migrate an existing loadout, move it inside the directory its files already live in: ```console $ cd ~/.config/minimal/loadouts && mkdir -p dev && mv dev.toml dev/loadout.toml ``` The **filesystem** names the loadout — the filename stem for `.toml`, the directory name for `/loadout.toml`: - Nothing inside the file names it, so renaming the file (or the directory) renames the loadout. - Names are trimmed and must be usable as a single directory entry: non-empty, neither `.` nor `..`, and with no `/`, `\`, or NUL characters. (A dot is only special as the whole name — `dev.v2` is fine.) - A file that still carries the old `name` field loads anyway, with a warning that the field is no longer required. If the declared name disagrees with the filename, the warning says so and the filename wins; the declared name is discarded. - Defining one name in **both** layouts at once is an error, not a precedence rule. `min loadout list` names both files and exits non-zero, and so does an activation that selects the name — whichever file lost would otherwise go on looking live while having no effect. The directory is not created automatically; create it and add loadouts in either shape to get started. Two things to know if you do version-control a loadout: - A cloned `/` contains a `.git` directory, which a broad `$LOADOUT_ROOT/**/*` patch source would sweep into your session along with everything else. Prefer patterns that name what you actually want. - `/` must be a real directory, not a symlink into a dotfiles repo elsewhere: a loadout's [external hook scripts](#lifecycle_hooks) refuse a symlinked anchor, so they would fail to resolve. Clone into `loadouts//` directly. ## Example A loadout that brings in the helix editor and zellij multiplexer, wired up with the user's dotfiles: ```toml description = "helix + zellij with my dotfiles" packages = ["helix", "zellij"] patches = [ # Helix: single config files plus a themes directory. { dest = ".config/helix/config.toml", source = "~/dotfiles/helix/config.toml" }, { dest = ".config/helix/languages.toml", source = "~/dotfiles/helix/languages.toml" }, { dest = ".config/helix/themes/", source = "~/dotfiles/helix/themes/**/*.toml" }, # Zellij: single config file plus a layouts directory. { dest = ".config/zellij/config.kdl", source = "~/dotfiles/zellij/config.kdl" }, { dest = ".config/zellij/layouts/", source = "~/dotfiles/zellij/layouts/**/*.kdl" }, ] [vars] EDITOR = "hx" VISUAL = "hx" # Declared to warm helix's tree-sitter grammar cache when the session # comes up. Best-effort; failures don't tank activation. [[lifecycle_hooks]] on_activate = { type = "inline", value = "hx --grammar fetch >/dev/null 2>&1 || true" } ``` `[[lifecycle_hooks]]` is top-level here because this is a loadout; in a project `minimal.toml` the same hooks nest under `[session]`, written `[[session.lifecycle_hooks]]`. Saved as `/minimal/loadouts/dev.toml`, this is applied with `min session activate --loadout dev`, or automatically via [`default_loadouts`](#client-config). ## Loadout schema ### `name` - The loadout's identifier _Not a field. Comes from the filename_ A loadout is identified by its filename stem — `dev.toml` is the loadout `dev` — which is what selection and error messages show. The field itself is obsolete. A file that still declares one loads with a warning that it is no longer required, and the filename is used regardless: ```toml # Delete this line; the file is already named `dev.toml`. name = "dev" ``` ### `description` - Describe the loadout _Optional_ Free-form text, shown alongside the name in `min loadout list`. ```toml description = "Editor + terminal multiplexer" ``` ### `packages` - Packages to bring into the session _Optional_ Package names installed into the session, in addition to the session's baseline packages and anything the project contributes. Duplicates across contributors are deduplicated. ```toml packages = ["helix", "zellij"] ``` Names are not checked at activation: an unknown package composes cleanly and fails later, when the session first spawns, with `no such package: `. ### `[vars]` - Environment variables {#vars} _Optional_ Variables set in the session environment. Names must be POSIX-shaped (`[A-Z_][A-Z0-9_]*`); for other names, see [`[[vars_lenient]]`](#vars_lenient). Each value takes one of three forms: ```toml [vars] EDITOR = "hx" # literal value PAGER = { inherit = true, default = "less" } # inherit, with fallback MUXER = { inherit = true } # inherit from the host env ``` - A **literal** string sets the variable to that value. - `PATH` is the one exception to literal values. A `PATH` that contains `$PATH` or `${PATH}` expands that reference to the session's default `PATH`. So `PATH = "/opt/bin:$PATH"` puts `/opt/bin` before the default directories instead of replacing them. A `PATH` without a reference stays verbatim, and every other variable stays literal. - `{ inherit = true }` passes the variable through from the environment of the `min` process on the host. If the host does not have it set, the variable is dropped from the session (with a warning) rather than failing activation, so opportunistically inheriting things like `COLORTERM` is safe. `TERM` is not worth inheriting: the daemon sets it from the terminal you attach from and keeps it current, above anything a loadout composes — see [The attached terminal](#attached-terminal). - `{ inherit = true, default = "..." }` inherits, falling back to `default` when the host does not have the variable set. `inherit = false` is rejected; omit the variable instead. The warn-and-drop rule is a loadout relaxation, not a property of the spelling. The same `{ inherit = true }` in a project's [`[session.vars]`](https://minimal.dev/docs/reference/minimal-dot-toml#session) is **required**: an unset host variable there fails activation instead of being dropped. A project is declaring what every contributor needs, where a loadout is declaring what one developer would like. See [Where `{ inherit = true }` differs](https://minimal.dev/docs/reference/minimal-dot-toml#inherit-divergence) for the rule on every surface that accepts the form. ### `[[vars_lenient]]` - Environment variables with non-POSIX names {#vars_lenient} _Optional_ An explicit opt-in for the rare variable whose name is not POSIX-shaped. Anything the Linux kernel accepts is allowed (no `=`, no NUL). Values use the same three forms as `[vars]`. ```toml [[vars_lenient]] name = "weird-thing" value = "x" ``` ### `patches` - Files copied from the host into the session {#patches} _Optional_ Each row names a `source` on the host and a `dest` inside the session, with an optional `description`. ```toml patches = [ { dest = ".psqlrc", source = "~/dotfiles/psqlrc" }, { dest = "certs/", source = "~/ca/*.pem" }, { dest = ".config/nvim/", source = "~/dotfiles/nvim/**/*.lua" }, ] ``` **`source`** is a host path or glob pattern, or a list of them (a list fans out into one independent patch per entry, each sharing the `dest` -- so list entries only make sense with per-entry dests or glob entries): - A leading `~` expands to the host home directory. `$NAME` / `${NAME}` references (including `$HOME`) resolve against the session's already-resolved variables (declared in `[vars]`); referencing an undefined name is an error. `$$` is a literal `$`. - `$LOADOUT_ROOT` expands to the loadout's own directory, for files you ship beside the loadout itself — see [`$LOADOUT_ROOT`](#loadout_root) below. - After expansion the path must be absolute; anchor home-relative sources with `~/`. - Glob patterns must have a literal directory prefix to walk from: `~/dotfiles/**/*.lua` is fine, a bare `**/*.pem` is rejected. - A plain directory path (no glob metacharacters) is shorthand for `dir/**/*`: `source = "~/dotfiles"` copies the whole tree. Common VCS and dependency directories (`.git`, `.hg`, `.svn`, `CVS`, `.jj`, `node_modules`) are excluded by default, and the total size of a plain-directory source is capped at 100 MiB — a larger tree fails the activation rather than being copied silently. Use an explicit glob to narrow the copy. - `..` components are rejected wherever they appear. - A source path that does not exist on the host is dropped with a warning at activation rather than failing it, so opportunistically patching a dotfile tree the host may not have is safe. Other enumeration failures (permission denied, unreadable entries) still fail the composition. **`dest`** is interpreted relative to the session user's home directory. Absolute paths and `..` components are rejected. For a literal (non-glob) source, `dest` is used verbatim as the destination file path; for glob sources, `dest` is the destination directory and each match's path under the walk root is appended to it. By default the walker does not follow symlinks while enumerating glob matches; see [`follow_symlinks`](#follow_symlinks) and the [client config](#client-config). **Permissions** carry across. A patched file lands with the source file's own permission bits, so a script stays executable and a `0600` key stays readable only by you. Two qualifications: - Preservation is exact, which cuts both ways: a read-only source (a `0444` file, or a dotfile symlinked into a read-only store) lands read-only, and editing it in the session takes a `chmod` first. - `setuid`, `setgid`, and the sticky bit are dropped; the nine standard permission bits are what survives. Ownership is not carried and cannot be: every file in the session belongs to the session user, whatever it was owned by on your host. #### `$LOADOUT_ROOT` - Files shipped with the loadout {#loadout_root} Not every file a loadout patches in belongs in your dotfiles. For the ones that exist only to serve this loadout, keep them beside it and name them with `$LOADOUT_ROOT`, which expands to a directory named after the loadout -- the same directory its [external hook scripts](#lifecycle_hooks) resolve against. That directory is `/minimal/loadouts//` under [either layout](#where-loadouts-live). With a flat `dev.toml` it sits beside the file; with `dev/loadout.toml` it *is* the directory the definition lives in, which is what makes a loadout and its files one version-controllable tree: ``` /minimal/loadouts/ /minimal/loadouts/ ├── dev.toml └── dev/ └── dev/ ├── loadout.toml ├── config.toml ├── config.toml └── themes/ └── themes/ └── nord.toml └── nord.toml ``` ```toml patches = [ { dest = ".config/helix/config.toml", source = "$LOADOUT_ROOT/config.toml" }, { dest = ".config/helix/themes/", source = "$LOADOUT_ROOT/themes/**/*.toml" }, ] ``` It resolves against the loadouts directory actually in use, so a `--config-dir` or `$XDG_CONFIG_HOME` that moves your config takes the loadout's files with it -- which a hard-coded `~/.config/minimal/loadouts/dev/` would not. It is derived from the loadout's *name*, so it is also the same directory whichever layout the loadout is filed in. Details worth knowing: - The reference is **only** available in the `source` of a patch a loadout declared. In a [user policy](https://minimal.dev/docs/reference/user-policy) pattern, or in a patch a project declared, it fails the activation rather than resolving to some other declarer's directory. `dest` is a path inside the session, so it has no use for it either. - The name is **reserved** here: a loadout that also declares a `LOADOUT_ROOT` variable still patches from its own directory. The variable reaches the session normally -- only patch sources ignore it. - `$LOADOUT_ROOT` alone names a directory, which is a plain-directory source: it copies the whole tree (equivalent to `$LOADOUT_ROOT/**/*`), with the default VCS/dependency directory excludes and the 100 MiB size cap applied. Write an explicit glob to narrow the copy. - The directory is optional. A loadout that never references it does not need one, and -- like any other source -- a path that isn't there is skipped with a warning rather than failing the activation. ### `[[lifecycle_hooks]]` - Scripts at session transition points {#lifecycle_hooks} _Optional_ Each hook groups up to four scripts — `on_activate` when the session is created, `on_destroy` when it is destroyed, `on_attach` when you connect, and `on_detach` when you disconnect — and at least one must be present. An optional `description` labels the hook. Scripts are either inline or a path to a file: ```toml [[lifecycle_hooks]] description = "warm caches" on_activate = { type = "inline", value = "cargo fetch || true" } on_detach = { type = "external", value = "./cleanup.sh", timeout = 120 } ``` Each script may set a `timeout` in whole seconds, defaulting to 60 and capped at 300; a script that exceeds the cap is rejected when the file is parsed. Scripts run under POSIX `sh` unless they open with a shebang, in which case they run under what it names — so a hook can be written in whatever you already think in: ```toml [[lifecycle_hooks]] on_activate = { type = "inline", value = "#!/usr/bin/env fish\nset -gx ...\n" } on_detach = { type = "external", value = "./teardown.py" } # #!/usr/bin/env python3 ``` The interpreter must be present *in the session*, and it is handed the script on standard input rather than as a file. That covers every shell and scripting language in common use; it does not cover one that insists on a file argument (`awk -f`). The shebang is parsed the way the kernel parses one — the interpreter is the first word, everything after it is a single argument (so `#!/usr/bin/env -S python3 -u` works, `-S` and all), and there is no quoting, so an interpreter path cannot contain spaces. Give an absolute path or a bare command for `env` to find: a relative path would resolve against the daemon, not the session. Default to POSIX `sh` where you can — it is the one interpreter a session is guaranteed to have. External script paths must be relative (absolute paths and `..` components are rejected at parse time). A loadout's scripts are anchored at the directory named after the loadout — so `dev.toml`'s scripts live in `dev/`, and a `dev/loadout.toml`'s live beside it: ``` /minimal/loadouts/ /minimal/loadouts/ ├── dev.toml └── dev/ └── dev/ ├── loadout.toml ├── activate.sh ├── activate.sh └── teardown.sh └── teardown.sh ``` This is the same directory [`$LOADOUT_ROOT`](#loadout_root) names, so a loadout's scripts and the files it patches in live together. A script must resolve to a regular file inside that directory. Symlinks are rejected rather than followed, at every path component *and at the directory itself* — a symlink is the one way a path that passes every other check could still reach outside the anchor. (This is why a version-controlled loadout directory has to be a real checkout rather than a symlink into a dotfiles repo.) All of this is checked on your machine before a session is created, so a mistyped path fails immediately and names the file. Hooks from multiple contributors concatenate in declaration order, and run in that order for the setup transitions (`on_activate`, `on_attach`) and in reverse for the teardown ones (`on_destroy`, `on_detach`) — so a project sets up before your loadouts and tears down after them. They execute inside the running session, sharing its packages, variables, files, and network. `on_attach` runs on the terminal you are attached to, so its output reaches you and `[ -t 1 ]` is true. The other three have no terminal and their output is captured into the daemon log — `on_detach` included, because the shell it would have run on is often the very thing that just went away (leaving a session by exiting its shell is a detach). On top of the session's own variables, each hook is given a few describing the run it is part of: | Variable | Value | |---|---| | `MINIMAL_SESSION_ID` | The session's id. | | `MINIMAL_SESSION_NAME` | Its display name. | | `MINIMAL_HOOK_EVENT` | The transition: `on_activate`, `on_destroy`, `on_attach`, or `on_detach`. | | `MINIMAL_HOOK_SOURCE_KIND` | `loadout` or `project` — for branching. | | `MINIMAL_HOOK_SOURCE_NAME` | The loadout's name, or the project's path as you refer to it on your own machine (not the daemon's copy of the tree). | | `MINIMAL_HOOK_INDEX` / `MINIMAL_HOOK_COUNT` | Position within this transition's run, 1-based — for logging, e.g. `[2/3]`. | Each teardown transition runs all of its hooks under a **single shared time budget** rather than giving each one its own timeout — `on_detach` and `on_destroy` get a budget each, not one between them. A session has to stay destroyable, and per-script caps alone would let enough hooks add up to hold one open. A hook that runs into the end of the budget is cut short and says so, and one the budget leaves no room for at all is reported as not run — either way the log distinguishes it from a hook that exhausted its own `timeout`, and teardown continues. A failing `on_activate` **fails the activation** — the session does not become attachable, and the error names the hook and what it printed. The other three never block their transition: a failing attach, detach, or destroy hook is logged and the session carries on. A session must always be attachable and always destroyable. `on_failure` was an earlier spelling that never executed; it has been removed. A hook file that still declares it is rejected with an error pointing at `on_destroy`. ### `follow_symlinks` - Symlink handling for patch sources {#follow_symlinks} _Optional_ Overrides the client-wide `[loadouts].follow_symlinks` setting (see [client config](#client-config)) for this loadout's patches only. When unset, the client-wide setting applies. ```toml follow_symlinks = true ``` ## Selecting loadouts at activation [`min session activate`](https://minimal.dev/docs/reference/cli-min#session-activate) decides which loadouts to apply from two flags: | Flag | Description | |------|-------------| | `--loadout ` | Apply the named loadout from `/minimal/loadouts/.toml` or `/minimal/loadouts//loadout.toml`. Repeatable. If given, the config file's `default_loadouts` are ignored | | `--no-loadouts` | Apply no loadouts at all. Conflicts with `--loadout` | Resolution order: 1. `--no-loadouts`: nothing is applied, regardless of configuration. 2. One or more `--loadout NAME`: exactly the named loadouts are applied. 3. Neither flag: the `[loadouts].default_loadouts` list from the [client config](#client-config) is applied. 4. Neither flag and an empty `default_loadouts`: the [built-in `default` loadout](#built-in-default-loadout) is applied, unless a user `default.toml` shadows it. Loadouts are resolved and composed **before** the CLI contacts the daemon: a missing or malformed loadout file fails the activation loudly on the client rather than producing a silently-empty session. When loadouts are applied, the CLI prints `Applying loadouts: ` to stderr. Activation is also when loadout contents are captured: the files are read once, inherited vars are resolved against the host environment, and the composed result is what the session runs with. Editing a loadout file does not change sessions that already exist; destroy and re-activate to pick up the edit. ## Built-in default loadout {#built-in-default-loadout} When a session is activated with no loadout flags and an empty `default_loadouts`, a built-in `default` loadout applies so a fresh box comes up oriented rather than in a bare shell. It contributes **no packages** — only a shaped `PS1` and a once-only banner (the minimal mark, the [orientation lines](#orientation-banner) naming the session and its loadouts, plus a pointer to `min add`), shipped through the [MOTD recipe](#vars-in-the-attach-shell). The banner is TTY-gated, prints exactly once per session, and renders without color. It is the lowest-precedence source: `--no-loadouts`, `--loadout`, and a non-empty `default_loadouts` all take priority, and a user `default.toml` in the loadouts directory shadows it entirely (the file is applied in its place). `min loadout list` shows it as a `default (built-in)` row unless that user file is present. ## Client config {#client-config} Client-wide loadout preferences live in `/minimal/config.toml`, under a `[loadouts]` section: ```toml [loadouts] default_loadouts = ["helix", "fish"] follow_symlinks = false ``` | Key | Default | Description | |-----|---------|-------------| | `default_loadouts` | `[]` | Loadouts (by filename stem) applied to each new session when no `--loadout`/`--no-loadouts` flag is given | | `follow_symlinks` | `false` | Follow symlinks while enumerating loadout patch sources. Turn on when your dotfile tree is a symlink farm (stow, chezmoi) and you want the walk to descend through the links | A missing file is equivalent to the defaults; unknown keys are rejected so a typo (`[loadout]` for `[loadouts]`) fails loudly. ### Session keys {#session-keys} The detach chord is configurable. The leader key (the chord that enters command mode) and its command-mode subcommand keys live under a `[session-keys]` section in the same `config.toml`. `[session-keys]` is client configuration, not a loadout: unlike everything a loadout composes, it is never baked into the session on activation. It is read fresh on every attach and negotiated per SSH channel, so each client — and each machine attaching to a session it didn't create — brings its own chord. The section is documented here only because that is where `config.toml` is described. ```toml [session-keys] leader = "ctrl-]" bell_on_leader = false [session-keys.subcommands] detach = "d" forward = "ctrl-]" ``` | Key | Default | Description | |-----|---------|-------------| | `leader` | `ctrl-]` | The chord that enters command mode, as a logical key name (`"ctrl-]"`, `"ctrl-^"`, `"d"`, …). Rejected loudly at load if termios-special (`ctrl-c`, `ctrl-w`, `ctrl-\`, … — consumed by the line discipline before the app) or wrapping-ambiguous (`ctrl-i` = TAB, `ctrl-m` = CR, …) | | `bell_on_leader` | `false` | Ring the terminal bell (BEL `0x07`) on entering command mode. The terminal renders it per its own bell config; minimal picks no modality | | `subcommands.detach` | `d` | The command-mode key that detaches the channel | | `subcommands.forward` | `ctrl-]` | The command-mode key that verbatim-forwards a leader byte down the PTY (for nested sessions). Defaults to the resolved `leader`, so a double-press forwards | Key names take one of two forms: `ctrl-`, where the glyph is a single ASCII character in `@`..`~` (so `ctrl-2` and `ctrl-?` are rejected), or a single printable ASCII glyph such as `d`. Only `ctrl-` is configurable; `alt-`, `shift-`, `meta-`, and `super-` are rejected. The `ctrl-` prefix is case-insensitive and `ctrl-` letters normalise to lowercase, since `Ctrl+a` and `Ctrl+A` send the same control code. Plain glyphs are case-sensitive. The leader is negotiated with the daemon per attach channel — sent as env vars alongside `MINIMAL_SESSION_ID` — so two clients with different configs on the same session each get their own chord. The daemon re-validates the leader as a silent backstop: a chord it rejects is logged and only that field falls back to the default — your valid `detach`/`forward` remaps survive — never garbling the screen. As with `[loadouts]`, every field defaults and unknown keys are rejected, so an old config keeps parsing. Two caveats on that per-channel model: - **The banner's detach hint is mint-scoped.** The orientation banner prints `MINIMAL_DETACH_HINT`, seeded from the channel that minted the shell. A second client attaching with a remapped chord gets a *working* chord, but the banner still advertises the minting channel's; trust your config over the banner in that case. - **Bindings must not shadow each other.** A `detach` that equals the `leader` or the `forward` key makes that other binding unreachable and is rejected at load; the daemon's backstop reverts just the detach field. ## Listing loadouts [`min loadout list`](https://minimal.dev/docs/reference/cli-min#loadout-list-alias-ls) (alias: `min loadout ls`) enumerates every `*.toml` file in the loadouts directory, one row per file: ``` NAME DESCRIPTION CONTRIBUTES * dev helix + zellij with my dotfiles 2 pkg / 4 var / 5 patch extra 1 pkg / 0 var / 0 patch default (built-in) orientation banner and shaped prompt 0 pkg / 3 var / 0 patch default (built-in) applied when no loadouts are configured * default (from `[loadouts].default_loadouts`) ``` The two trailing lines are the legend `min loadout list` prints below the table: the first appears only when the built-in `default` is shown, the second only when `default_loadouts` is non-empty. - Loadouts named in `default_loadouts` are marked with a leading `*`. - The [built-in `default` loadout](#built-in-default-loadout) is listed as a `default (built-in)` row unless a user `default.toml` shadows it. - Malformed entries are listed with their parse error so they can be fixed in place; a `default_loadouts` entry with no matching file produces a warning. - `--dir ` overrides the loadouts directory. ## Composition, conflicts, and policy At activation, the client composes the selected loadouts into a single contribution and ships it to the daemon, where it is merged with the project's contribution (the `[session]` block of the project's `minimal.toml`, plus per-package contributions) into the session's final configuration. Merge semantics across all contributors: - **Packages** deduplicate: set semantics, there is no value to disagree on. - **Vars** with the same name and the same resolved value deduplicate. The same name with *different* values is a hard conflict that fails the composition; there is no override precedence between loadouts and the project. The error's hint applies: add the name to your policy's `ignore` list to drop all contributors of that variable. - **Patches** with the same destination and different sources are likewise a conflict. - **Lifecycle hooks** concatenate in declaration order; setup transitions run in that order and teardown transitions in reverse. Two loadouts with the same name cannot be applied together. Loadout contributions are gated by the user's policy (`/minimal/user_policy.toml`): items you declare yourself automatically pass the `allow` check, but the policy's `deny` and `ignore` rules still apply: a loadout patch matching a `deny` pattern fails the composition on the client, before the daemon is involved. A missing policy file means an empty policy; a fresh install activates fine without it. ## Vars in the attach shell The interactive shell minted by [`min session attach`](https://minimal.dev/docs/reference/cli-min#session-attach) is bash unless a loadout says otherwise — see [`SHELL`](#session-shell) below. bash is started as `bash --noprofile --rcfile -i`, so it finds none of its startup files on its own: the daemon's rc is the only file it is given. That rc installs the [attached terminal](#attached-terminal) hook and then sources **your `~/.bashrc`** out of the session home, so a [patched-in](#patches) `.bashrc` applies — the same way a patched `.zshrc` or `config.fish` does for those shells. `~/.bashrc`, and nothing else — that is the file bash would read here on its own, had `--rcfile` not displaced it. A session shell is an interactive **non-login** bash, and the login chain (`~/.bash_profile`, `~/.bash_login`, `~/.profile`) belongs to a login shell, which this is not: `shopt login_shell` reports off inside it. Those files hold once-per-login setup, so running them here would be a thing bash does nowhere else. If your bash setup lives in `~/.bash_profile`, point the patch at `.bashrc` — a patch names its own destination, so the file's name on your host doesn't have to be the name it lands under: ```toml patches = [ { dest = ".bashrc", source = "~/dotfiles/bash_profile" }, ] ``` The daemon's hook goes in before your file runs, which leaves one sharp edge: bash's refresh is a `DEBUG` trap, and a `~/.bashrc` that installs a `DEBUG` trap of its own replaces it. Have your trap call `__minimal_attach_env` to keep the [`TERM` refresh](#attached-terminal) working. Interactive setup can also travel through the environment, i.e. through `[vars]`, which is where the prompt and the banner live: - **Prompt**: the session launcher seeds a baseline environment (a stock `PS1`, plus the [orientation banner](#orientation-banner) vars below) before merging in the composed vars, and a composed var overwrites a baseline entry with the same name. Setting `PS1` in `[vars]` therefore replaces the stock prompt. This baseline is a layer *beneath* composition, not a contributor: the no-override conflict rule above arbitrates between contributors and does not apply to the launcher's defaults. - **Banner / MOTD**: bash evaluates `PROMPT_COMMAND` from the environment before the first interactive prompt, so a once-only banner can ship as a payload var plus a trigger that clears its own payload: ```toml [vars] PROMPT_COMMAND = 'if [ -n "${MINIMAL_MOTD:-}" ]; then eval "$MINIMAL_MOTD"; unset MINIMAL_MOTD; fi; if [ -r "${MINIMAL_ATTACH_ENV:-}" ]; then . "$MINIMAL_ATTACH_ENV"; fi' MINIMAL_MOTD = ''' [ -t 1 ] && printf '%s\n' '' ' Welcome to the dev session.' '' ''' ``` The trigger clears `MINIMAL_MOTD`, so the banner prints exactly once and never runs for non-interactive commands; the `[ -t 1 ]` guard keeps redirected output clean. Multi-line literal values survive composition intact. Replacing `PROMPT_COMMAND` costs you nothing but the banner: the [attached terminal's](#attached-terminal) `TERM` is refreshed by a hook the daemon installs, not by this variable. ### `SHELL` - Which shell an attach starts {#session-shell} Set `SHELL` in `[vars]` to be dropped into that shell instead of bash: ```toml packages = ["fish"] [vars] SHELL = "/usr/bin/fish" ``` Only the file name is read, so a value carried over from your host (`/opt/homebrew/bin/fish`, a Nix store path) still works — the path itself names a filesystem the session does not have. Five shells are supported, each because the session can install it *and* the daemon ships it an [attached terminal](#attached-terminal) hook: | `SHELL` | Package to install | |---|---| | `bash` | `bash` (always present) | | `sh` | `bash` — it ships `sh` as a POSIX-mode symlink, so this needs nothing | | `zsh` | `zsh` | | `fish` | `fish` | | `nu` | `nushell` | Anything else starts bash and prints one line saying why. A shell from that table which the session hasn't installed is named along with the package that would supply it; a `SHELL` naming anything else is told which five shells there are to choose from. Nothing fails either way. This is the loadout's `SHELL`, not the `$SHELL` of the terminal you ran `min` from: a session is a declared environment, and the shell it hands you is part of the declaration. A project sets it the same way through its [`[session.vars]`](https://minimal.dev/docs/reference/minimal-dot-toml#session). Four things worth knowing: - **Every shell reads your startup file.** zsh, fish, and nushell find theirs in the session home — which is where [patches](#patches) land — on their own; bash is handed none of its own files, so the daemon's rc sources yours for it ([above](#vars-in-the-attach-shell)). The one exception is `sh`, whose startup file *is* `$ENV` by POSIX rule, and `$ENV` is the daemon's. - **The shell is chosen once**, at the attach that mints the session shell, and that shell outlives later attaches. Changing `SHELL` afterwards takes a new session. - **The stock prompt follows where it can.** The session default `PS1` is written in bash's syntax, so zsh is given the same prompt in zsh's syntax instead of printing `\u@\h` at you; fish and nushell build their prompts from their own config and ignore `PS1` entirely. A `PS1` you set in `[vars]` always wins, in whatever syntax you wrote it. - **The banner does not follow.** The orientation banner rides `PROMPT_COMMAND`, which is bash's; a fish or nushell session simply doesn't print it. `TERM` refresh *does* work in all five — that is what the daemon's hooks are for. ### The attached terminal {#attached-terminal} A session shell is spawned once and outlives the terminals that attach to it, so `TERM` — a fact about *the terminal currently attached* — cannot live in its environment alone. The shell may have been minted for a different terminal, or for no terminal at all: a lifecycle hook or an exec brings the sandbox up the same way an attach does. **Nothing is required of a loadout or a project for this to work.** The daemon publishes the current terminal's facts on every attach, and installs the hook that re-reads them into every session rootfs, for every shell it supports. This is deliberately not carried by a `[vars]` entry: an environment variable is composed, so a loadout could replace it — and the MOTD recipe above unsets `PROMPT_COMMAND` outright — which would leave the session's `TERM` depending on whether an author knew to preserve it. The published files, rewritten on each attach (edits to them are lost): | Var | File | Read by | |---|---|---| | `MINIMAL_ATTACH_ENV` | `~/.local/state/minimal/attach-env.sh` | bash, zsh — POSIX `export TERM='…'` | | `MINIMAL_ATTACH_ENV_FISH` | `~/.local/state/minimal/attach-env.fish` | fish — `set -gx TERM '…'` | | `MINIMAL_ATTACH_ENV_JSON` | `~/.local/state/minimal/attach-env.json` | nushell — data, not a script | `ENV` is also set, to a daemon-owned file that sources the POSIX form at shell startup — see [Other POSIX shells](#attached-terminal-posix). The variables are there for anything you want to script against. The hooks themselves do not read them: each hard-codes the path, so a composition that happens to set a variable of that name cannot redirect the refresh. The hooks live at each shell's own vendor/system integration point, inside the rootfs rather than the session home — so a loadout patch cannot collide with one: | Shell | Hook location | At the prompt | Before a command | |---|---|---|---| | bash | the rc the session shell is started with (`--rcfile`) | — | `DEBUG` trap | | zsh | `/etc/zsh/zshrc` | `precmd` | `preexec` | | fish | `/usr/share/fish/vendor_conf.d/` | `fish_prompt` event | `fish_preexec` event | | nushell | `/usr/share/nushell/vendor/autoload/` | `pre_prompt` | `pre_execution` | Two hooks, because they answer different questions. The prompt one keeps a shell that is sitting idle current. The pre-execution one matters when you re-attach: the prompt on your screen was drawn *before* you detached, and it has already fired its prompt hook — so without a hook that runs before the command, the first thing you type at that restored prompt would still see the terminal you had last time, and only the *next* prompt would catch up. bash's `DEBUG` trap covers both cases on its own. bash uses a trap rather than `PROMPT_COMMAND` for the same reason the mechanism is not a var at all: `PROMPT_COMMAND` is composed, and the MOTD recipe unsets it. A trap is shell state, which nothing in a composition can reach. Your own `~/.bashrc` can, though — the same rc sources it, right after setting the trap — so a `DEBUG` trap you install there replaces this one; have your trap call `__minimal_attach_env` to keep the refresh. #### Other POSIX shells {#attached-terminal-posix} A plain `sh`, dash, the BusyBox or BSD ashes, ksh and mksh get a weaker guarantee, and the limit is POSIX's rather than ours. The one hook POSIX defines is `$ENV`: an interactive shell expands it and sources that file at **startup**. The daemon points `ENV` at a file of its own (`/usr/share/minimal/attach-env-posix.sh`) which sources the published POSIX form, so any such shell starts with the terminal that is current at that moment — including one started from a parent whose own value was stale. What these shells cannot do is *refresh*. POSIX has no per-prompt or per-command hook, and none can be built: `PS1` expansion cannot change the shell's own environment, because a command substitution runs in a subshell and `${var:=word}` assigns only when the variable is unset. So a `sh` you left running across a re-attach keeps the terminal it started with. Two ways out, either of which is a one-liner: ```sh . "$MINIMAL_ATTACH_ENV" # re-sync this shell exec sh # or just start a new one ``` ksh93 is the exception in this family — its `PS1.get` discipline function is a genuine per-prompt hook — but the daemon does not install one, so ksh93 behaves like the rest here. Anything the daemon itself runs inside the session — an exec, a lifecycle hook — gets the current terminal's facts applied directly, above the composed vars, without any of this. ### Orientation banner {#orientation-banner} Unless a loadout overrides it, the first interactive prompt of an attached session prints a two-line orientation banner: ``` minimal · session api-server-4f2a · loadout default (built-in) detach: ctrl-] then d · no minimal.toml here — min init to add one ``` The second line drops the `min init` pointer when the session workspace carries a `minimal.toml` (either layout, `minimal.toml` or `.minimal/minimal.toml`) — the template tests the workspace root (`/workbench`) in-shell at the moment it prints, so the clause reflects the session's actual filesystem: it stays correct when an activation skipped the file upload, and disappears after an in-session `min init` once a fresh shell launches. The banner is TTY-gated, prints exactly once, and is plain text (`NO_COLOR`-safe). It ships as a *static template* in the launcher baseline (the MOTD recipe above), interpolated by the shell at print time from two env vars every session carries: | Var | Value | |-----|-------| | `MINIMAL_SESSION_NAME` | The session's name | | `MINIMAL_LOADOUTS` | Display list of the active loadouts: comma-joined names, `default (built-in)` for the zero-config fallback, `none` with `--no-loadouts` | Both are seeded daemon-side in the launcher baseline; the loadout list travels from the client as a first-class field on the composition (control-plane data, never a session var), so user vars and user policy cannot collide with either. Because the trigger lives in the baseline layer, a loadout that sets its own `PROMPT_COMMAND` replaces the banner cleanly — and can interpolate the same `$MINIMAL_*` vars in its own MOTD, as the built-in `default` loadout does for its orientation lines. It carries no obligation beyond the banner — the [attached terminal's](#attached-terminal) `TERM` is kept current by a daemon-installed shell hook that no composition can reach. --- # Build specs > Build spec schema for defining Minimal packages in Nickel: build_deps, runtime_deps, commands, outputs, tests, and metadata. Source: https://minimal.dev/docs/reference/build-specs Last updated: 2026-08-21 Build specs declare everything about a [package](https://minimal.dev/docs/concepts/packages), the fundamental unit of software in Minimal. This declaration includes: * The packages that are needed to build the software, including runtime dependencies (i.e. need to be present wherever this software runs) * The source code, and configuration to build the software * Extensive metadata capture information such as software version, the provenance of source code, directories used at runtime for state, and a number of other data points. Build specs are defined in a [Nickel](https://nickel-lang.org/) file located at `packages//build.ncl`, either in your codebase or any layer in your [software supply chain](https://minimal.dev/docs/concepts/software-supply-chain). The `packages/` directory in a layer is always adjacent to the [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) file at its base. The directory can be omitted if the layer does not define any packages. ## Example Here is a simplified build spec for `jq`, based on the [Minimal Public Package Registry](https://github.com/gominimal/pkgs/tree/main/packages): ```ncl let { standaloneTest, Attrs, BuildSpec, Local, OutputBin, OutputData, OutputLib, Source, Test, .. } = import "minimal.ncl" in let base = import "../base/build.ncl" in let glibc = import "../glibc/build.ncl" in let make = import "../make/build.ncl" in let toolchain = import "../toolchain/build.ncl" in let version = "1.8.1" in { name = "jq", build_deps = [ { file = "build.sh" } | Local, { url = "https://github.com/jqlang/jq/releases/download/jq-%{version}/jq-%{version}.tar.gz", sha256 = "2be64e7129cecb11d5906290eba10af694fb9e3e7f9fc208a311dc33ca837eb0", extract = true, } | Source, base, make, toolchain, ], runtime_deps = [ glibc, ], cmd = "./build.sh", build_args = { include version, }, outputs = { jq = { glob = "usr/bin/jq" } | OutputBin, libjq = { glob = "usr/lib/libjq.{so*,a}" } | OutputLib, mans = { glob = "usr/share/man/**" } | OutputData, }, tests = { smoketest = standaloneTest "/bin/jq --version", basic = { class = 'Standalone, test_deps = [base], cmds = [ ["/bin/bash", "-c", "echo '{\"name\":\"minimal\"}' | jq -r '.name' | grep -q minimal"], ], } | Test, }, attrs = { upstream_version = version, license_spdx = "MIT", source_provenance = { category = 'GithubRepo, owner = "jqlang", repo = "jq", }, } | Attrs, } | BuildSpec ``` Note the shapes this uses, because they are load-bearing: * The spec is a plain record with the `BuildSpec` contract applied at the end (`} | BuildSpec`). The stdlib also exports `build`, so `build { ... }` is an equivalent spelling. * A dependency on another package is a Nickel import of that package's `build.ncl`, used directly in the array. Source archives and adjacent files are records carrying the `Source` and `Local` contracts. * Outputs and tests carry their own contracts (`OutputBin`, `OutputLib`, `OutputData`, `Test`); `standaloneTest` is a shorthand that builds a `Standalone` test from a single command string. * Binding `version` once and forwarding it through `build_args` keeps a version bump to a single edit, since `build.sh` reads it as `$MINIMAL_ARG_VERSION`. Each package also has a `build.sh` script adjacent to `build.ncl` that performs the actual compilation. That script is responsible for making the build byte-reproducible; see [build reproducibility](https://minimal.dev/docs/reference/reproducibility) for the per-toolchain determinism flags it needs. More examples are maintained in the [Minimal Public Package Registry](https://github.com/gominimal/pkgs/tree/main/packages). ## Schema The canonical typing of a Build-spec is defined using [Nickel](https://nickel-lang.org/) in Minimal's embedded standard library, in [`crates/stdlib/minimal-ncl/minimal.ncl`](https://github.com/gominimal/minimal/blob/main/crates/stdlib/minimal-ncl/minimal.ncl) (the `BuildSpec` contract). | **Field name** | **Type** | **Usage** | |---|---|---| | `name` | String | Declares the name of the package. May only contain alphanumeric characters, dashes, and underscores. Must match the name of the containing directory. | | `build_deps` | Array<BuildDep> | A list of dependencies that are needed at build time. This may be:- Other packages- `Subset`s of other packages- `Source` objects- `Local` objects | | `runtime_deps` | Array<RuntimeDep> | A list of dependencies that are needed both at build time, and wherever the package is needed. These may be:- Other packages- Subsets of other packages | | `cmd` | String or Array<String> | The command that is invoked to build the package, either as a single string or as an array of arguments. | | `cmds` | Array<String or Array<String>> | Several commands, run in order, each in either of the `cmd` forms. | | `build_args` | Map<String, String> | A list of arguments to be passed to the build command. These are made available to the build commands as environment variables. eg: `build_args.abc = "def"` would be available as the environment variable `MINIMAL_ARG_ABC` with the value `def`. | | `outputs` | Map<String, Output> | The named set of output files that are captured from a build. If no files match a named output, it's a build error. | | `attrs` | Attrs | The typed set of metadata attached to a given package. | | `needs` | Needs | Any abstract needs declared on a given package. | | `tests` | Map<String, Test> | A list of tests that are run by [`mip check`](https://minimal.dev/docs/reference/cli-mip#check). | | `target` | String | The target string this package supports. Defaults to the current target. | | `prebuilt` | Bool | Indicates that a package is not built, but just the unpacked files from the first `Source` object in `build_deps`. | --- # Build reproducibility > How to make Minimal package builds byte-reproducible: the environment the sandbox provides, per-toolchain determinism flags for C/C++, Go, Rust and the Linux kernel, and how to verify. Source: https://minimal.dev/docs/reference/reproducibility Last updated: 2026-08-21 Two builds of the same [build spec](https://minimal.dev/docs/reference/build-specs) from the same source must produce **byte-identical** output. Minimal's artifact cache is content-addressed: a package is identified by the hash of its inputs, and the artifacts stored under that key are reused everywhere the same key is resolved. A build that embeds a timestamp, a build path, or a random ID produces a different artifact each time it runs, which makes the cached result unverifiable — you can no longer confirm that what you fetched is what the spec describes. Minimal makes the *environment* reproducible for you. Making the *compiler output* reproducible is the build script's responsibility, and it is toolchain-specific. For background and the full taxonomy of non-determinism in software builds, see [reproducible-builds.org](https://reproducible-builds.org/). ## What the sandbox already provides Package builds run in a hermetic, isolated [sandbox](https://minimal.dev/docs/concepts/sandboxing) so the machine running the build cannot leak into its result. The one thing the sandbox shares with the outside world is network access, and only when a dependency calls for it — so a build that reaches the network is reproducible only to the extent that what it fetches is pinned. `Source` entries are pinned for you, because Minimal verifies them against the `sha256` in the spec. Anything a build resolves for itself over the network — Go modules, cargo crates, npm packages, all of which require an explicit [`needs`](https://minimal.dev/docs/reference/build-specs) declaration — is an unpinned build input unless a committed lockfile pins it. Among the fixed environment the sandbox sets for every build are two variables that most build systems honour: | Variable | Value | Effect | |---|---|---| | `SOURCE_DATE_EPOCH` | `0` | The [standard](https://reproducible-builds.org/docs/source-date-epoch/) timestamp override. Build systems that respect it stamp this instead of the wall clock. | | `PYTHONHASHSEED` | `0` | Pins Python's string-hash randomisation, so iteration order in build-time Python scripts is stable. | Do **not** set either of these in your `build.sh`. They are already set, and [`mip check`](https://minimal.dev/docs/reference/cli-mip#check) fails the package if it finds them being assigned — the build-script audit reports `SOURCE_DATE_EPOCH=0 is already set by the build sandbox`. ## Per-toolchain recipes Apply **only** the section matching your build system. These are alternatives, not a checklist to run all of. ### C / C++ Strip the build directory out of debug info, stop recording the exact compiler invocation, drop the linker's random build ID, and make `ar` write deterministic archives: ```bash export CFLAGS="${CFLAGS:-} -ffile-prefix-map=$(pwd)=/builddir -gno-record-gcc-switches" export CXXFLAGS="${CXXFLAGS:-} -ffile-prefix-map=$(pwd)=/builddir -gno-record-gcc-switches" export LDFLAGS="${LDFLAGS:-} -Wl,--build-id=none" export ARFLAGS=Drc ``` Note the `${CFLAGS:-}` form: these determinism flags must be *added* to whatever optimisation flags the package already sets, not substituted for them. If you are building a package that ships its own `ar`/`ranlib` — binutils, or a cross-binutils — also pass `--enable-deterministic-archives` to `./configure`, which makes the tools it installs default to deterministic archives. It is a binutils configure option, not general autotools determinism advice: other packages ignore it with an `unrecognized options` warning, so `ARFLAGS=Drc` above is what actually does the work for them. With autotools, delete libtool archives after install — they embed absolute paths: ```bash find "$OUTPUT_DIR" -name '*.la' -delete ``` ### Go Pass `-trimpath`, clear the build ID, and turn off VCS stamping on every `go build` and `go install`: ```bash go build -trimpath -buildvcs=false -ldflags "-buildid=" -o "$OUTPUT_DIR/usr/bin/my-tool" . ``` Set all three unconditionally. Go's default is `-buildvcs=auto`, which stamps commit metadata whenever the main package, its module, and the working directory sit in the same repository — and Go searches parent directories for that repository root, so it is not enough to observe that the extracted source tree has no `.git` of its own. Passing `-buildvcs=false` removes the question. Setting it once for the whole script works too: ```bash export GOFLAGS="-trimpath -buildvcs=false" ``` ### Rust Remap both the build directory and the cargo registry path: ```bash export RUSTFLAGS="${RUSTFLAGS:-} --remap-path-prefix=$(pwd)=/builddir --remap-path-prefix=$HOME/.cargo=/cargo" ``` As with the C flags above, append rather than overwrite — a package that needs `-C linker=gcc` still needs it after this line. If two builds still differ in `.text` or `.rodata`, the remaining variation is usually parallel codegen. Pin it: ```bash export RUSTFLAGS="$RUSTFLAGS -C codegen-units=1" ``` If the crate graph pulls in [`const-random`](https://github.com/tkaitchuck/constrandom) (commonly by way of `ahash`), it seeds a constant at compile time from system randomness, which differs on every build. That crate reads its seed from the environment, so pin it as well: ```bash export CONST_RANDOM_SEED=0 ``` This is that crate's own variable, not a `rustc` or Cargo setting — setting it does nothing for a dependency tree that does not use `const-random`. ### Linux kernel Kbuild stamps its own identity strings, which `SOURCE_DATE_EPOCH` does not cover: ```bash export KBUILD_BUILD_TIMESTAMP=@0 export KBUILD_BUILD_USER=builder export KBUILD_BUILD_HOST=minimal ``` Those three cover the timestamp, user and host only. Two further inputs bite in practice: - **Build paths.** An out-of-tree build can put absolute filenames in the debug info. Remap them in the assembler flags as well as the compiler flags, so `.S` files are covered alongside `.c`: `export KCFLAGS="-fdebug-prefix-map=$(pwd)=/builddir"` and the same value in `KAFLAGS`. - **Module signing.** With `CONFIG_MODULE_SIG_ALL` enabled, Kbuild generates a fresh temporary key per build, so every module signature differs. The simplest fix is to disable it; signing reproducibly means treating a persistent key as a source input and attaching detached signatures in a second pass. The kernel documents its own remaining cases — `CONFIG_RANDSTRUCT`'s seed, `CONFIG_IKHEADERS` timestamps, and the module-signing workflow above — in [Reproducible builds](https://docs.kernel.org/kbuild/reproducible-builds.html). ### Builds that stamp their own wall-clock time Some build systems generate version strings or headers from the current time without consulting `SOURCE_DATE_EPOCH`. Pin that specific stamp at its source — override the variable the generator reads — rather than re-exporting `SOURCE_DATE_EPOCH`, which the checker rejects. For example, a package whose version header is generated by make variables holding a date and a build time can override both: derive the date from `$SOURCE_DATE_EPOCH` and set the build-time variable to empty so the time is omitted entirely. ## Verifying Build the package twice and compare the two `$OUTPUT_DIR` trees. They must be byte-for-byte identical. Force both runs to actually build. By default a build is served from the binary cache when a matching artifact exists, so a plain second `mip package build` can hand back the first run's artifact and the comparison proves nothing: ```bash mip --no-cache --no-fetch package build --rebuild ``` [`--no-cache`](https://minimal.dev/docs/reference/cli-mip) ignores locally-available artifacts, `--no-fetch` stops Minimal fetching them from the remote cache, and `--rebuild` builds the named packages even when they are already available. Keep the tree each run produces, then compare them: ```bash diff -r first-output/ second-output/ ``` When they differ, the diff names the cause. Common signatures and their fixes: | Symptom in the diff | Cause | Fix | |---|---|---| | An embedded date or time string | Build stamps the wall clock | Pin the generator's own date variable | | An absolute path under the build directory | Path baked into debug info or an `.la` file | `-ffile-prefix-map` / `--remap-path-prefix`; delete `*.la` | | A differing `.note.gnu.build-id` section | Linker-generated random build ID | `LDFLAGS="-Wl,--build-id=none"` | | Differing member order or timestamps in a `.a` | Non-deterministic `ar` | `ARFLAGS=Drc` | | Varying uid/gid on extracted files | Extraction restored the archive's recorded ownership | Extract with `tar -xof` | | Varying mtime on installed files | A step stamped the wall clock onto files it wrote | Normalise explicitly, e.g. `find "$OUTPUT_DIR" -exec touch -hd @"$SOURCE_DATE_EPOCH" {} +` | `mip check`'s build-script audit flags plain `tar -xf` and directs you to `tar -xof`. Note what that fixes and what it does not: `-o` is `--no-same-owner`, so it only settles ownership. Extraction still restores each member's recorded mtime — which is itself fine, because those timestamps are fixed by the archive's content. Wall-clock mtimes come from build steps that write files, which is why the two rows above have different fixes. ## See also - [Build specs](https://minimal.dev/docs/reference/build-specs) — the schema a package is declared with. - [Packages](https://minimal.dev/docs/concepts/packages) — how built artifacts are cached and reused. - [`mip check`](https://minimal.dev/docs/reference/cli-mip#check) — runs the build-script audit described here. --- # Stacks > Stack spec schema for defining build system stacks in Nickel: packages, build commands, env vars, and project detection rules. Source: https://minimal.dev/docs/reference/stack-specs Last updated: 2026-07-29 Stacks specify a set of tools and how to use them in a codebase. They encapsulate a common pattern for building software. Stacks are defined in a [Nickel](https://nickel-lang.org/) file located at `stacks//stack.ncl`, either in your codebase or any layer in your [software supply chain](https://minimal.dev/docs/concepts/software-supply-chain). The `stacks/` directory in a layer is always adjacent to the [`minimal.toml`](https://minimal.dev/docs/reference/minimal-dot-toml) file at the base of the layer. The directory can be omitted if the layer does not define any stacks. ## Examples A number of stacks are defined and maintained in the [Minimal Public Package Registry](https://github.com/gominimal/pkgs/tree/main/stacks), which serves as the canonical reference for defining stacks. Here is a stack for code that uses `pnpm`: ```ncl let { stack, .. } = import "minimal.ncl" in stack { name = "pnpm", runtime_packages = ["node"], build_packages = ["pnpm", "base"], # A command that generates the build command, i.e. commands executed by `mip run build`. build_cmds_cmd = [ "/bin/bash", "-c", m%" # Run pnpm install if no packages are installed (first start) [ -z "$(pnpm list 2>/dev/null)" ] && echo "/bin/pnpm install" echo '/bin/pnpm build' "% ], # Rules to detect when the stack is applicable to a codebase. matches_project_if_any = [ { file_regexes = { "pnpm-lock.yaml" = "*", "pnpm-workspace.yaml" = "*", }, } ], } ``` ## Schema ### `name` _String_ The name of the stack. Must match the name of the containing directory. ### `runtime_packages` _Array of String's, optional_ A list of package names that must be present wherever the codebase is run, including in its compiled form. This typically contains system libraries that must be present, and in the case of interpreted languages, this usually references the interpreter as well. ### `build_packages` _Array of String's, optional_ A list of package names that must be present when the build command, `cmd`, is invoked. This usually contains build-time tools such as pkg-config or compilers. ### `build_env_vars` _Dictionary of String's, optional_ Pairs of environment-variable names and their values that should be set for a build. These are inherited by all tasks in a codebase that uses the stack, unless the task sets the environment variable itself. ### `build_cmd` or `build_cmds_cmd` _`build_cmd`: String or array of strings for the build command_ _`build_cmds_cmd`: String or array of strings for a command that generates the build command_ `build_cmd` and `build_cmds_cmd` are mutually exclusive fields that declare the command for building software which uses this stack. `build_cmd` defines the command to run when [`mip run build`](https://minimal.dev/docs/reference/cli-mip#run) is invoked. It can be a shell-style string or an array containing the executable followed by its arguments. ```ncl let { stack, .. } = import "minimal.ncl" in stack { name = "rust", build_packages = ["gcc", "rust", "binutils", "pkgconf"], build_cmd = ["cargo", "build", "--release"], # [!code focus] # ... } ``` `build_cmds_cmd` defines the arguments for a command that generates the build command. This command is expected to print the necessary build commands, with each terminating with a newline. ```ncl let { stack, .. } = import "minimal.ncl" in stack { name = "rust", build_packages = ["gcc", "rust", "binutils", "pkgconf"], build_cmds_cmd = [ # [!code focus:9] "/bin/bash", "-c", m%" # Run pnpm install if no packages are installed (first start) [ -z "$(pnpm list 2>/dev/null)" ] && echo "/bin/pnpm install" echo '/bin/pnpm build' "% ], # ... } ``` ### `matches_project_if_any` _Array of ProjectMatcher objects, optional_ Defines a list of rules which detect when a stack is applicable to some codebase. This is the mechanism underlying [`mip init`](https://minimal.dev/docs/reference/cli-mip). A stack is considered applicable if any ProjectMatcher in the array matches. Each matcher has the following fields, all optional: * `file_regexes` - A map of file paths to regexes that must match in the codebase for this matcher to be considered matched. The regex syntax is as per the [regex](https://docs.rs/regex/) crate. If the special regex string `*` is used, this predicate automatically matches if the file exists. * `file_predicates` - A map of file paths to [jq](https://jqlang.org/) filters that must match for the matcher to be considered matched. * `build_package_if_any` - A map of package names to a list of `PackageMatcher` objects. The additional package will be wired as a build package if any object in the list matches. * `runtime_package_if_any` - A map of package names to a list of `PackageMatcher` objects. The additional package will be wired as a runtime package if any object in the list matches. The `PackageMatcher` object has both `file_regexes` and `file_predicates` fields, with identical semantics. --- # Sandbox operations > In-sandbox min helper commands: add packages, search, check configuration, and run tasks from within a sandbox. Source: https://minimal.dev/docs/reference/sandbox-operations Last updated: 2026-08-07 `min` helper commands are available within task sandboxes (Linux-only) and inside [sessions](https://minimal.dev/docs/reference/cli-min), which install the same helper as `/usr/bin/min`. > **Naming note**: the in-sandbox `min` helper is a different tool from the > [`min` session CLI](https://minimal.dev/docs/reference/cli-min) that happens to share its name. Inside a > task sandbox, `min` always refers to the helper documented here; the session > CLI is not available there. ## Commands ### `add [FLAG] ` {#add} Installs tools & dependencies into the running sandbox. The default differs by sandbox type: in a **session**, `min add ` with no flag defaults to `--session`, installing the package live and recording it in the `[session]` `packages` list of the project's `minimal.toml`; in a **task sandbox**, no flag installs for the current sandbox only and `minimal.toml` is not modified. With a flag, the named packages are recorded as a session, runtime, or build dependency in `minimal.toml`. | Flag | Description | |------|-------------| | `--session` | Also add packages to the `[session]` `packages` list in `minimal.toml` | | `--runtime` | Also add packages to `stack.runtime_packages` | | `--build` | Also add packages to `stack.build_packages` | `min add` is the in-sandbox counterpart of [`mip add`](https://minimal.dev/docs/reference/cli-mip#add); note the flag surfaces differ (`mip add` requires one of `--runtime`, `--build`, or `--task `). `min package add` is the canonical noun-verb spelling; `min add` is an accepted alias. ### `search ` Searches for and lists packages related to the search term. `min package search` is the canonical noun-verb spelling; `min search` is an accepted alias. ### `check [] [FILTER_NAMES...]` Validates minimal configuration including packages & stacks. | Flag | Description | |------|-------------| | `--fix` | Attempt to fix any issues | | `--packages` | Check packages defined in the codebase | | `--stacks` | Check stacks defined in the codebase | If no type flags are specified, all types are checked by default. If filter names are specified, any package or stack matching a specified name is checked. `min check` is the in-sandbox equivalent of the [`mip check`](https://minimal.dev/docs/reference/cli-mip#check) command, with a reduced flag set (no `--skip-checkers`, and no short flag aliases). ### `run [...]` Runs the specified task in a new Minimal sandbox. Interactive tasks are not supported. `min run` is the in-sandbox equivalent of the [`mip run`](https://minimal.dev/docs/reference/cli-mip#run) command. `min task run` is the canonical noun-verb spelling; the bare `min run` is a legacy alias. ### `package build ` Builds the named packages in a clean room, making them available in the local cache. ### `package patched-build ` Builds the named package in a clean room using potentially-stale dependencies (the already-cached builds of its dependencies, without rebuilding them first), and commits the result to the local cache. Useful for quickly iterating on a single package's build. ### `materialize [--arch ] -o ` Materializes an output specified in an [`[outputs.]`](https://minimal.dev/docs/reference/minimal-dot-toml#outputs) section of `minimal.toml`. | Flag | Short | Description | |------|-------|-------------| | `--output ` | `-o` | **(required)** The output file to write | | `--arch ` | | Override the architecture used when building the output (e.g. `amd64`, `arm64`); takes precedence over the `arch` field in `minimal.toml` and the host default | --- # Linux host setup > Preparing a Linux host to run minimald: the unprivileged user namespace the session sandbox needs, and the sysctls and AppArmor profile that grant it on Ubuntu 24.04+. Source: https://minimal.dev/docs/reference/linux-host-setup Last updated: 2026-10-10 `minimald` and `mip` run every session and task inside their own sandbox composed of an unprivileged user namespace and other Linux namespaces, similar to how containers are sandboxed on Kubernetes. Root/sudo access is not needed to create these sandboxes; however, some Linux distributions require enabling unprivileged user namespace creation. Most distributions allow this by default. **Ubuntu 24.04 and later require the small configuration change described below.** ## The symptom Ubuntu 24.04 ships `kernel.apparmor_restrict_unprivileged_userns=1`, which stops an unconfined program from creating a user namespace. The sandbox child dies writing its uid map before it runs anything, so *no* session can start: ``` DIAG hakoniwa container/process exited non-zero code=125 exit_code=None reason=write("/proc/self/uid_map", ..) => Operation not permitted (os error 1) ``` On such a host `min session activate` fails before any session exists. The daemon checks the namespace on every create and refuses with the cause and its fix, which `min` prints as its error. `mip` prints the same cause and the fix for its own binary when a task starts. Make sure that the host is the cause with stock tools, no Minimal involved: ```console $ cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 1 $ unshare --user --map-root-user id unshare: write failed /proc/self/uid_map: Operation not permitted ``` ## The fix, by cause The refusal names one of two causes. Each has its own fix. Minimal does not suggest `kernel.apparmor_restrict_unprivileged_userns=0`: that sysctl turns the protection off for every program on the host. ### Ubuntu restricts unprivileged user namespaces This is the AppArmor restriction above. An AppArmor profile that grants `userns` to one binary lifts it for that binary alone, and the profile attaches by binary path. For `minimald`, installed with the `curl … | sh` installer, finish the install. The step loads the profile for the installed daemon: ```console $ min finalize-install --show $ min finalize-install ``` The profile takes effect when the daemon next starts. Run `min stop`, then your command again. To remove the profile run: ```console $ min finalize-install --undo ``` For a `minimald` built from source, run the loader from the checkout with the binary's path: ```console $ sudo scripts/install-apparmor-profile.sh --path "$PWD/target/debug/minimald" ``` For `mip`, which builds every task in the same kind of sandbox, attach the profile to the `mip` binary with `--path`. `min finalize-install` covers the daemon alone. The installer places the loader under the Minimal data directory: ```console $ sudo bash ~/.local/share/minimal/apparmor/install-apparmor-profile.sh --path "$(command -v mip)" ``` From a checkout, the same loader is `scripts/install-apparmor-profile.sh`, and one run can take several `--path` flags: ```console $ sudo scripts/install-apparmor-profile.sh --path "$PWD/target/debug/minimald" --path "$PWD/target/debug/mip" ``` To remove a profile the loader installed: ```console $ sudo scripts/install-apparmor-profile.sh --uninstall # from a checkout ``` ### User namespaces switched off Separately from the AppArmor restriction, no program can create a user namespace on a kernel built without `CONFIG_USER_NS`, or on a host with `user.max_user_namespaces` set to `0`. No profile lifts this. The daemon reads the sysctl on every create, so the first command below clears the refusal at once. The second keeps it across reboots: ```console $ sudo sysctl -w user.max_user_namespaces=15000 $ sudo sh -c "echo 'user.max_user_namespaces=15000' > /etc/sysctl.d/60-minimal-userns.conf" ``` On a kernel built without `CONFIG_USER_NS`, use a kernel with user namespaces enabled. Debian-derived kernels that carry `kernel.unprivileged_userns_clone` need it set to `1` too. The daemon does not read that key, so it does not refuse the create. The session then fails at attach with the `uid_map` error above. --- # Packages API > Reference for the read-only /api/pkgs JSON endpoints, the /pkgs markdown mirrors, the /llms.txt index, and the /pkgs/advisories.xml feed. Source: https://minimal.dev/docs/reference/api The `/api/pkgs/*` namespace is a read-only agent surface over the minimal.dev package catalog. It exists for AI agents, integrations, and `curl`-driven exploration — the webapp itself renders the same data server-side, and both paths flow through one compose pipeline so they cannot drift. Every endpoint is `GET`. The floats-latest half is anonymous and identity-invariant — no tokens, no cookies, the same body for every caller — while the seven sha-pinned endpoints require a signed-in session and answer `401` without one. This page is the human-facing companion to the machine-readable [OpenAPI 3.1 document](#the-openapi-document) at `/api/pkgs/openapi.json` and the [`/llms.txt`](#llmstxt-agent-index) agent index; the endpoint inventory below is drift-guarded against that OpenAPI document so the two can't diverge. The same catalog is also exposed as typed tools by the hosted [MCP server](https://minimal.dev/docs/reference/mcp) at `/api/mcp` — agents that speak the Model Context Protocol can attach it by URL instead of calling these endpoints directly. ## Conventions **Base URL.** All paths are relative to `https://minimal.dev`. **Authentication.** A browser session cookie, and only for the seven sha-pinned endpoints under `/api/pkgs/bundle/{sha}/…`. There are no API tokens yet, so a non-browser client authenticates by signing in at `/auth/login` and replaying the session cookie. Everything else is anonymous — see [Auth and caching contract](#auth-and-caching-contract). **`schemaVersion`.** Every JSON payload carries a `schemaVersion` field. Compare it against the major version of the OpenAPI document before trusting a cached body; a bump signals a breaking change to the payload shape. **Cache classes.** Responses fall into two families: - **Sha-pinned, authenticated** — URLs under `/api/pkgs/bundle/{sha}/…` are content-addressed by commit sha, so the body for a given URL never changes, but they are [auth-gated](#json-endpoints) and therefore uncacheable: `Cache-Control: private, no-store`. Each still carries a weak `ETag` that identifies the body, so a client keeping its own copy can tell that copy is still current; no endpoint answers a conditional `GET`. - **Floats-latest** — URLs without a sha resolve the latest `gominimal/pkgs:main` commit at request time (`Cache-Control: public, max-age=60, s-maxage=3600, stale-while-revalidate=86400`). **Path parameters.** | Parameter | Grammar | On mismatch | |-----------|---------|-------------| | `{name}` | Package name: a leading alphanumeric, then alphanumerics plus `.`, `_`, `-` (case-insensitive). | `404` | | `{sha}`, `{base}` | Canonical 40-hex lowercase commit sha on `gominimal/pkgs:main`. | `404` unknown/malformed | Short sha prefixes (7–39 hex chars) are accepted on the sha-pinned endpoints: a unique prefix `301`-redirects to the canonical 40-hex URL (query params preserved), an ambiguous prefix returns `400 {"error":"ambiguous sha"}`, and an unknown or malformed sha returns `404`. The redirect is cached `public, max-age=3600` for prefixes ≥12 hex chars and `private, no-store` for shorter ones. ## JSON endpoints Fourteen endpoints make up the surface. The thirteen below are described by the OpenAPI document; the fourteenth is the document itself ([`/api/pkgs/openapi.json`](#the-openapi-document)). **The seven sha-pinned endpoints require a signed-in session.** Everything under `/api/pkgs/bundle/{sha}/…` answers `401` with a `{"error":"auth_required","message":…}` body unless the request carries a session cookie; sign in at `/auth/login` to get one. No `WWW-Authenticate` challenge is sent — the credential is a cookie, not an HTTP auth scheme. The gate sits after sha resolution, so short shas still canonicalize exactly as described above (`301`/`400`/`404` are unchanged for every caller) and only the payload needs the session. Both the `401` and the authenticated `200` are `private, no-store`. The floats-latest endpoints are unaffected, and the [MCP server](https://minimal.dev/docs/reference/mcp) cannot present a session yet, so its sha-addressed calls return an error result. | Endpoint | Purpose | Cache class | |----------|---------|-------------| | `GET /api/pkgs/{name}.json` | Full per-package record at the latest commit | Floats-latest | | `GET /api/pkgs/{name}/sbom.json` | SBOM at the latest commit | Floats-latest | | `GET /api/pkgs/{name}/badge.svg` | Score/advisory SVG badge for READMEs | Floats-latest | | `GET /api/pkgs/{name}/attestation.json` | Build attestations per architecture at the latest commit | Floats-latest | | `GET /api/pkgs/recent-commits.json` | Last N commits on `pkgs:main` | Floats-latest | | `GET /api/pkgs/bundle/latest.json` | Redirect to the current listing bundle (target needs a session) | 302 alias | | `GET /api/pkgs/bundle/{sha}.json` | Listing-tier bundle at a sha | Sha-pinned, authenticated | | `GET /api/pkgs/bundle/{sha}/full.json` | Full-tier bundle at a sha | Sha-pinned, authenticated | | `GET /api/pkgs/bundle/{sha}/diff/{base}.json` | Catalog diff between two commits | Sha-pinned, authenticated | | `GET /api/pkgs/bundle/{sha}/{name}/deps.json` | Direct-dependency rollup at a sha | Sha-pinned, authenticated | | `GET /api/pkgs/bundle/{sha}/{name}/versions.json` | Version-history timeline at a sha | Sha-pinned, authenticated | | `GET /api/pkgs/bundle/{sha}/{name}/sbom.json` | Reproducible sha-pinned SBOM | Sha-pinned, authenticated | | `GET /api/pkgs/bundle/{sha}/{name}/attestation.json` | Build attestations for what that commit shipped | Sha-pinned, authenticated | ### `GET /api/pkgs/{name}.json` Full composed record for one package — description, dependencies, archive, full advisory payloads, and OpenSSF scorecard — wrapped in the `{schemaVersion, commitSha, pushedAt, package}` envelope. Floats with the latest `gominimal/pkgs:main` commit (the `commitSha` is resolved at request time), so it is byte-identical to the `