CLI reference
snag [OPTIONS] [COMMAND]
| Command | Purpose |
|---|---|
run | Execute test suites. Default. |
list | Discover and print tests without executing them |
check | Parse manifests and compile scripts, no network |
init | Scaffold a suite plus an example script |
revision | Write the release manifest for this build |
update | Replace this binary with the latest release |
completions | Generate a shell completion script |
The implicit run verb
snag demo/suite.toml is rewritten to snag run demo/suite.toml. The rule: if
the first non-flag argument is not one of run, list, check, init,
revision, update, completions, or help, then run is inserted in front
of it. Arguments after
a -- separator are left alone.
snag # no arguments at all → run
snag demo/suite.toml # a path → run demo/suite.toml
snag list # a known verb → left as-is
:::caution Flags alone do not trigger it
An invocation made only of run flags has no positional argument, so nothing
triggers the rewrite and clap rejects the flag:
$ snag -t smoke
error: unexpected argument '-t' found
Write snag run -t smoke, or add a path: snag . -t smoke. Global options
(-f/--format, --color, -v, -q) are accepted in either position.
:::
Global options
Available on every command.
| Option | Values | Default | Meaning |
|---|---|---|---|
-f, --format <FORMAT> | human, jsonl, json, teamcity, junit | human | Reporter to use |
--color <COLOR> | auto, always, never | auto | ANSI color. auto = on when stdout is a TTY |
-v, --verbose | repeatable | off | More output. -v also prints captured output for passing tests |
-q, --quiet | off | Suppress non-essential human output. Conflicts with -v | |
-h, --help | Help. --help is long-form, -h is a summary | ||
-V, --version | Print the version |
Selection options
Accepted by run, list, and check, with identical behaviour.
| Option | Repeatable | Meaning |
|---|---|---|
[PATH]... | yes | Suite files, directories, or globs. Empty = walk the current directory |
-k, --filter <FILTER> | yes | Keep tests whose name or id contains this substring. Multiple values are OR-ed |
-e, --exclude <SUBSTR> | yes | Drop tests whose name or id contains this substring. Beats --filter |
-t, --tag <TAG> | yes | Keep tests carrying this exact tag. Multiple values are OR-ed |
--regex | no | Treat --filter and --exclude as regular expressions |
Different option kinds are AND-ed; repeats of one option are OR-ed. See selecting tests.
snag run
snag run [OPTIONS] [PATH]...
Runs the selected tests and reports the results.
| Option | Default | Meaning |
|---|---|---|
-j, --jobs <N> | 0 | Worker count for the parallel phase. 0 = logical CPU count. Clamped to the number of tests |
--fail-fast | off | After the first failure or timeout, report all not-yet-started tests as skipped |
--timeout <DURATION> | none | Per-test timeout, overriding both manifest levels. humantime syntax: 500ms, 30s, 2m |
--retries <N> | 0 | Re-run a failing test up to N extra times. attempts is reported |
--seed <SEED> | none | Shuffle test order deterministically with this seed |
--dry-run | off | Execute nothing; report every selected test as skipped with the message dry run |
--report <FILE> | none | Write the machine report to a file, keeping human output on the terminal |
Plus every global and selection option.
snag # everything under .
snag tests/ -t smoke # one tier
snag run -j 4 --fail-fast # four workers, stop early
snag tests/ --retries 2 --timeout 30s # tolerate flakiness, bound each test
snag tests/ -f junit --report report.xml # human on stdout, XML in a file
--report and --format together
--report FILE splits the audiences: the terminal always gets the human
reporter, and the file gets the machine one. Because a human report is useless
in a file, --format human is translated to json for the file only.
| Invocation | stdout | File |
|---|---|---|
--report f | human | JSON |
-f json --report f | human | JSON |
-f jsonl --report f | human | JSONL |
-f junit --report f | human | JUnit XML |
-f teamcity --report f | human | TeamCity messages |
-f junit | JUnit XML | — |
The file is created before the run starts; a failure to create it aborts with exit code 2.
snag list
snag list [OPTIONS] [PATH]...
Runs discovery and filtering, prints the result, executes nothing.
| Option | Meaning |
|---|---|
--long | Also print each test's name, tags, and resolved script path |
$ snag list --long demo/suite.toml
demo/suite.toml::example-responds
name: example.com answers 200
tags: network, smoke
script: demo/example_test.snag
demo/suite.toml::json-api
name: JSON body can be walked by path
tags: network
script: demo/json_test.snag
demo/suite.toml::assertions
name: assertion helpers, no network
tags: fast
script: demo/assertions_test.snag
3 test(s)
Without --long, one test name per line, followed by a count (suppressed by
-q).
With -f json, -f junit, or -f teamcity, a JSON array is printed; with
-f jsonl, one JSON object per line:
$ snag list demo/suite.toml -t fast -f jsonl
{"id":"demo/suite.toml::assertions","name":"assertion helpers, no network","parallel_safe":true,"script":"demo/assertions_test.snag","suite":"Snag Demo Regression Suite","suite_path":"demo/suite.toml","tags":["fast"],"test_id":"assertions","timeout_ms":10000}
Fields: id (qualified), test_id, name, suite, suite_path, script,
tags, timeout_ms (resolved, null when unset), parallel_safe.
snag check
snag check [OPTIONS] [PATH]...
Loads every manifest and compiles every script with the same engine and the same registered functions the runner uses. No requests are sent.
$ snag check -v
ok: demo/suite.toml::example-responds
ok: demo/suite.toml::json-api
ok: demo/suite.toml::assertions
checked 3 test(s): no errors
Failures print per test, and the command exits 2:
$ snag check
error: ./suite.toml::a: Unexpected ';' (line 1, position 9) (a.snag)
Error: 1 of 1 test(s) failed to compile
What it catches: missing script files, syntax errors, and manifest problems. What it does not catch: unknown function names and constant reassignment — Rhai resolves those at call time.
-v lists the passing tests too; -q suppresses the summary line.
snag init
snag init [OPTIONS] [PATH]
Scaffolds a suite and an example script.
| Argument / option | Default | Meaning |
|---|---|---|
[PATH] | suite.toml | Where to write the manifest |
--force | off | Overwrite an existing manifest (and script) |
$ snag init
created suite.toml and ./get_status.snag
run it with: snag suite.toml
The script is written next to the manifest as get_status.snag, and parent
directories are created as needed — snag init tests/api.snag.toml writes
tests/api.snag.toml and tests/get_status.snag. An existing manifest is never
overwritten without --force; an existing script is left alone the same way.
snag revision
snag revision [OPTIONS]
Writes a revision.json describing this build: the
version it was compiled as, the commit it was compiled from, and a download URL
for each released platform. The release workflow runs it on every tag and
uploads the result next to the binaries.
| Option | Default | Meaning |
|---|---|---|
-o, --output <FILE> | revision.json | Where to write it. - writes to stdout |
--tag <TAG> | v{version} | Release tag the download URLs point at |
--commit <SHA> | the commit baked in at build time | Commit hash to record |
--repo <OWNER/NAME> | ShortyPing/snag | Repository hosting the release assets |
--build <N> | one past the previous release's | Build number to record. Skips the lookup |
--previous <SOURCE> | the latest release asset in --repo | File path or http(s) URL to increment from |
--offline | off | Skip the lookup and start the build number at 1 |
$ snag revision
wrote revision.json (v0.1.0 build 1 41795ba65fb5a667b606f91a139390c415c7306f at 2026-08-26T18:57:11Z)
The build number
build counts releases, not invocations: each generated file is one past the
build number in the previous release's revision.json. Without --previous,
that file is fetched over the network from
https://github.com/{repo}/releases/latest/download/revision.json, with a
10-second timeout.
A lookup that finds nothing starts the count at 1, which is right for a first
release and wrong for a network blip — so the fallback always says so on stderr:
$ snag revision --previous ./does-not-exist.json -o -
warning: no previous revision at ./does-not-exist.json (reading ./does-not-exist.json: No such file or directory (os error 2))
warning: starting the build number at 1
-q does not silence those two lines. In a pipeline, resolve the previous file
yourself and pass --previous, so a failed download fails the job instead of
rewinding the counter — see revision.json.
Use --offline for a deliberate first release, and --build N to set the
number outright. The three flags are mutually exclusive.
The version comes from the crate itself and cannot be overridden — the whole
point is that the file reports what the binary generating it actually is. The
tag can differ from the version (--tag nightly) when assets live under a
moving tag; the URLs follow the tag, the version field follows the binary.
-q suppresses the confirmation line. --format has no effect here: the output
is always JSON.
snag update
snag update [OPTIONS]
Downloads the release binary for this platform and replaces the running executable with it. See updating for the full picture.
| Option | Default | Meaning |
|---|---|---|
--check | off | Report whether a newer release exists; install nothing |
--force | off | Reinstall even when already on the latest version |
--tag <TAG> | latest release | Install a specific release |
--manifest <SOURCE> | the release on GitHub | Read the manifest from a path or http(s) URL |
--repo <OWNER/NAME> | ShortyPing/snag | Repository to fetch from |
$ snag update --check
snag 0.1.0 is up to date
The platform is chosen by the target triple this binary was compiled for, which
must be a key in the release's revision.json:
$ snag update
Error: release v0.1.0 has no binary for aarch64-apple-darwin (it has: x86_64-unknown-linux-gnu)
Before anything is replaced, the download is run with --version and its answer
compared to the version the manifest promised. A mismatch aborts and leaves the
installed binary alone:
$ snag update --force
Error: expected the download to report version 9.9.9, got "0.1.0"
The manifest carries no checksum, so this is the only integrity check there is;
the rest rests on TLS to github.com.
Installing needs write access to the directory holding the binary, not to the binary itself — the file is replaced by rename, not overwritten:
$ snag update --force
Error: updating /usr/local/bin/snag needs write access to /usr/local/bin: writing /usr/local/bin/.snag-update-31102: Permission denied (os error 13)
--tag and --manifest are mutually exclusive, as are --check and --force.
snag completions
snag completions <SHELL>
Writes a completion script to stdout. Shells: bash, zsh, fish, elvish,
powershell.
snag completions zsh > "${fpath[1]}/_snag"
snag completions bash > /usr/local/etc/bash_completion.d/snag
snag completions fish > ~/.config/fish/completions/snag.fish
Exit codes
| Code | Meaning |
|---|---|
0 | All selected tests passed (or none were selected) |
1 | At least one test failed or timed out |
2 | The run could not be completed: no suites found, unparseable manifest, bad regex, unwritable report |
A closed stdout pipe (snag list \| head) is not an error — it exits 0.
See exit codes for the full contract.
Environment
Snag reads no configuration from the environment. Scripts read it explicitly
with env("NAME") and env_or("NAME", "default"); anything you export before
invoking snag is visible to them.
Two exceptions:
| Variable | When | Effect |
|---|---|---|
SNAG_NO_UPDATE_CHECK | run time | Any non-empty value disables the update check and its notice |
SNAG_GIT_COMMIT | build time | Sets the commit hash snag revision reports |
Without SNAG_GIT_COMMIT the build asks git rev-parse HEAD, and falls back to
unknown outside a checkout.
The update check also reads XDG_CACHE_HOME (LOCALAPPDATA on Windows) to find
where to cache its result — see updating.