# 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 <taskname>`](https://minimal.dev/docs/reference/cli-mip#run)

## 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](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/<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.

```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 <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 {#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 <name>
  --greeting <greeting>

Usage: mip run greeter --name <name> --greeting <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 <task>` inside a session and
  [`min session run <session> <task>`](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.
