CLI reference

Every action mmz accepts, every exit code it returns, and the JSON it can be asked for — generated from the binary's own help text.

ActionWhat it does
<command>The whole point: run a command, or skip it when nothing it depends on has moved.
--initScaffold .mmz/config.yaml and .mmz/.gitignore in the current directory.
--statusShow every rule’s freshness and the age of its record, as a table.
--status=jsonThe same report as JSON, with every resolved input, its hash, and what the cached record saw.
--status=json-schemaPrint the JSON Schema for --status=json, so a consumer can validate what it parses.
--is-freshAssert freshness without running anything: exit 0 when fresh, exit 1 when not.
--pruneDelete cache records whose rule no longer exists in the manifest.
--importCopy cache records in from another checkout of the same repo, such as the worktree being landed.
--schemaPrint the JSON Schema for .mmz/config.yaml.
--schema=fragmentPrint the JSON Schema for a file named in a manifest’s imports: list.
--dump-configPrint the manifest mmz actually assembled — its effective policy plus every scope, probe and command — with the source file behind each one.
--dump-config=jsonThe same dump as JSON: a policy object plus a source on every scope, probe and command.
--versionPrint the version, and how many languages this build can parse.
--helpPrint the usage text, including the exit-code table.
--Force the rest of the line to be the wrapped command, for a command whose own name begins with a dash.

Exit codes

mmz returns the wrapped command’s own exit code when it runs one, so these are the codes that mean something happened before, instead of, or around that.

CodeMeaning
0Fresh and skipped, or the wrapped command ran and succeeded. Also the exit for a satisfied --is-fresh gate, and for the read-only actions (--status, --schema, --dump-config, --version, --help, --prune, --import).
1--is-fresh: the targeted rule — or some rule in the gated set — is not fresh. Reserved for the gate verdict, never for an error. Every offending rule is named on stderr with the reason it would re-run.
2Usage error: an empty invocation, an unknown option, --init over an existing manifest, an --import directory that is not another checkout, or a MMZ_NOW that is not a Unix epoch. The clock case is the one that is not about argv: a malformed pin is refused rather than ignored, so the misconfiguration surfaces here instead of as a stamp nobody can reproduce.
3Strict refusal: no rule matched, or a matched rule resolved to zero files. Both cases are relaxable per project through strict, and then the invocation runs unmemoized instead of stopping.
4The manifest is missing or invalid. Never relaxable. mmz will not memoize against a manifest it could not read or could not validate — and the unreadable case names the file, because a permission bit on a config is not an internal error. Shape is checked here, at load, even for a probe no rule names — a probe declaring both run: and file: is refused before anything runs, rather than resolved by a precedence rule.
5The command succeeded without producing a declared output; nothing was recorded. The missing path is named. Recording the run anyway would leave a rule that quietly never hits again.
6A probe did not produce a value mmz can trust; nothing was recorded. The probe is always named. A run: line that exits non-zero or cannot be spawned comes with its exit code and stderr; a file: that is missing or unreadable comes with the path; and a json: selector reports bytes that were not one JSON value, a program that would not compile or that raised, or — the case worth the code on its own — a selection that measured nothing. An ast: pattern lands here the same way: one that matched no node, one the grammar could only recover into an error node, bytes that are not UTF-8, or a lang: this build has no grammar for — which is the one failure under this code whose fix is a rebuild, so the message names the cargo feature. None of them reach the hasher, so no digest is ever computed from partial output.
7--is-fresh: the selection holds no rule, so the gate would have asserted nothing. Three ways to reach it: the manifest declares no commands:, a --tag filter no rule carries (a typo, a rename, a rule that quietly lost its tags: entry), or a selected rule that fans over a scope resolving to no files. All three would otherwise exit 0 — a pass over an empty set, which reads exactly like a build that ran. The message names the tags and lists the ones the manifest does declare. Never relaxable, and distinct from 1 on purpose: a hook branching on $? can tell a stale build from a gate pointed at nothing.
8

A file mmz needed could not be read or written; nothing was recorded. The path is always named, rendered the way --status renders it. Three kinds of file reach this code — an input a rule declared, the scaffold mmz --init writes, and the cache a run records into, --prune sweeps or --import fills — and they share one fact: the filesystem refused. That is a condition of the tree rather than a bug in mmz, which is why it is not 70. A manifest that cannot be read is the exception, and lands at 4 with the rest of the manifest failures.

An input the walk resolved and something else removed before the hasher opened it says so in as many words, rather than reporting the errno: that race is what gating a parallel runner sits in front of by construction, and “no such file” alone reads like an accident. Either way no record is written and the wrapped command never runs, so re-running once the tree has settled is safe.

70Internal error.Worth reporting: nothing a manifest can say should produce one.
127The wrapped command could not be spawned. The conventional shell code for it, so a caller inspecting $? reads the same number it would have from sh.

Distinguishing them matters for the two that write no record — 5 and 6 — because they are the cases where a run happened and deliberately left no claim behind.

Rule states

--status and --status=json report one of these per rule:

Freshness verdict. `fresh`: cache hit. `stale`: inputs changed — a file, or a probe whose stdout moved. `never`: no record yet. `failed`: last run failed, so it re-runs. `no-inputs`: the rule resolves to zero files and names no probe. `missing-output`: a declared output is gone, so the record is void whatever the inputs hash — it outranks `stale`, since an input change is the assumption a reader already holds. `expired`: nothing moved, but the pass is at least the rule's `ttl` old, so it re-runs; every other non-fresh state outranks it.

Actions in detail

mmz <command>

The whole point: run a command, or skip it when nothing it depends on has moved.

mmz receives a clean argv vector, not a shell-expanded string, so matching is on whole tokens and there is no nesting blind spot. Other wrappers go outside it (chronic mmz cargo test). There is no runner integration and no --no-memo: a bare command without mmz is simply unmemoized.

On a hit, nothing runs and the exit code is 0. On a miss the command runs with its stdout and stderr streamed through untouched, and a record is written only if it exits 0 — a failing command never becomes a claim that the work is done.

mmz --init

Scaffold .mmz/config.yaml and .mmz/.gitignore in the current directory.

Refuses to overwrite an existing manifest (exit 2). Everything mmz needs lives under the one .mmz/ directory: the tracked config, plus a .gitignore that ignores the cache — so a project gains one entry and its root .gitignore stays untouched.

mmz --status

Show every rule’s freshness and the age of its record, as a table.

Read-only: it resolves inputs and compares digests, and runs no wrapped command. --tag/-t narrows it to rules carrying every listed tag.

A filter that keeps no rule still exits 0 — a report asserts nothing, so an empty one is an answer — but it says which emptiness it is, naming the tags it filtered on and the ones the manifest declares. --is-fresh over the same selection is refused instead (exit 7): that one is an assertion, and an assertion over nothing passes without having checked anything.

mmz --status=json

The same report as JSON, with every resolved input, its hash, and what the cached record saw.

This is the form to reach for when a rule is stale and the question is which input moved: the resolved set and the recorded set are both in there, so a diff is a jq away.

mmz --status=json-schema

Print the JSON Schema for --status=json, so a consumer can validate what it parses.

mmz --is-fresh

Assert freshness without running anything: exit 0 when fresh, exit 1 when not.

The inverse of wrapping. mmz <command> runs a stale command; mmz --is-fresh -- <command> refuses it. With no command it gates every rule at once; with --tag it gates the tagged subset. A non-fresh gate names each offender and why, then prints one hint to re-run the listed commands under mmz — a standalone run is not observed, so it leaves the rule exactly as stale as it found it.

A gate that selected no rule at all is refused with exit 7 rather than passing: a typo’d tag, a renamed one, or a rule that lost its tags: entry would otherwise be indistinguishable from a green build.

mmz --prune

Delete cache records whose rule no longer exists in the manifest.

Renaming or removing a rule orphans its record; nothing else collects them, because mmz never deletes state it cannot prove is unclaimed.

mmz --import

Copy cache records in from another checkout of the same repo, such as the worktree being landed.

Each checkout keeps its own gitignored cache, so work that passed the gate in a sibling git worktree still reads stale in the primary one after a fast-forward. Run from the primary checkout and pointed at the worktree, --import copies the records over. A worktrunk pre-remove hook does it as the worktree goes away. The hook runs inside that worktree, so it steps into the primary first:

[pre-remove]
mmz = "cd {{ primary_worktree_path }} && mmz --import {{ worktree_path }}"

Only records for rules this checkout’s manifest declares are taken, and a record the primary already holds with a later ran_at is kept. mmz still judges every copied record against this tree: the input digest is built from repo-relative paths and content hashes, so a record for other inputs imports and reads stale, never fresh. It prints one line, mmz: imported N records from <dir>, and no rule names. Importing nothing is a success.

A <dir> with no .mmz/ in it, or one that resolves to this checkout however it is spelled, is a usage error and exits 2.

mmz --schema

Print the JSON Schema for .mmz/config.yaml.

The same bytes the manifest reference on this site is generated from, and the same schema the $schema line --init writes points at.

mmz --schema=fragment

Print the JSON Schema for a file named in a manifest’s imports: list.

Narrower than --schema: a fragment may not set cache_dir, gitignore, strict or on_hit, so validating it against the config schema instead would accept documents mmz rejects. Point a fragment’s own # yaml-language-server: $schema=… line here.

mmz --dump-config

Print the manifest mmz actually assembled — its effective policy plus every scope, probe and command — with the source file behind each one.

Leads with the source list in load order, so the import graph is visible before the entries it fed are, then the effective gitignore, cache_dir, strict and on_hit (defaulted ones marked, since they can only come from the root manifest), then each scope, probe and command annotated with its own file. Read-only, and prints the merged model only after validation — it is not a debugging aid for a manifest that fails to merge; that error already names both files.

mmz --dump-config=json

The same dump as JSON: a policy object plus a source on every scope, probe and command.

Aimed at a gate hook: a generator that emits a fragment can assert the fragment it wrote is the one actually in effect, not merely present on disk.

mmz --version

Print the version, and how many languages this build can parse.

The count is there because a version number does not identify a binary on its own: which grammars mmz carries is a compile-time choice, so the release publishes two builds under one version — mmz, parsing Rust alone, and mmz-full, parsing all of them. (1 ast lang) and (28 ast langs) are what tells them apart.

A count rather than the list, because twenty-eight names would bury the version. Which languages is answered where the answer is actionable: a lang: this build lacks fails naming every language it does carry, and the feature flag that would add the one you asked for.

Twenty-eight against twenty-seven grammar crates — typescript and tsx are two names a manifest may write and one crate on disk.

mmz --help

Print the usage text, including the exit-code table.

mmz --

Force the rest of the line to be the wrapped command, for a command whose own name begins with a dash.

Without it, a leading -… token is read as an mmz option and rejected. The separator is consumed, so the rule matched is the command after it.

JSON output

--status=json reports every rule with its resolved inputs and their hashes, plus what the record saw, so a disagreement is diffable rather than guessable:

mmz --status=json | jq '.rules[] | select(.state != "fresh")'

Its shape is itself schema-documented — mmz --status=json-schema prints the JSON Schema, so a consumer can validate what it parses instead of pattern-matching on field names.

This is the form to reach for when a rule is stale and the question is which input moved. The resolved set and the recorded set are both in there.

Environment

mmz reads one variable out of the environment, and only for the times it writes down:

MMZ_NOW
pin “now” to a Unix epoch in seconds.

A cache record’s ran_at is stamped from it, and --status’s AGE column is measured against it. Both resolve it once per invocation, so a build that captures either gets the same bytes every time — and a fixture can show a genuinely aged record instead of the 0s ago a just-recorded run always prints. Unset, mmz reads the system clock.

It is deliberately not SOURCE_DATE_EPOCH. Dev shells and CI routinely export that one at the 1980-01-01 zip-epoch floor, and honouring it here would silently rewrite every stamp in every project that has it set.

A value that is not an epoch is refused with exit 2 by every action that reads the clock: a wrapped run stops before the command runs, and both --status renderings stop before printing. Falling back to the system clock would hide the misconfiguration and hand back exactly the non-determinism the pin exists to remove.

Freshness is untouched by any of this. mmz compares digests, never times, so a pinned clock changes what the output says and never which rules are fresh.

The full help text

Everything above is generated from this:

mmz 0.13.0 — memoized command runner

Usage:
mmz <command> [args...] run a command, skipping it when its inputs are unchanged
mmz --init write a starter .mmz/config.yaml in the current directory
mmz --status [--tag t]... show each rule's freshness as a table
mmz --status=json [--tag t]... the same as JSON, with each rule's inputs and hashes
mmz --status=json-schema print the JSON Schema for --status=json
mmz --is-fresh [--tag t]... [-- cmd]
exit 0 if cmd's rule (or every/tagged rule) is fresh; runs nothing
mmz --prune delete cache records whose rule no longer exists
mmz --import <dir> copy cache records in from another checkout of this repo
mmz --schema print the config JSON Schema
mmz --schema=fragment print the JSON Schema for an imported fragment
mmz --dump-config print the merged manifest and where each entry came from
mmz --dump-config=json the same as JSON
mmz --version print version and how many languages it parses
mmz --help print this help
mmz -- <command> [args] run a command whose name begins with a dash

Config lives in .mmz/config.yaml (nearest one, searching upward). mmz errors when no
manifest is found, the manifest is invalid, no rule matches, or a matched rule
has no inputs; relax the last two per project with the `strict` list.

`--tag`/`-t <tag>` (repeatable) narrows --is-fresh/--status to rules carrying
every listed tag (AND, not OR); untagged rules never match. Combining --tag
with a targeted command is a usage error — a command already resolves to one rule.
A --is-fresh whose selection holds no rule is refused (exit 7) rather than passing
on the strength of having checked nothing; --status reports it and exits 0.

Environment:
MMZ_NOW pin "now" to a Unix epoch in seconds, so a record's ran_at and
the AGE column read the same on every run. A malformed value is
refused, never ignored. Unset, mmz reads the system clock.

Exit codes:
0 fresh, skipped, or succeeded 6 a probe failed, could not run, or
1 --is-fresh: not fresh printed nothing (nothing recorded)
2 usage error 7 --is-fresh: nothing to gate (the
3 strict refusal (no rule / inputs) selection holds no rule)
4 manifest missing or invalid 8 a file mmz needed could not be
5 declared output missing after a read or written (nothing recorded)
successful run (nothing recorded) 70 internal error
127 command could not be spawned
otherwise the wrapped command's own exit code