Annotations
This page defines each annotation, its purpose, constraints, and common mistakes.
@in input contracts
@in src/**/* Cargo.toml Cargo.lock
- Type: list of glob patterns.
- Purpose: defines inputs that influence graph fingerprints.
- Effect: changing matched files can trigger cache miss.
- Scope: graph tasks.
Use narrow patterns. Prefer src/**/* Cargo.toml over **/*.
@out output contracts
@out dist/app
@out reports/test.xml
- Type: list of paths.
- Purpose: declares expected materialized outputs.
- Effect: implies graph mode when
@modeis omitted. - Validation: graph mode requires at least one output.
If a task should be cached, declare explicit outputs.
If an @in pattern contains ** and matches nothing, Broski does not fail validation. It falls back to
treating the literal pattern string as a (likely-missing) input path, which then shows up in --explain output
as a plain, unresolved manifest entry. A silently-empty glob is a common source of "why didn't this rebuild"
confusion — if a task never reruns when you expect it to, double check the glob actually matches files on disk.
@stage_ro read-only staged paths
@stage_ro node_modules
@stage_ro .venv
- Type: list of workspace-relative paths.
- Purpose: makes a path visible inside the task's sandbox/stage without treating it as a fingerprinted input.
- Effect: paths are symlinked (or copied, as a fallback) into the stage read-only. Use this for large,
slow-to-hash, or already-reproducible directories — dependency caches, virtualenvs, vendored toolchains —
that a task needs to see but that shouldn't drive cache invalidation the way
@indoes. - Constraints:
- Every declared path must already exist on disk; validation fails otherwise.
- A
@stage_ropath cannot overlap a declared@outpath — Broski rejects the broskifile with@stage_ro path '<path>' overlaps output '<output>'. - Not allowed on
@mode interactivetasks (interactive tasks run directly in the workspace, so there is no stage to link into).
build:
@in src/**/* Cargo.toml
@stage_ro node_modules
@out dist/app
npm run build
Prefer @stage_ro over @in for anything you don't want to pay a content-hash for on every run.
@env runtime environment
@env RUST_LOG=info
@env HOME
Forms:
KEY=value: sets fixed value for task.KEY: inherit from runtime environment.
@env participates in fingerprints for graph tasks.
@secret_env secret environment keys
@secret_env GITHUB_TOKEN
@secret_env AWS_SECRET_ACCESS_KEY
- Purpose: mark env keys as sensitive and inherit them from the runtime environment —
@secret_env KEYis self-sufficient, you do not also need@env KEY. Values resolve from.env(@load) first, then the host environment, and the task fails fast if the key is missing everywhere. - Effect: matched values are redacted from captured stdout/stderr via an Aho-Corasick scan and replaced with
[REDACTED]. Only values 6 characters or longer are redacted — shorter secrets (rare, but e.g. a 4-digit PIN) are not pattern-matched and can leak into logs. Treat this as a floor, not a guarantee. - Fingerprinting: secret values never appear in the manifest in plaintext. Their contribution to the fingerprint
is a keyed BLAKE3 digest, keyed by a workspace-local salt generated once and persisted at
.broski/config/salt(file mode0600). This means the fingerprint still changes when a secret changes, but the manifest itself never reveals the value — even to someone with.broski/cacheaccess. - Explain reason stays generic:
cache miss: secret env changed(no key name, no value).
Do not put literal secrets in broskifile.
@dir working directory override
@dir services/api
- Type: single workspace-relative path.
- Purpose: run command from specific directory.
- Constraint: exactly one value.
Use this instead of prefixing every command with cd ... &&.
@mode execution mode
@mode graph
@mode interactive
Modes:
graph: deterministic, staged, cache-aware.interactive: live workspace execution, no cache, TTY inherited.
Inference when omitted:
@outpresent => graph mode.- no
@out=> interactive mode.
@isolation sandbox policy
@isolation strict
@isolation best_effort
@isolation off
strict: run inside abwrapsandbox — network unshared, filesystem restricted to a minimal ro-bind set (/usr /bin /lib, an empty writable/etcplus a ro-bound/etc/resolv.conf,$HOMEtmpfs-masked) plus a read-write bind of the task's own stage directory. Linux-only (needsbwraponPATH); on any other platform this errors out rather than silently degrading.best_effort: run the command directly, no sandbox — but still clear the process environment down to a minimal safe set before layering task@env/@secret_envon top.off: run the command directly with the full parent environment inherited unchanged — no env clearing.
When @isolation is omitted, the default is platform-dependent: Linux defaults to strict, every other OS
defaults to best_effort.
strict and best_effort share the same environment policy: both strip the process environment down to just
PATH, HOME, USER, TMPDIR, SHELL, TERM before adding the task's declared @env/@secret_env values. off is
the only mode that leaks the full ambient environment into the task.
--force-isolation CLI flag forces strict for every graph task in the run, regardless of each task's own
@isolation setting. It requires Linux + bwrap on PATH (the run aborts up front otherwise), and it is
rejected outright for interactive tasks — --force-isolation is not supported for interactive task '<name>'.
@requires tool preflight checks
@requires cargo docker python3
- Purpose: fail fast when required binaries are missing.
- Check timing: one upfront pass over the whole resolved graph, before any task in the run starts — not a
per-task check at that task's own start time. Every unique tool name across every task in the target's
dependency closure is resolved via
PATHlookup before execution begins; a single missing tool aborts the entire run before anything executes. - Error style: task-scoped, explicit missing binary.
Use this on setup/dev/deploy tasks to reduce trial-and-error failures.
@private hidden task
@private
- Purpose: hide helper tasks from
broski list. - Use for internal wiring tasks not meant for direct invocation.
@confirm safety prompt
@confirm "Drop local database? Type y to continue"
- Purpose: prevent accidental destructive execution.
- Behavior: prompts before running task.
- Constraint: prompt text must be non-empty.
In non-interactive contexts, this should fail safe.
@load env file directive
@load is a top-level directive (not task-scoped).
@load .env
- Purpose: load key-value pairs into task environment resolution.
- Path: workspace-relative.
- Typical usage: local development defaults.
Top-level variables
Before any task header, you can declare workspace-level variables that are available to every task via
{{ NAME }} interpolation:
version = "0.5"
REGISTRY = "ghcr.io/acme"
GIT_SHA = $(git rev-parse --short HEAD)
build:
@out dist/{{ GIT_SHA }}.tar
docker tag app {{ REGISTRY }}/app:{{ GIT_SHA }}
KEY = "value"— a static string. Supports{{ OTHER_VAR }}interpolation of previously-declared variables (cyclic references are rejected at parse time).KEY = $(command)— a dynamic variable evaluated by runningcommandthrough/bin/sh -lc. The command's environment is stripped toPATH, HOME, USER, SHELL, TMPDIR, TERMonly, it has a 5-second timeout, and its combined stdout/stderr is capped at 1 MiB. A non-zero exit surfaces the command's stderr in the parse error.- Dynamic variables are evaluated lazily — only if the task actually being run references them (an unused
$(...)variable never executes). - All top-level variable declarations must appear before the first task header in the file.
- Both static and dynamic variables participate in the task's fingerprint (as
var:<name>manifest entries), so changing a variable's value can trigger a cache miss for any task that references it.
$(date), $(uuidgen), or anything else that changes on every invocation will bust the cache on every run of
any task that references it. Reserve dynamic variables for values that are stable across a given commit/build
(git SHA, package version, resolved tool path), not wall-clock time.
@shell is not a DSL directive
TaskSpec has a shell_override field internally, but the DSL parser never populates it — there is no @shell
annotation. Every task written in the .broskifile DSL runs its body via the default shell (sh -lc <script>
on Unix). The only way to override the shell program is the legacy TOML broskifile format, which is otherwise
deprecated in favor of the DSL. If you're writing DSL broskifiles (the documented, current path), assume the
shell is fixed and don't look for a per-task override.
Full example
version = "0.5"
@load .env
build [target] [mode="release"]:
@in src/**/* Cargo.toml Cargo.lock
@out dist/{{ target }}
@env RUST_LOG=info
@secret_env GITHUB_TOKEN
@dir services/api
@mode graph
@isolation best_effort
@requires cargo
cargo build --bin {{ target }} --{{ mode }}
clean-db:
@private
@confirm "Reset local database?"
@mode interactive
@requires rm
rm -rf backend/data/app.db
Mistakes to avoid
- Using
@mode graphwithout@out. - Using broad
@in **/*for every task. - Expecting
@secret_envvalues in explain output. - Using
@dirwith multiple values. - Forgetting
@requiresfor tool-heavy tasks. - Declaring
@stage_rofor a path that doesn't exist yet, or that overlaps a declared@out— both fail validation. - Assuming a non-matching
@inglob is an error — it silently becomes a literal (likely-missing) input path instead. - Assuming
@secret_env KEYstill needs a companion@env KEYto be inherited — it doesn't; it's redundant.