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"

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}!\""

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'

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, an empty file or directory is created.

Mapped paths must be absolute or start with ~/, in which case the tilde is expanded to the user’s home directory.

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

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>

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}"