CLI reference¶
Every command, its key options, and a worked example. Run mylonite COMMAND --help for
the authoritative, always-current list (the help strings and usage examples live in the
CLI itself). Global options --api-key-file and --env-file work before any command.
Beyond recognised provider credential/config names, --env-file also loads every
MYLONITE_TARGET_...-prefixed name and every name the run's own --target-file (if
any) references as ${NAME} — see
Secrets stay out of the file. A
recognised value that is itself an unresolved ${VAR}-shaped placeholder (never
substituted by a templating tool or secrets manager) is a config error naming the
variable and the file, not a silent load of that literal text as a credential.
Hard spend limit. The global option --max-llm-requests N (before the command), or
MYLONITE_MAX_LLM_REQUESTS=N, caps every LLM request the whole run sends, retries
included, across scan, validation and gate. It is the hard ceiling:
request N+1 is never sent. The run stops, exits 3, prints one
MYL-ABT-001 line naming the limit, and reads NOT
TESTED, never clean. The global option wins over the variable; with neither set
there is no ceiling. While a ceiling is set, Mylonite makes the provider retries itself
(num_retries from mylonite.yaml or MYLONITE_NUM_RETRIES, default 2) so each one is
counted toward the ceiling; with no ceiling set the same num_retries passes straight
through to LiteLLM's own retry loop. The default of 2 applies even when num_retries
is never configured anywhere, so setting a ceiling never silently removes the retries a
429 would otherwise have gotten — an explicit num_retries (including an explicit 0)
always wins over that default. Either way, a rate limit (HTTP 429) is retried with
exponential backoff and jitter, honouring the provider's Retry-After when it sends one
(capped at 60 s); an authentication error (401/403) or a bad request (400) is never
retried. If a rate limit still skips an attempt after every retry, the report names the
cause ("a provider rate limit (429) that survived every retry") instead of advising an
unrelated fix like declaring an effect_probe — lower --max-concurrent or raise your
provider's rate limit and re-run. --max-llm-calls is a softer per-scan budget, described
below.
Extra LLM headers. Some keys need a header on every request beyond the key itself:
an unscoped Anthropic key needs its workspace id, and some gateways route on a header.
Pass --llm-header NAME=VALUE before the command (repeat it for more than one), or set
MYLONITE_LLM_HEADERS to comma-separated NAME=VALUE pairs (a value can't contain a
comma there; use the flag for one that does); the flag wins over the variable for the
same name. --env-file loads MYLONITE_LLM_HEADERS too, so the header can sit next to
the key. Use the header name your provider's error names; for example:
Header values are treated as secrets. They go to the provider and nowhere else: the
console, artefacts, fixtures and replay cache keys never carry them, and Mylonite's
log filters mask them in log messages, tracebacks and extra fields (including LiteLLM's
own loggers).
Values shorter than 4 characters are not masked. A malformed entry exits 2, and the
error names only the entry's position, never any part of it. LiteLLM's own debug
logging (LITELLM_LOG=DEBUG) prints request headers; leave it off when a header carries
a secret.
Key check before the target starts. scan, validate, gate and ablate send one
tiny request per distinct role model (a few tokens, no retries, counted against
--max-llm-requests) before they launch or touch the target. If the provider refuses
the key, the run stops with one line and exit 4: an invalid or expired key (HTTP 401)
names the key variable, and a key that needs an extra header (HTTP 400, for example a
missing workspace id) names --llm-header, MYLONITE_LLM_HEADERS and the header the
provider's error names. Local providers
(Ollama, vLLM) are skipped. A timeout or rate limit on this request does not stop the
run; the later reachability check reports those. A key that is not set at all still
exits 2 before any request.
Exit codes: 0 ok (for gate: the scan ran and found nothing) · 1 structural findings present (the experimental check
--enforce — see experimental.md) · 2 config or usage error (incl. an
empty scan) · 3 LLM-call budget or the hard request ceiling exceeded · 4 no model
chosen, the provider refused the key or a required header, or the chosen provider
unreachable · 5 test rejected
(not kept), or generate refused an input whose validation did not keep it · 6
gate: the test generator returned nothing (internal collaborator failure) · 7 gate: the validator returned nothing (internal collaborator failure) · 8
gate: the git/gh step failed (your findings and validation report are still in --out) ·
9 gate: at least one proven finding was kept and its gate test written · 10 gate:
nothing was kept, but at least one finding reproduced without proof (STABLE, NOT PROVEN); it
is listed as a candidate and no test is written.
gate writes a gate test only for a finding whose verdict is KEPT: a passing build and a
passing differential or effect leg. A finding that reproduced but proved nothing is a
candidate. gate prints it with the reason and how to get it proven, lists it in the PR
body when something else was kept, and keeps its evidence outside the gate directory.
validate on its own still exits 0 for a STABLE, NOT PROVEN test (see below).
Budget exhaustion always exits 3, whichever layer of the run observes it first — and it
wins over a finding, not the other way round: a run that finds a real weakness and then
runs out of --max-llm-calls still exits 3. On gate, the finding is still turned into
a test, validated, and gated (the PR is opened or printed) before that exit code is
returned — see the gate page's precedence note.
No model chosen at all? There is no default provider or model. scan, gate,
validate and ablate each need one picked before touching a target — via
--model, mylonite.yaml's model: key, or MYLONITE_MODEL. With all three
unset, the command prints one line naming every approved provider, an example
--model value and the environment variable it needs, then exits 4 — before
any adapter, subprocess or LLM call, --dry-run included. check, scan
--scaffold (introspection only — no LLM call either way) and mylonite demo's
offline replay never need a model and keep working with nothing configured.
Model chosen, no credential for it? scan, gate, validate and ablate
all check every model they will call before doing any other work and exit 2
naming the missing environment variable. The message also offers a way past it
that needs no key at all: --model ollama_chat/llama3.2:3b with Ollama
running locally, or, on scan only, --dry-run to
preview the run with no LLM calls (this credential check, unlike the one above,
is skipped under --dry-run). validate's exit 4 (a credential IS set but
the provider can't be reached, on either the custom-target or the
reference-target path) points at the same local model and names the API-key
variable for the model's own provider. When the provider refused the check
with a rate limit (HTTP 429), could not be reached, or stalled, exit 4 says
that instead, names the provider and model, and says what to do; see
When the provider rate-limits or drops the run.
Which variable does it check? A provider in Mylonite's approved table (Anthropic,
Ollama, OpenAI, Gemini, Azure OpenAI, Vertex AI, Bedrock, vLLM/any OpenAI-compatible
endpoint, a LiteLLM proxy) is checked for that table's own variable(s) — a self-hosted
route like Ollama/vLLM needs none. A provider outside the table falls back to LiteLLM's
own <PROVIDER>_API_KEY naming convention and warns once, the first time. Either way,
this is a local name lookup: Mylonite never calls the provider to run the check.
Vertex AI authenticates via Application Default Credentials, not a key, so it's checked
for VERTEXAI_PROJECT/VERTEXAI_LOCATION instead. See Choose a model
for the full provider table, how --env-file/--api-key-file and the ambient shell
environment stack up when more than one sets the same variable, and a per-command
needs-a-key table.
demo — the reference-app playground¶
Zero-config, zero-key: shows one kept finding end to end, then the
vulnerable-vs-guarded scan behind it, on the bundled reference agent. This is the
fastest way to see what the tool actually does. Needs
pip install "mylonite[demo]" (the extra pulls the reference app).
mylonite demo # offline replay, a few seconds, no API key
mylonite demo --live # the scan makes real calls with the model you choose
What it prints, in order:
- The kept finding (W4, an email sent with no approval step). The verdict is
re-derived from a recorded run of the validator: fired without the safeguard,
resisted with it, held under a deterministic rewording, and the generated test
collects. Then come the severity, impact and suggested fix that
validateand the gate PR print. - Its regression test, red then green. The generated test's check fails on the vulnerable build, and the test file itself passes on the guarded build, both replayed from recordings.
- The scan table for W1 to W4 on both builds, the headline counts, a coverage note, and a legend for every mark.
The kept finding rests on one recorded run per build, so its flakiness leg reads "not
measured (1 run per build)" rather than a pass; a live validate repeats each build
several times to measure it. W1 does not land in the recording (see
limitations), and the
output says so instead of leaving a bare NO VERDICT cell.
Options: --live; --provider, --model (both --live only — replay is pinned to the
provider/model the fixtures were recorded against, and the command says so rather than
silently ignoring the flags).
What the default mode is, precisely. It replays LLM responses recorded against the
bundled targets, so it makes no network call and is byte-for-byte deterministic. The
scan, the adapters, the predicates and the differential are the real ones; only the model
responses are canned. The output labels itself mode: replay (offline) and carries the
model and date the fixtures were recorded — do not read a replayed number as a fresh
measurement of today's model. --live is the fresh measurement.
One deliberate difference from scan: the demo's scan table runs with the per-seed LLM
customiser and the LLM-judge fallback off, so it drives raw seed bodies judged by
deterministic predicates only. That is what makes the recording reproducible. A --live
scan exercises both of those stages and can legitimately reach a different result. The
kept finding is the exception: it replays the validator's own recorded run, customiser and
judge included, so --live changes the scan table and never the kept finding.
If fixtures are missing or stale the command fails with exit 2 and tells you to
re-record. It does not fall back to a live call, and it does not render a clean result —
a stale fixture would otherwise make the vulnerable side look guarded. See
the reference app.
checkis experimental. A structural, no-LLM pre-check of a tool surface used to be documented here. It still exists, but it's hidden and needsMYLONITE_EXPERIMENTAL=1— see experimental.md.
scan — find weaknesses¶
Run the exploit-finding loop against a target.
Target (positional): reference:vulnerable / reference:guarded (the bundled
reference app builds), or mcp:custom with --command/--arg. Omit when using --target-file
(your own MCP app). Non-reference targets need --authorize.
Key options: --target-file PATH (a path that doesn't exist prints the exact
--scaffold command that creates one, and points at
target.yaml), --authorize NAME (must equal the target's scope,
or its family when it declares no scope; a missing value prints the one to pass, and a
bundled target that needs a scope, such as mcp:filesystem, prints the mcp:filesystem:<scope>
form), --model (any LiteLLM
provider via a provider/model prefix, e.g. openai/gpt-4o),
--planner-model, --customiser-model, --judge-model, --max-llm-calls N
(a budget, not a hard ceiling — every seed keeps a floor of it, so the worst
case is higher; see Sizing --max-llm-calls; for a hard limit use
--max-llm-requests above),
--max-concurrent N (capped at 1 when the target declares an effect_probe, since a
concurrent attempt's change to shared state can't be told apart from this attempt's; see
What the effect probe still cannot see),
--output-dir PATH, --config mylonite.yaml, --dry-run,
--allow-no-seed-arm, --purpose "…" (a one-line description of what the app is for;
tailors the probes to its domain — overrides purpose in the target file, and is
persisted so generate/validate reuse it); --randomize-exfil/--no-randomize-exfil
(mint a unique exfil address per run so a finding proves the target leaks to ANY
attacker destination, not one demo literal — defaults ON for live custom-target
scans, off for the reference/replay path; matches generate/validate/gate).
--weakness-class W2 (repeatable; one meaning everywhere — only these classes run.
On reference:* and a bundled mcp:<family> it filters which seeds run. On a custom
target, --target-file or inline mcp:custom flags, it filters the target file's
declared weakness_classes the same way — it never widens past what the file
declares; naming only classes it doesn't declare matches nothing and refuses before
any spend, naming both the flag's values and the file's declared ones. With no
weakness_classes declared at all, the flag declares them instead — the same thing
the inline mcp:custom flags already do with nothing else to set them. Rejects an
unknown or lowercase value, e.g. w4, naming it). For a custom target: --command,
--arg, --env, --scope, --system-prompt/--system-prompt-file, --primary-tool.
--command/--arg/--env build an INLINE target from flags; combining any of them
with --target-file (which already fully describes the target) refuses with exit 2
naming the flag(s) passed, except with --scaffold, which uses them to BUILD the file
in the first place.
If an attack module that would have run fails to import or construct, scan (and
gate) runs the rest and reports each weakness class that module covers as NOT TESTED
with MYL-NT-015. An attack modules: line names the
module, and a scan with no finding exits 2.
For a custom/--target-file target, scan (and gate) refuses before any LLM call if
a declared weakness_classes entry has zero seeds this surface could ever run — see
Coverage.
--dry-run downgrades the refusal to a warning; --allow-no-seed-arm exempts a
declared W2 with no seed_arm from the refusal and proceeds, with its seeds scheduled
and each one honestly reporting NOT TESTED. The refusal's own describe() call runs
after the provider key and model checks, so a missing key or a bad --model never
reaches it. Inferring seed_arm (auto-wire) is a separate, earlier probe: it runs
after the model check but before the key check, so a target with a declared W2 and no
seed_arm can still launch the server once with no key set. If either probe can't
describe the server in time, the run exits 2 with that reason instead of skipping the
check. Auto-wire's describe() call carries a budget of at least 20 seconds (more if
the target file's timeout_s is larger — see
target-file.md); a first-run npx/uvx server
download can genuinely take that long. A timeout there names timeout_s and says to
re-run (the download is cached after that) or raise timeout_s, and exits — never the
misleading "add a seed_arm" advice. --dry-run runs the auto-wire too (it reads the
tool list and makes no LLM call; the model check is skipped), so a dry run previews W2
the way the real scan will run it.
Every model scan resolves (--model/--planner-model/--customiser-model/
--judge-model) is checked against LiteLLM's own provider registry once, before any
seed runs (also true of gate/validate/ablate). A value LiteLLM can't route —
including one shaped like provider/model with an unknown provider, e.g.
not-a-real/model — exits 2 with one message naming the value and --model, instead
of a live call failing (and repeating) once per seed.
Before any live call, scan prints one line estimating how many LLM calls the
run will make, from the seed count after filters — a range, since a seed can
need anywhere from a couple of calls to several. It also names the hard
ceiling (--max-llm-requests/MYLONITE_MAX_LLM_REQUESTS) when one is set, and
says cost is unknown rather than guessing a price (no provider carries one
yet). --dry-run makes no call and skips this line.
Just before the estimate, scan prints a short run plan, ahead of calibration's control
writes and the first model call:
Run plan (nothing has been sent yet):
Model: openai/gpt-4o-mini (planner, customiser and judge)
System prompt: declared in prompt.txt (system_prompt_file)
Consequential tools this run may drive: send_email, create_ticket
never_call (blocked before the server): wipe_account
Run against a test instance, never production.
The tools come from the target's tool list, minus anything under
control_config.never_call,
which Mylonite blocks before the server. With no system_prompt or system_prompt_file
declared, the plan names Mylonite's generic default prompt and adds a warning that says how
to declare yours. --dry-run skips the plan too.
Scaffold mode — --scaffold PATH (with --command) introspects an MCP server
(one launch, no LLM call, no attack, so no --authorize needed) and writes a
target.yaml that runs as written: the weakness_classes the introspected surface can
cover, plus a seed_arm and an effect_probe written live, tagged # auto-detected,
when it finds them (see target-file.md). A candidate that still needs
a value from you stays commented. Add --force to overwrite. Review it, then scan with
--target-file.
The one launch --scaffold makes (and check's, and the first connect a scan/gate
makes) gets a generous 120-second budget by default, since a first npx -y <package>
or uvx <package> run downloads the package before the server can answer at all. If it
still times out, the message names the budget and both fixes: run the server's launch
command once yourself first (or install the package), or raise the timeout — there is
no --target-file yet at this point to set timeout_s in, so set the
MYLONITE_MCP_STARTUP_TIMEOUT_S env var instead (ignored once a target file sets
timeout_s; see target-file.md).
The file also lists every tool in a comment block, the tool inventory: each tool's role
(consequential, egress, read, store, recall), where that role came from (declared in
the target file, the server's MCP annotation, a schema parameter, the tool's name,
or unknown), and whether the scan's confirm gate treats it as consequential. A tool
nothing recognises reads unknown and is treated as consequential, which is what the
scan already does with it. The scaffold names the unknown tools when it finishes. To
confirm them, list your consequential tools under control_config.consequential_tools;
every tool left off that list is then treated as not consequential. The block is
comments only, so the scan reads the same file either way.
To scaffold a REST/HTTP agent instead of an MCP server, pass --rest-url URL (no
--command needed) — add --rest-body for a request-body template other than the
default {"prompt": "{prompt}"}, and --rest-response-path for a dotted path into
the JSON reply (e.g. choices.0.message.content) when the agent's answer isn't the
whole body. See the HTTP-agent walkthrough.
mylonite scan --command python --arg my_server.py --scaffold app.yaml --scope my-app # generate the target file
mylonite scan --target-file app.yaml --authorize my-app # then scan it
A secret passed with --env (for example --env GITHUB_TOKEN=ghp_...) is not written
to the file. The file gets a ${MYLONITE_TARGET_ENV_GITHUB_TOKEN} placeholder, and
scaffold prints the variable to set, before its next: line, in both shell forms:
note: secrets in headers and env were kept out of app.yaml. It reads them from these environment variables; set them before you use the file:
bash/zsh:
export MYLONITE_TARGET_ENV_GITHUB_TOKEN='<your GITHUB_TOKEN>'
PowerShell:
$env:MYLONITE_TARGET_ENV_GITHUB_TOKEN = '<your GITHUB_TOKEN>'
args has no key name to mask a value by, so a credential passed through --arg
instead (an api-key=sk-...-shaped flag, a URL with ?access_token=...) is still
written in plain text — but now warns, naming the position and withholding the value:
args[0] looks like it carries a credential (value withheld). Move it to env: .... See
A credential in args is written in plain text.
Set it before check, scan or gate loads the file; an unset one stops the load with
exit code 2 and the same export line. The scan-directory copy, generate's copy and
gate's copy print the same note. See
Secrets stay out of the file.
When a scan finds something, the summary table is followed by one block per finding,
most severe first: a Severity line, a one-sentence Impact line naming what an
attacker gets from that weakness class, and a suggested fix — the same facts the
gate PR body opens with, reused rather than re-derived, so the fix is
never exclusive to a committed test or a validated directory. It is always introduced
as a suggestion; scan proves and gates a weakness, it does not patch your code. The
Next: mylonite generate … hint follows the last block, with the scan directory's
path rendered with forward slashes on every platform (so it needs no quoting on its
own, and parses the same way in cmd, PowerShell or bash).
generate — emit the regression test¶
Emit a pytest regression test from a confirmed exploit. Offline and deterministic — no LLM call. Carries the compliance metadata.
Options: scan_path (an exploit_*.json or scan dir) or --latest (searches
--scans-dir, default the resolved scans dir, normally .mylonite/scans — an
input, where --latest looks for a scan to read, not where this command writes;
ignored when you pass scan_path directly); --out PATH;
--target-file PATH (custom targets — co-locates the YAML so the live test re-drives
your app; usually not needed, since this auto-resolves target.yaml from the scan
directory scan_path points at — pass it only when that file isn't there);
--prove-control (emit a control-efficacy test); --unvalidated (see below).
A test written from a scan is a candidate until validate keeps it, so its first
line reads # mylonite: unvalidated and a short UNVALIDATED header follows. The
command prints an UNVALIDATED line with the validate command to run, literally —
for a custom target it carries the --authorize value that target's scope (or
family, if it declares none) requires, and every printed path renders with forward
slashes on every platform (quoted too, only if it still needs it — a space, say).
The "set your provider key" line next to it names that finding's
own provider's env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, ...), or "your
provider's API key" — naming no vendor — when the provider isn't known. When
validate returns KEPT it removes the header; a STABLE, NOT PROVEN or
REJECTED verdict adds it back, also to a test that was kept before or written
without one. The header always matches the latest verdict.
generate reads the validation_report.json next to the input exploit, if there
is one (validate and gate write it). If it is REJECTED, STABLE, NOT PROVEN or
unreadable, generate writes nothing and exits 5. Pass --unvalidated to write
the test anyway, with the header. If it is KEPT, the test goes without the header
only when that report proved this exact test: the folder holds one exploit, the
exploit and the folder's target.yaml are no newer than the report, any
--target-file matches that target.yaml, --prove-control is off, and the test
the report names matches the one about to be written. Otherwise the test is stamped.
validate — prove the test¶
Run the generated test through the validation engine, LIVE. On a real
--target-file app the control-efficacy check
holds the model constant and toggles only the safeguard; against the bundled reference app
it runs the two-build differential. A test is kept only when it discriminates reliably.
Options: target (the generated dir/file); --iterations N (default 5); --model
(any LiteLLM provider via a provider/model prefix); --planner-model,
--customiser-model, --judge-model (the three model roles,
each defaulting to --model); --config PATH; --target-file PATH (re-drive
your REAL app instead of the reference build — usually not needed: for a custom
target this auto-resolves target.yaml co-located with the test, the same file
generate wrote; pass it explicitly only when that file isn't there);
--authorize (required when --target-file names a custom target — must equal the
target's declared scope, or its family name if no scope is declared; see
target-file.md); --fast
(skip the differential leg — faster, weaker); --prove-input-control (for a
black-box HTTP/rest target, run the input data-framing ("spotlighting")
differential to measure whether that input defence is load-bearing; a no-op once
--fast has already skipped the differential leg); --randomize-exfil/--no-randomize-exfil
(mint a unique exfil address per run so the finding proves the target blocks ANY attacker
destination, not one demo literal — defaults ON for live custom-target runs, off for the
reference/replay path); --iteration-timeout S (default 120s — the wall-clock budget for each
differential run, on a custom target and on the bundled reference twins alike; a
stuck or slow run is cut off cleanly, is reported as reaching no verdict, never
counts as the guard resisting, and never hangs the job. The robustness re-drives
and the fixture recording have their own call budget instead).
Before any live call, validate also prints one line estimating how many LLM
calls the run will make — from --iterations, the twin count, and (on the
reference target) the metamorphic re-drive count — plus the hard ceiling
(--max-llm-requests/MYLONITE_MAX_LLM_REQUESTS) when one is set. It never
prices the run: no provider carries a price yet. The same run plan as scan comes
first; on a custom target its tool line names the declared consequential and egress
tools, or else the calls the recorded finding made.
Before any live run, validate prints a banner naming the model it resolved
(--model/mylonite.yaml/MYLONITE_MODEL) and checks that model — and the planner,
customiser and judge models when they differ — is reachable with one tiny completion
each, not a full scan, so a slow-but-working model (a local or self-hosted one in
particular) answers well inside --iteration-timeout instead of being misdiagnosed as
unreachable. The check honours a configured api_base (see
Self-hosted models).
The report's notes record the models the test was proved against
(validated against model: <planner>, plus the customiser and judge when they differ).
To re-prove a committed test after a model change, see
Re-validate on a new model.
A kept test exits 0. A plain KEPT verdict is followed by the same severity,
impact and suggested-fix block scan shows under a finding, before the "Next: commit"
line — so the fix is shown here too, not only in the gate PR body. If its verdict
reads STABLE, NOT PROVEN (no differential or
effect proof, see what the numbers mean),
validate says the committed test would gate reproduction only, and points you at a
guarded side or an effect_probe — without the severity/impact/fix block, since
nothing was proven yet.
A run that cannot reach a verdict stops with one line and no traceback. It exits 3
(MYL-ABT-001) when the LLM call budget runs out mid-run, and 2 (MYL-ABT-006)
when a custom target does not come up, so a run could not even describe it. If the
target went down after some runs finished, the line says how many and that their
results were discarded. Neither is reported as a rejected test. gate reports a
target that does not come up the same way, and says later findings were not
validated.
Against the bundled reference app, validate records replay fixtures into the
test's fixtures/ folder. It checks that folder before the provider check's
first call and exits 2 with one line if it cannot record there: the folder was
recorded in an older format (cache_key_version 1 or 2), names a version this
release does not know, has a damaged _meta.json, or holds recordings with no
_meta.json. Delete the folder and run again. A recording error later in the
run exits 2 the same way. gate re-records each finding's own fixtures/
folder instead, and puts the earlier one back if the run does not keep (see
CI gating).
When it finishes, validate prints an llm: line with the calls it made (by role), the
tokens the provider reported, and the wall-clock time. The metamorphic stage runs under
its own call budget; if that budget is reached before every perturbation has run, the
stage does not pass.
gate — scan → generate → validate → PR (the full pipeline)¶
The whole pipeline; only a kept test makes it through. The PR body always includes a Proven fix (control-efficacy findings) or Recommended fix (otherwise) — an evidence-anchored recommendation naming the actual tool and argument that landed the exploit, as a fenced code sketch. See Reading the results.
gate does not modify your repository unless you ask it to. By default it writes
only to --out (normally .mylonite/gate/) and prints the git/gh commands to
commit and open the PR yourself. --open-pr performs the branch/commit/push/PR;
--workflows scaffolds the two CI templates, but only once this run has kept at
least one finding — a run that keeps nothing writes no workflow file, and says so,
naming why. Both default to off — changed in 0.8.5,
where --workflows defaulted on and the branch and commit happened on every run.
Either flag resolves the repository root with git rev-parse --show-toplevel — so
gate works from any subdirectory — and fails fast, on exit code 8, outside a git
repository entirely.
gate generates, validates, and (for every KEPT one) gates every finding the
scan proves, not just the first — one branch, one PR, in deterministic pattern-id
order. See Gating every finding for the branch
naming, the multi-finding console output, and the budget-exhaustion-with-findings
exit code.
For a --target-file target, gate refuses before any LLM call if a declared
weakness_classes entry has zero seeds this surface could ever run — the same
pre-flight check scan runs; see
Coverage.
gate does not auto-wire a seed_arm the way scan does, so for a W2 target without
one, run scan first and pass the target.yaml it writes to gate — or pass
--allow-no-seed-arm to gate it as-is: those seeds report NOT TESTED instead of
blocking the whole run, the same meaning the flag has on scan.
gate runs its pre-flight checks in this order, all before any LLM call:
- The repository checks, each exiting
8: a--basethat is empty, contains whitespace or starts with-; with--open-pror--workflows, an output directory outside the repository; with--open-pr, a working tree with staged files or uncommitted changes to tracked files. - On Windows without long paths, the output path check, exiting
2(see Windows path length). - The target,
--authorize, model, provider key and uncoverable-class checks, each exiting2. A bundledmcp:<family>[:scope]target combined with--open-pror--workflowsis refused here too, before the scan runs: that target has notarget.yamlof its own for the gate to commit, so the scaffolded workflow could never launch it in CI. Runmylonite scan --scaffold <path>to write one, then gate with--target-file <path>instead. Dropping both flags still gates the bundled target and writes its artefacts locally, unchanged.
So when both kinds of problem are present, the exit-8 error is the one you see first.
Options: target or --target-file (a custom target comes only through
--target-file; gate does not take inline mcp:custom flags); --authorize (the
target's scope, or its family when it declares no scope); --open-pr (create the branch,
commit, push, and open the PR via gh; refused on a tree with staged or uncommitted
changes, and a rollback never deletes a branch the run didn't create; after the commit it
returns you to the branch or detached commit you started on and keeps the gate branch,
naming it in the output); --base BRANCH (the branch the PR targets; defaults to the
repository's default branch: origin/HEAD, else the current branch's upstream, else
main; running from a feature branch, pass --base); --config; --model (any LiteLLM provider via
a provider/model prefix); --planner-model, --customiser-model, --judge-model
(the three model roles, each defaulting
to --model, same split as scan); --purpose "…" (a one-line description of what
the app is for; tailors the probes to its domain — overrides purpose in the target
file); --out PATH; --max-llm-calls (a budget for the scan
phase, not a hard ceiling — see Sizing --max-llm-calls; the
global --max-llm-requests caps scan and validation together);
--iterations N (validation-leg iterations, default 3 — the kept verdict reflects
reproducibility across runs; pass 1 for the fastest, weakest gate); --runs-on LABEL
(GitHub runner; use a self-hosted label for in-perimeter MCP backends);
--workflows/--no-workflows (default off); --llm-enrich (append a labelled, unverified LLM fix
suggestion, rendered after the structural recommendation above); --fast (skip the
differential leg for a custom target: gate then keeps a finding only when an
effect_probe proves it, otherwise the finding is a candidate and gate exits 10);
--prove-input-control (for a black-box HTTP/rest target, run the input
data-framing differential to measure whether that input defence is load-bearing;
opt-in, otherwise a rest target is validated by stability + effect + consensus;
gate lists a rest finding as a candidate and never commits it);
--randomize-exfil/--no-randomize-exfil (defaults ON for a live custom target);
--allow-no-seed-arm (gate a custom target that declares an indirect-injection class
like W2 with no seed_arm anyway; those seeds report NOT TESTED instead of blocking
the run — gate never auto-wires one the way scan does, so this is the only way to
gate such a target without running scan first).
Before the scan phase starts, gate prints one line estimating how many LLM
calls the scan will make (from the seed count after filters), plus about how
many more each finding kept and validated adds — the validation count depends
on how many findings the scan keeps, so it is a per-finding note, not a false
total. It also names the hard ceiling (--max-llm-requests/
MYLONITE_MAX_LLM_REQUESTS) when one is set; see
Sizing --max-llm-calls for the full formula. The same run plan as
scan comes first.
gate ends with a gate llm: line: the LLM calls made across every stage (by role), the
tokens the provider reported, and the wall-clock time.
report — render findings¶
Render a saved scan or validation as a terminal trust panel — including the same evidence-anchored recommendation the gating PR carries — offline. See Reading the results.
Options: target (a scan dir, a generate-emitted dir once validate has run, or a
*_report.json); --sarif PATH (SARIF 2.1.0
for GitHub code scanning); --json PATH (machine-readable finding bundle). Both carry
the differential proof, the OWASP/ASI/ATLAS/NIST tags, and the same recommendation.
For a scan directory with a verdicts.json, report also prints the same per-class
summary and calibration status the scan printed (see The per-class
summary).
ablateis experimental. Scoring each safeguard as load-bearing, security theater, redundant, no-attack or inconclusive used to be documented here. It still exists, but it's hidden and needsMYLONITE_EXPERIMENTAL=1— see experimental.md.Scaffolding moved. The old
mylonite init-targetcommand is nowmylonite scan --scaffold PATH(seescanabove). See Test your own app and the target.yaml reference.
version¶
Print the installed version.
plugins¶
List installed extension plugins across all five contract
groups, with each plugin's declared contract version. Discovery here also runs
the version-compatibility check, so a major-mismatched plugin is reported rather
than failing silently mid-run. Attack modules are the group that is also run
by scan/gate; the other four use the bundled reference implementation.
An adapter marked "configured per target" is not broken: the target-adapter contract flows configuration through the target-file factory, so an adapter for a named server family is built with that family rather than discovered ready-made.
A plugin marked "INCOMPATIBLE" declares a contract major version this Mylonite cannot load, and will be skipped at run time. The full listing still prints — that is the point of the command — and the exit code is non-zero so a scripted check still catches it.
A plugin marked "FAILED TO LOAD (ImportError)" raised when it was imported. An
attack module that raises when built with no arguments is marked the same way, with
its error type, because a scan can't run it either. Only the error type is shown, never
the message. A scan reports such a module's classes NOT TESTED with
MYL-NT-015.
Run config (mylonite.yaml)¶
scan, gate, validate, ablate and check accept --config mylonite.yaml (auto-discovered from ./mylonite.yaml)
to declare target_file / authorize / provider / model / budget once. An explicit
flag always wins.
Most keys also have a flat MYLONITE_* env var, the lowest-precedence source of the
three: an explicit flag wins over mylonite.yaml, which wins over the env var, which
wins over the command's own built-in default.
mylonite.yaml key |
Env var | What it sets |
|---|---|---|
target_file |
— | the custom-target YAML (--target-file) |
authorize |
— | the ownership assertion (--authorize) |
provider |
MYLONITE_PROVIDER |
the LiteLLM provider id — deprecated, prefix model instead |
model |
MYLONITE_MODEL |
the model LiteLLM calls |
planner_model |
MYLONITE_PLANNER_MODEL |
the model driving the agent under test; defaults to model |
customiser_model |
MYLONITE_CUSTOMISER_MODEL |
the model crafting attack payloads; defaults to model |
judge_model |
MYLONITE_JUDGE_MODEL |
the model deciding the LLM-judge fallback verdict; defaults to model |
max_llm_calls |
— | the process-wide LLM call budget for a scan (--max-llm-calls) |
api_base |
MYLONITE_API_BASE |
a self-hosted or proxy LiteLLM endpoint |
max_tokens |
MYLONITE_MAX_TOKENS |
the per-call max_tokens |
temperature |
MYLONITE_TEMPERATURE |
the per-call temperature |
timeout |
MYLONITE_TIMEOUT |
the per-call socket timeout, in seconds |
num_retries |
MYLONITE_NUM_RETRIES |
the per-call LiteLLM retry count (default 2) |
root |
MYLONITE_ROOT |
overrides the artefact root every command's output nests under |
target_file, authorize and max_llm_calls have no flat env var — set them with
--target-file/--authorize/--max-llm-calls, or in mylonite.yaml.