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 onlyMINIMAL_SESSION_IDand 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
denyfails the run, naming the rule’s file. - a name matching
ignoreis 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
allowis 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 runcomposes an ephemeral session with the default mode,host_ip.min run <task>inside a session andmin 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