Skip to main content

CLI reference

snag [OPTIONS] [COMMAND]
CommandPurpose
runExecute test suites. Default.
listDiscover and print tests without executing them
checkParse manifests and compile scripts, no network
initScaffold a suite plus an example script
revisionWrite the release manifest for this build
updateReplace this binary with the latest release
completionsGenerate 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.

OptionValuesDefaultMeaning
-f, --format <FORMAT>human, jsonl, json, teamcity, junithumanReporter to use
--color <COLOR>auto, always, neverautoANSI color. auto = on when stdout is a TTY
-v, --verboserepeatableoffMore output. -v also prints captured output for passing tests
-q, --quietoffSuppress non-essential human output. Conflicts with -v
-h, --helpHelp. --help is long-form, -h is a summary
-V, --versionPrint the version

Selection options

Accepted by run, list, and check, with identical behaviour.

OptionRepeatableMeaning
[PATH]...yesSuite files, directories, or globs. Empty = walk the current directory
-k, --filter <FILTER>yesKeep tests whose name or id contains this substring. Multiple values are OR-ed
-e, --exclude <SUBSTR>yesDrop tests whose name or id contains this substring. Beats --filter
-t, --tag <TAG>yesKeep tests carrying this exact tag. Multiple values are OR-ed
--regexnoTreat --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.

OptionDefaultMeaning
-j, --jobs <N>0Worker count for the parallel phase. 0 = logical CPU count. Clamped to the number of tests
--fail-fastoffAfter the first failure or timeout, report all not-yet-started tests as skipped
--timeout <DURATION>nonePer-test timeout, overriding both manifest levels. humantime syntax: 500ms, 30s, 2m
--retries <N>0Re-run a failing test up to N extra times. attempts is reported
--seed <SEED>noneShuffle test order deterministically with this seed
--dry-runoffExecute nothing; report every selected test as skipped with the message dry run
--report <FILE>noneWrite 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.

InvocationstdoutFile
--report fhumanJSON
-f json --report fhumanJSON
-f jsonl --report fhumanJSONL
-f junit --report fhumanJUnit XML
-f teamcity --report fhumanTeamCity messages
-f junitJUnit 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.

OptionMeaning
--longAlso 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 / optionDefaultMeaning
[PATH]suite.tomlWhere to write the manifest
--forceoffOverwrite 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.

OptionDefaultMeaning
-o, --output <FILE>revision.jsonWhere 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 timeCommit hash to record
--repo <OWNER/NAME>ShortyPing/snagRepository hosting the release assets
--build <N>one past the previous release'sBuild number to record. Skips the lookup
--previous <SOURCE>the latest release asset in --repoFile path or http(s) URL to increment from
--offlineoffSkip 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.

OptionDefaultMeaning
--checkoffReport whether a newer release exists; install nothing
--forceoffReinstall even when already on the latest version
--tag <TAG>latest releaseInstall a specific release
--manifest <SOURCE>the release on GitHubRead the manifest from a path or http(s) URL
--repo <OWNER/NAME>ShortyPing/snagRepository 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

CodeMeaning
0All selected tests passed (or none were selected)
1At least one test failed or timed out
2The 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:

VariableWhenEffect
SNAG_NO_UPDATE_CHECKrun timeAny non-empty value disables the update check and its notice
SNAG_GIT_COMMITbuild timeSets 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.