Arguments
You have already used a handful of these in
Quickstart — --scripts, --fast,
--skip, --dry-run. This page is the full list.
denver’s CLI is subcommand-based: denver run <env> ... is the normal entry
point, and every flag below except --version/--license belongs to it.
(--version and --license are top-level, before the subcommand, e.g.
denver --version. The other two subcommands are denver clean <env> — see
Remove an environment’s state below — and
denver complete, see Shell completion.)
denver run --help is always authoritative and lists every run flag;
the notes below are for the ones whose behavior isn’t obvious from a
one-line description.
Control the stages: Choosing what runs
--until <stage>truncates the pipeline: every stage up to and including<stage>runs, everything after it is dropped — there’s no “run only this one stage” flag, since a stage practically always needs the ones before it. The command (if any) still runs afterwards, in whatever partial environment those stages built.--skip <stage>removes individual stages from whatever--untilleft; repeatable. Skipping a wrapper stage (--skip docker) is how you run the stack directly on the host instead of relocating into a container.--scripts <name>runs every (filtered) stage’s ownscripts: <name>:list, then exits without running the rest of the pipeline — see “scripts:” in Configuration for an example (one-time host setup, e.g. installing Docker itself, is the usual use).<name>is open-ended, not a fixed set of flags: a project can declarescripts: migrate:and rundenver run <env> --scripts migratewithout denver itself changing. Repeatable — each name’s entries run in the order given. With no<name>(on any occurrence), it lists the names this env defines instead of running anything. If<name>isn’t declared by any (filtered) stage, denver logsno '<name>' scripts to run for env '<env>'at info level rather than exiting silently.--setup/--loginare shorthand for--scripts setup/--scripts login— two conventional names most envs use. They share--scripts’ ordering and repeat rules, so--login --setuprunsloginthensetup, and either can still be combined with a plain--scripts <name>.--cleanis the third such shorthand, plus one step: it runs the env’s ownscripts: clean:entries and then removes the env’s state directory — see Remove an environment’s state below. Use plain--scripts cleanfor only the scripts, with the state directory kept.
Control the config: Change values for one run
-c/--config KEY.PATH=VALUEoverrides a single value in the mergeddenver.yml(e.g.-c uv.python=3.13); any missing parent section is created as an empty mapping.KEY.PATH+=VALUEappends to an existing list/string/number instead of replacing it (behaves like=if the path doesn’t exist yet).VALUEis parsed as JSON when that succeeds, so"true"/"3"/'["a", "b"]'become their real type; anything that isn’t valid JSON on its own (a bare word, an unquoted version string like3.12.3) is kept as a plain string. Repeatable; later-cs win when they target the same path.-cf/--config-file FILEoverlays a whole config file (denver.ymlordenver.toml, dispatched the same way<env>itself is) on top of the env’s own config, using the same merge rules asimport:. Repeatable, applied in the order given;-coverrides are applied last, on top of every-cffile.
Both follow the same merge rules as import:, explained in
Configuration.
An env’s own flags: denver-custom-args:
-c can set anything, which is exactly why it is a poor fit for the one
or two knobs an env actually wants to offer (“which board?”, “debug or
release?”): its users have to know the dotted path, nothing shows up in
--help, and a typo just becomes a new config key instead of an error.
denver-custom-args:, in the env’s own denver.yml, declares those knobs
as real flags instead. Each entry is one parser.add_argument() call:
flags: names the flag (a string, or a list to give it aliases), and
every other key is forwarded verbatim as a keyword argument — so an env
gets argparse’s whole vocabulary (help:, default:, action:, nargs:,
choices:, required:, metavar:, dest:, …) without denver
re-inventing, or restricting, any of it:
denver-custom-args:
- flags: ["--board", "-b"]
default: nrf52840dk
help: which board to build for
- flags: ["--release"]
action: store_true
help: build with optimisations
denver run my-project --board nrf5340dk --release -- west build
What the user passed is exported as DENVER_ARG_<DEST>, where <DEST>
is argparse’s own destination name uppercased (--board → DENVER_ARG_BOARD,
--build-type → DENVER_ARG_BUILD_TYPE, or whatever an explicit dest:
says). That is one variable in the environment denver is building, so it
reaches everything through the same mechanisms as any other variable —
${...} interpolation in the same denver.yml, hooks, scripts:, and the
final command:
zephyr-build:
provider: custom
cmd: west build -b ${DENVER_ARG_BOARD}
The value is always a string, since an environment variable is:
action: store_true/store_false→"1"/"0".a multi-value flag (
nargs:,action: append) → its items, space-joined.a flag with no
default:that wasn’t given → not exported at all, rather than exported empty — so${DENVER_ARG_BOARD:-nrf52840dk}still falls back the way it reads.
For the same reason, type: is rejected: argparse’s type= is a callable,
which TOML cannot express, and every value ends up a string anyway. Use
choices: (argparse validates it, and lists it in --help) or an
action:.
A few more properties worth knowing:
The flags are ordinary argparse flags, so
denver run <env> --helplists them alongside denver’s own, and a mistyped one is argparse’s usualusage:/error:on stderr — never a silently ignored token.Write them after
<env>(denver run my-project --board x): the flags an env declares live in the very file<env>names, so denver has to resolve<env>before it can know them.A flag that would collide with one of denver’s own (
--force, or adest:ofci) is a hard error, not a silent override.denver-custom-args:is a list, so it follows the normal list-merge rule: an env inherits every flag itsimport:chain declares and adds its own (see “Layering” in Configuration).They survive a wrapper relocation: the tokens are re-passed to the denver re-invoked inside the container, which would otherwise only see each flag’s
default:(see “Wrapper / relocation” in Configuration).
Control the speed: Trading speed against freshness
--fastskips every provider’s (re-)build step and only activates what a previous full run already built (each provider’s own page under Providers documents exactly what that means for it). Run once without--fastfirst — a provider dies with a clear message if what it needs isn’t there yet.--forceforces a provider to redo expensive work it would otherwise skip because nothing looked like it changed (again, see each provider’s own page for specifics). Like--cibelow, this is only ever set by the flag itself, never inherited from a same-named real environment variable.--ciswaps in narrower/faster args a provider judges appropriate for a CI runner (currently just zephyr’swest update, adding a shallow-clone strategy on top of whateverupdate-args:already configures).
Inspect an environment
--show-configprints the fully merged configuration —import:chain resolved, overrides applied, every default filled in — then exits, showing only the keys that actually carry a value (whatever the env itself configured, or a provider default computed from it); a key nothing ever set is left out rather than shown as unset. The fastest way to understand an env you didn’t write, and it needs no toolchain, no network and no Docker.--show-config-fullis--show-configplus every key left unset, each shown explicitly (key: null— YAML, the default; a commented-out# key = nullline with--format toml, since TOML has nonull) — the complete key reference for a stage’s own section, not just what this particular env happens to set.--format {yml,toml}picks the output format for either of the above, independent of whether the env itself is adenver.ymlor adenver.toml—yml(the default) always works,tomlneeds no extra install either (denver’s own hand-written renderer, nottomllib— writing TOML doesn’t need what only reading it does).
--format toml output is syntax-highlighted (table headers, keys, strings,
numbers/booleans and # key = null comments each their own color) whenever
stdout looks like a real terminal — auto-detected, off the moment output is
piped or redirected, so a golden-file snapshot, | grep, or a redirect into
a file all still get plain text. Two environment variables override the
auto-detection, the same convention most CLI tools use:
NO_COLOR (any non-empty value) always forces it
off, and FORCE_COLOR forces it on even when piped (e.g. into less -R).
(--format yml’s output is always plain text — PyYAML’s own renderer, not
denver’s, doesn’t have this.)
--dry-runshows what each stage would do instead of doing it: no command is executed for its effect, no file is written, and the final command is printed rather than launched. Useful for answering “what does this env actually run?” without waiting for (or committing to) a real build. Every line is tagged[dry-run <marker>], each marker in its own color (a legend stating the same key prints once, up front):marker
color
meaning
+green
a command that would run (skipped)
?cyan
a read-only query, really run — its output is what decides the commands below it
~yellow
a file or directory write that would happen (skipped)
.blue
a script sourced into the environment, really done
!red
a note about what this preview cannot show
The two “really” rows are the deliberate limit. A dry run has to answer questions like is the image already cached?, which conan home?, what does
west listsay? to render the commands that follow — so those read-only queries execute, and scripts are still sourced (that is how denver computes the environment a command is rendered against). Two further consequences worth knowing: the preview reflects the state your machine is in now, so an already-built env legitimately shows fewer commands than a clean one (that is what a real run would do too); and a wrapper stage (docker) can’t be previewed past its own boundary, since entering the container is itself one of the commands not being run — denver says so and points you at--skip dockerto preview those stages on the host instead.
Hand the built env to something else
--export-env <file>writes what denver itself changed in the environment asexport KEY=VALUElines to<file>, right before launching the final command — for bash or zsh (not fish) to source. Useful when something other than that final command needs the same env: a container’s interactive terminals, say, started fresh by an IDE and unaware denver ever ran. Carried along automatically into a docker wrapper’s own reinvocation, so it still reaches the process that actually builds the env rather than the outer one that only relocates into the container.Only variables that differ from what this process already had are written — a plain dump of the whole (inherited-plus-built) environment would re-assert a hundred unrelated variables (
SSH_AUTH_SOCK, say) that the sourcing shell already has its own, possibly different, value for. A variable denver only prepended or appended to (PATHvia a provider’s own setup, most commonly) is written asprefix"$VAR"or"$VAR"suffixrather than the whole resolved value, so it composes with whatever that variable already is in the sourcing shell instead of overwriting it outright. A value set with-e/--envis always written, even if it happens to match what the process already had.
Remove an environment’s state
denver run <env> --clean # run 'scripts: clean:', then remove the state
denver run <env> --clean --dry-run # only say what would go
denver run <env> --scripts clean # only the scripts, state kept
After running the env’s own scripts: clean: entries, --clean deletes the
whole state directory denver keeps for that env — its venvs, installed tool
trees, downloads, logs and the fingerprints that decide what a run can skip
(see
Where an environment’s state lives).
There is nothing to preserve in it: the next denver run rebuilds all of it
from the denver.yml, which is the whole point of an environment being
code. It removes the directory whether or not the env declares any clean
scripts at all.
Four things worth knowing:
The scripts run first, deliberately: they run against the built environment (a venv’s tools, a container) that is about to go.
Like
--setup/--login, it neither builds nor enters the env. Only the env’s owncleanscripts run — no stage’s setup work, and no final command afterwards.It only removes what denver itself wrote. Anything the env’s own config points somewhere else — a
CONAN_HOME: "${DENVER_ENV_DIR}/.conan2", say — is a location the project chose for a tool’s own cache, not denver’s state, and is left alone.The
.denverdirectory goes too, once nothing but the.gitignoredenver wrote is left inside it, so the env comes back to exactly the files its author checked in. A.denverstill holding another config variant’s state (denver.debug.yml’s) is kept, along with that state — cleaning one variant never touches its neighbours. denver’s own shared-root fallback (~/.denver) is never removed either way; only this env’s own directory inside it is.
--clean is the bigger hammer next to --force: --force rebuilds what a
run would otherwise skip, --clean removes it so there is nothing to skip
in the first place.
denver clean <env>
denver clean <env> # remove every directory denver keeps for it
denver clean <env> --dry-run # only say what would go
denver clean <env> -y # skip the confirmation prompts
denver clean <env> --all # also remove the shared DENVER_CACHE_DIR
The subcommand is the wider one, and runs no scripts. It removes state in
every place it can live — an explicit DENVER_ENV_WORKDIR (if currently
set), the workdir inside the env (<env>/.denver/<config stem>), and
denver’s own shared-root fallback (.envs) — so an env built under more
than one of these across different runs is left with nothing anywhere. And
it does the same for every env <env> imports, since a base env is built as
part of building whatever imports it.
It needs no working config: an env whose denver.yml will not parse still
has its own directories removed, with a warning that the envs it imports
were left alone.
Before removing anything, it shows the directory and asks to confirm it,
one directory at a time — declining one keeps it and moves on to the
next rather than stopping the whole clean. -y/--yes skips every one
of those prompts, for scripts and CI. With no tty to ask on and no -y, it
refuses rather than guessing either way. --dry-run never asks (there is
nothing to confirm: it removes nothing either way).
--all additionally removes denver’s shared, content-addressed cache
root (DENVER_CACHE_DIR, default ~/.cache/denver — see
Where an environment’s state lives).
Unlike everything else clean touches, that directory isn’t this env’s own:
it’s shared across every env and checkout on the machine, so it’s only ever
removed when asked for explicitly.
Output and version
By default denver prints only the coarse progress trail — one line per
stage (-- [i/n] stage 'id' (provider) or its “skipped by …” reason) —
plus whatever each stage’s own build tool prints on its own. denver’s own
finer detail (sub-step banners, performance timings, the + cmd echo of
every command it runs) is off unless asked for with -v/--verbose.
-q/-qqare two quiet levels.-qsilences denver’s own output entirely (the progress trail included), but leaves each stage’s own build tool output showing — that’s usually still what you want to watch.-qqadditionally discards that too, so only the launched command’s own output reaches the terminal.-q/-qqalways win over-v: there is nothing left for-vto add once either is given. Errors are always reported.-v/--verboseturns on denver’s own diagnostic detail, hidden by default: each stage’s finer sub-step banners (e.g. acustomstage’scmd/source, auvstage’sinstall/activate), the per-stage “finished in Ns” line and the closing stage-timing summary (both in blue), the env’s own “started in Ns” duration, and the+ cmdecho ahead of every command denver runs (including the final launched command’s own+ exec: cmdline). Silenced by-q/-qqlike everything else denver prints.--version(top-level:denver --version, before any subcommand) prints the running denver’s version and exits — derived from the checkout’s git tags when denver runs from a checkout (script or editable install), otherwise from the installed package’s metadata. A checkout ahead of its last tag reports as a development build of the release it is heading for (1.1.0-17-gabc1234). Adenver.ymlcan require a minimum withdenver-version: ">=1.1.0", and is rejected up front by a denver older than that (see Configuration).
Each stage’s runtime is also appended to
$DENVER_ENV_WORKDIR/performance.jsonl as JSON Lines of Chrome Trace
Event Format events — concatenate them into a {"traceEvents": [...]}
document to load at chrome://tracing or https://ui.perfetto.dev.
[!NOTE] Next: Shell completion — tab-complete subcommands, env paths and flags with
denver complete. Then Environment variables — the two variables denver itself reads, and where an environment’s state lives on disk.