Tasks

Tasks are defined in a minimal.toml file, and describe a runtime environment + command invocations to be executed using mip run <taskname>

Tasks schema

Tasks are defined in a [tasks.<task-name>] block in your minimal file.

packages - Packages to be installed in the runtime environment

Optional

packages lists additional packages which will be installed in the tasks’ runtime environment. Packages listed here are in addition to any installed due to the stack.

[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:

[tasks.my_task]
exec = "pnpm build"

or as a program and its list of arguments:

[tasks.my_task]
exec = ["pnpm", "build"]

When exec names a command without an absolute path (or a ./ prefix), the command is resolved to /bin/<command> 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.

[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:

[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 are set on the task, arguments can be substituted into the invocation using Nickel’s string interpolation syntax:

[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 %{"%"}{:

# 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.

[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.

[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

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.

[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 }:

[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):

$ 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 <task>, from inside a session, has no invoking client to read from.
  • min session run <session> <task> 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

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.

# user_policy.toml
[vars]
allow = ["ZZ_TASK_*", "RUST_*"]
deny  = ["AWS_*"]

A denied name:

$ 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:

$ 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).

[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.

[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.

[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

Optional

args configures named arguments and their datatype, which can be substituted into the command being executed by the task.

[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:

$> mip run greeter
error: the following required arguments were not provided:
  --name <name>
  --greeting <greeting>

Usage: mip run greeter --name <name> --greeting <greeting>

min task run takes the same flags after the task name:

$> 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):

[tasks.greeter]
args.name = { type = "string", help = "who to greet", default = "world" }
exec = "echo Hello %{name}"

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 sets:

  • min task run composes an ephemeral session with the default mode, host_ip.
  • min run <task> inside a session and min session run <session> <task> 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 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.

Last updated