CLI
Commands, flags, output formats, and exit codes.
The e2e command scaffolds a project, runs, lists, and explores tests,
manages the replay cache, subscription logins, and telemetry, and upgrades
itself. Every command takes --help.
The CLI needs Node.js 24.8 or newer, or 22.22.3 or newer on Node.js 22. Any
package manager installs it: npm, pnpm, Yarn with or without Plug’n’Play, or
Bun. The CLI runs on Node.js, not on the Bun or Deno runtimes; bunx e2e
runs it on Node.js. Under Yarn Plug’n’Play on Node.js 24.8 through 24.11.0, a
.cts file fails to load: Node.js rejects the source Yarn’s loader hands on.
24.11.1 and later load it.
Reference
Help and version
- Output is colored on a terminal or in CI, and plain when piped or when
NO_COLORis set.FORCE_COLORkeeps the colors. e2ewithout a command prints the help on stderr and exits 2.- On a terminal 55 or more columns wide,
e2e --helpopens with the e2e wordmark: the site’s lettering in block characters. Piped output and narrower terminals get the text alone. - When a newer
e2eis on npm, a command run on a terminal ends with the version and thee2e upgradecommand for the project’s package manager. The registry is checked once a day. Nothing prints in CI, when stderr is not a terminal, fore2e mcpande2e upgrade, or withE2E_UPDATE_CHECK_DISABLED=1,NO_UPDATE_NOTIFIER=1,E2E_TELEMETRY_DISABLED=1, orDO_NOT_TRACK=1set.
Commands
An option with no default listed is off unless given.
e2e init
Scaffolds a project and adds e2e to devDependencies. It skips existing
config and test files.
- A flag answers its question and skips that prompt.
--engine,--gateway, and--endpointapply only to a new config. With--examplethey are a usage error.
Prompts
On a terminal, the wizard opens with the e2e wordmark, written letter by
letter over about a second, the way a pen writes it. With --yes, in CI, on
a dumb terminal, or on a terminal under 11 rows, it prints at once. Piped
output never shows it.
For a new project, the wizard asks for:
- The engines. Select one, both, or none. Web (Playwright, the
default) adds
@e2e-dev/web. Mobile (iOS/Android) adds@e2e-dev/mobile, defaults to iOS on macOS and Android elsewhere, and opens Settings. Selecting none adds no engine, for HTTP tests or your own. - A model provider or subscription for agent steps: Vercel AI Gateway,
OpenRouter, an OpenAI-compatible endpoint, ChatGPT, GitHub Copilot, OpenCode
Console, or SuperGrok. None is for tests that use only locators. The wizard
adds the required packages and model config. Subscription users then sign
in with
e2e login. See Models and Subscriptions. - Where to install the agent skill (see e2e guide).
.agents/skills/serves Codex, Cursor, Copilot, Gemini CLI, OpenCode, and Zed;.claude/skills/serves Claude Code. Both are preselected. - Where to register the MCP server.
.mcp.jsonfor Claude Code,.cursor/mcp.jsonfor Cursor. Both are preselected.
After you confirm the file changes, the wizard asks whether to install the
selected dependencies. The installer follows the project’s packageManager
field or lockfile, then the invoking package manager, then npm.
Files
Starting from an example
--example copies one of the
example apps into a
new or empty directory, then runs the wizard in its e2e project. That is
the directory itself for a web or Expo app, and its e2e/ folder for a
native app.
- The CLI downloads the example from GitHub, from the
e2e@<version>tag of the running CLI. When that tag doesn’t exist, it usesmainand warns. - A directory that holds only
.git,.DS_Store, orThumbs.dbcounts as empty. - The example’s config, tests, and scripts stay as they are. The wizard
asks only for the skill, the MCP server, and the install, and adds missing
.gitignoreentries. For a native app these files go ine2e/, so open your coding agent there. - The e2e project’s
package.jsontakes the directory’s name (with-e2ebeside a native app).e2e,@e2e-dev/web, and@e2e-dev/mobileget the rangesinitwrites for a new project. - The closing
Next:line names the example’s test script. A native app needs a build first; itsREADME.mdsays how.
Re-running init
- An existing config is kept, and no packages are added for it.
- MCP registration keeps extra settings on the e2e server, such as environment variables and client timeouts, even when it repairs the launch command. A current registration stays byte-for-byte unchanged.
- Once a skill directory exists, later runs refresh copies that differ from
the installed version. They never add a directory the project did not
choose.
--skillsand--mcpname the exact locations instead and leave the others as they are. - A
.claude/skills/e2e/copy from an earlier version becomes the symlink when it holds only the shipped files. A copy with other files stays a copy and is refreshed. initnever writes through a skill directory that is a symlink elsewhere, or sits under one.--yesskips it with a warning naming the link and its target. An interactive run offers to replace it with a copy, or with the link where the link goes.- A copy with a directory where a file goes, or a file where a directory
goes, is skipped with a warning naming the entry.
initremoves nothing to repair it. - An MCP config file reached through a symlink, or under a linked directory, is skipped with the same warning and left unchanged, so the server is not registered there. A plain config is merged as usual.
e2e login
Signs in to a subscription and stores the credentials in
$XDG_CONFIG_HOME/e2e/oauth.json, by default ~/.config/e2e/oauth.json. See
Subscriptions for the model configuration.
e2e logout
Forgets the stored login of one provider.
e2e models
Asks the vendor which models the stored login serves and prints one line per
model. For OpenCode Console it asks with OPENCODE_API_KEY when set.
Each line holds:
- the id
chatgpt(),copilot(),opencodeConsole(), orgrok()takes; - the vendor’s name for the model;
- its notes: reasoning levels, vision, preview, hidden; for Copilot
not enabledorno chat or responseswhencopilot()cannot call it; for OpenCode Console the plan that serves it, Zen or Go.
Only a call finds out whether a Copilot account can use a listed model.
See Pick a model.
e2e guide
Prints the skill that init installs; npx skills add tester-army/e2e
installs the same skill. See Coding agents.
e2e mcp
An MCP server on stdio that gives a coding agent a live session on the app.
The MCP server page covers these options in full, plus the tools and the session rules.
e2e run
Runs the tests the config discovers and writes report.json under the
output directory.
Positional arguments
Test files, directories, or globs relative to the project root:
- Lines. A line that names one member of a serial group runs the group
whole. Several lines of one file add up; the file without a line selects it
whole. When no test is declared at a named line,
NO_TESTSnames the positional and the lines the file declares tests at. A positional that exists on disk as written is a path, even when it ends in:12. - Globs. Braces, character classes, and extglobs are
INVALID_GLOBwith a hint, not a pattern that matches nothing. There is no expansion, so name one glob per alternative../,.., a trailing/, and an absolute path inside the project resolve as for any positional path. An existing file or directory is selected by its path, whatever characters its name holds. - Positionals narrow the config globs and the tag, platform, and capability
filters; they never bypass them. Names and globs are case-sensitive on
every OS. A path outside the project root is
COLLECTION_ERROR. - The runner imports every discovered test file, even when you select a few,
so a selected test finds its session setup in another file. Other ordinary
tests stay unselected. A collection error fails the run when it is in a
selected file, or in any file of a run with no positionals. A file the
positionals left out that fails to import or register its tests is skipped
with a notice. So a half-written file elsewhere does not stop
npx e2e run tests/login.e2e.ts. If a selected test’s session then has no setup test, the error names that file as where the setup may be. - A positional that starts with
-and names no file is a usage error, exit 2.pnpm test:e2e -- --headedproduces it: the--reaches e2e, which would otherwise take--headedfor a file. The message names the direct form,pnpm exec e2e run --headed. For npm it isnpm exec -- e2e run --headed; npm needs the separator before the binary to pass its flags through. - When nothing is left to run,
NO_TESTSnames the cause: no file matched the globs (with look-alikes such astests/login.test.ts), a positional matched nothing (with the nearest path), a matched file registered no tests, or every test was filtered out.
Flags
Selection
Execution
Output
--trace and --video apply over the config’s and every target’s value; a
test’s own trace or video still wins. A bare flag is on: bare --trace
keeps a page for every test. The value is optional and greedy:
--trace tests/a.e2e.ts reads the file as a mode and fails. Write
--trace=<mode>, or put test files before the flag.
The removed --artifacts <dir> fails with the --output it maps to:
--artifacts out/artifacts is --output out, and a directory whose parent is
the project root or outside it maps to --output .e2e.
--last-failed selects the tests the previous run did not pass. It reads them
from the report.json this run overwrites, <output>/report.json (--output,
else the config’s output):
- Tests that failed, timed out, or were interrupted.
- Tests skipped because a setup, a serial predecessor, a hook, or the worker
failed, or because
--max-failuresor--max-durationstopped the run first. - Every test a failed
beforeAllorafterAllselected in its describe or file, on that target; for a setup, the tests that consume its session. A rerun that runs the hook again reports it again if it still fails. A rerun that runs any test of its scope without it failing resolves it. - A test another filter (
--grep,--tag,--target,--agent, a positional) leaves out of the rerun stays owed. The rerun’s report carries its last result, and the hook failure whose scope holds it, underrun.carried. The next--last-failedselects it again, and the@e2e-dev/githubpage stays red until it passes (⏹️ for a test the run before interrupted). The rerun’s own exit code and summary count only what it ran. - The rerun keeps the artifacts the previous report names and writes its own
under each test’s
rerun-<n>/, so no attempt overwrites an earlier one’s files. - A test is named per target and agent.
- No report is
NO_LAST_RUN. When nothing failed in the previous run, it selects nothing:NO_TESTSunless--pass-with-no-tests.
Out-of-range --retries and --workers fail with INVALID_CONFIG before
anything starts, as the config value would. --target, --tag,
--exclude-tag, and --agent take each name once. An empty value (--tag '',
or --tag "$TAGS" with the variable unset) is a usage error, exit 2, never a
wider selection. An empty --reporter is the same usage error, never a run
with no reporters.
Filters only narrow, and each applies after the one before: positionals, then
.only, --tag, --exclude-tag, --grep, --grep-invert, --last-failed,
then the platform and capability rules, then --shard. When they leave
nothing to run, NO_TESTS counts the tests each filter removed and names the
tags and patterns. It marks each tag no test declares and names the nearest
declared one. With --changed-since, the configured test filters
run last.
Debug output
--debug appends two tables to stderr. Phase timings are summed across the
runner and every worker. The agent step table lists every agent step in
execution order: its per-phase split, model calls, tokens, and cost when the
provider reported one.
cached is the share of input tokens the provider served from its prompt
cache, - when it reports no split. The report records the same per step
(model.cacheReadTokens, model.cacheWriteTokens) and per run
(usage.modelCachedTokens).
Output
Every run that gets to its tests writes, under the
output directory (.e2e by default):
report.json, for scripts, whatever the reporters are;- the trace pages its
tracemode keeps, in each test’s directory underresults/; - the reporters’ files beside the report.
A run that stops before its tests writes none of them and leaves the previous
run’s files in place. That covers a config or collection error, NO_TESTS,
NO_LAST_RUN, UNSUPPORTED_ARTIFACT, an app command that fails to start, and
an interrupt. Its errors are on the terminal and in the exit code. A
junit.xml or summary.md that could not be written is a line on stderr,
never a run error.
run.summary in the report counts the results:
discoveredis every test-target pair the runner collected, andselectedthe ones this run chose. The left-out pairs are inresultsasselected: false, but in no other count.passed,failed(timed out included),interrupted,flaky, andskippedcount the selected results and add up toselected.interruptedis a test the run stopped before its verdict (a signal,--max-failures,--max-duration, or a run-level error such asMODEL_UNAVAILABLE), never a failure.executedis the selected results that ran at least one attempt.
With --ai-trace, the run also writes <output>/ai-trace.json, an AI SDK
devtools database. It holds one entry per model round trip: the exact prompt,
tool definitions, response, usage, and provider metadata. Observations in it
are already redacted, and inline image and file bytes become byte counts.
Open it with unbox-ai:
npx unbox-ai runs .e2e/ai-trace.json lists its runs. See
Debugging a run. A test filter’s model
calls are not recorded.
Watch mode
--watch runs the selection, prints the summary, and waits instead of
exiting.
- A saved test file (one the config’s
testsglobs match) runs again alone and whole, in place of the positionals. - A saved source file near the tests (a helper or a fixture:
.ts,.js, or.jsonunder the directories the globs name, or beside the config) or a saved config reruns the selection. - Both keep the command line’s other filters and the
pandtfilters. - Changes during a run wait for it and run next, together. During the first run only the config and its directory are watched, since the test directories come from the config.
- On a terminal the screen clears before each rerun, and a banner names the trigger.
- Each rerun reads the project’s files afresh, including the helpers a test imports.
On a terminal, keys drive it as in vitest:
- During a run, any of these keys (and space or
c) cancels it, as an interrupt would. A second one forces it, and the session waits again. - Ctrl-C or Ctrl-D interrupts the run and ends the session, from inside a
portprompt too. Escape does the same outside a prompt, and inside one only closes the prompt. Ctrl-Z suspends the process as usual. - Every rerun is a complete run: once the config loads, it writes
report.jsonand the reporter files, and starts and stops the app command, unless it has nothing to run. To keep a dev server up across reruns, start it yourself and setreuseExistingon the command. --last-failedand the test filters apply to the first run only, so a rerun never calls a filter’s model; a rerun that selects nothing passes empty.- On quit, the exit code is that of the last run that finished.
- Without a terminal on stdin, only file changes rerun.
--watch is not for CI: a process that never exits on its own hangs the job,
so it is refused when CI is set. It is also refused with --reporter json, and
exits 2 after the first run when the config’s reporters include json.
Signals
Signals escalate.
- The first
SIGINTorSIGTERMinterrupts. The running test endsinterrupted; itsafterEachhooks and session close run within the cleanup budget, and every worker disposes its engine. Tests not started are skipped with causeinfrastructure-unavailableand the reasonrun interrupted before execution. So the report lists every planned test, and--last-failedreruns the interrupted and the unstarted ones. The runner writes the report and exits 130. An interrupt before any test starts (during collection, an engine’sprepare, or app startup) writes no report and leaves the previous one in place. - A second signal forces. Workers dispose their engines at once and are
killed when the cleanup budget is spent. The report is still written,
and so are
junit.xmlandsummary.mdwhen thejunitandmarkdownreporters are on; a custom reporter’sonRunFinishedis abandoned. - A third signal exits on the spot, after killing the process groups of
every app command the run started. A process attached through
reuseExistingis left alone.
A worker whose runner disappears disposes its engine and exits within the cleanup budget.
e2e explore
A run with no test file. The agent gets a goal, plans one exploration step at
a time, reports defects through its report_finding tool, and ends with an
assessment. See Exploring without a test.
For --trace and --video, a bare flag is on, and retain-on-failure
keeps the page or video only when the exploration did not pass. The
exploration is one attempt, so a retry mode records nothing, and a plan-time
notice says to pass --trace on (or --video on). Put the goal before the
flag, or write --trace=<mode> or --video=<mode>.
Report
- The run has one result, under the virtual file
explore, titled by the goal. - The exploration opens the app first: the target’s
app.url, or on a device the pinned app, launched fresh. It installs no build, so a local device target needsapp.bundleId. - With
--session, the report also holds the setup test’s result. It lists the project’s other tests as skipped with causefiltered, asrunlists the tests of a file no positional named. - The exploration runs with the replay cache off and zero retries, whatever
the config says. A
--sessionsetup test runs as it would forrun, with the config’s agents, cache, and retries. <output>/report.jsongainsrun.explore: the goal, the budgets, why the run ended (finished,step-limit,time,stuck, oraborted), the assessment, the steps, and the findings.- Artifacts go to a directory named from the goal’s first words and a digest
of the goal, target, and agent, such as
<output>/results/explore-check-the-cart-totals-1a2b3c4d5e6f7a8b/. A rerun of the same goal on the same target and agent writes to the same directory.
Step budgets
Each step has its own budget: 40 actions and 40 model calls, unless the agent
sets maxSteps or maxModelCalls (1 to 100). A step that reaches its action
or time budget is time-boxed, and its findings still count. A failed step
does not end the run. Three failed or blocked steps in a row with no new
finding do (stuck).
Findings
- A finding of kind
issueis a functional defect; awarningis cosmetic. - Severity runs from
trivial(1) tocritical(5). The report lists issues before warnings, the most severe first. - Each finding has its location, the expected and observed behavior, the steps to reproduce, and a screenshot when capture is allowed and succeeds. After a secret fill, findings carry no screenshots; see screenshots after a secret fill.
Outcomes
e2e list
A dry run: list selects tests exactly as run does, prints one line per
test-target pair, and exits. It starts no app process, engine, or worker, and
writes nothing.
Pairs a selection flag removed are not listed. list from e2e/runner
runs the same selection in process and also returns the removed pairs.
e2e cache
Inspects or clears the replay cache. Each subcommand loads the discovered
config or --config <path>, reads the directory a run would, and exits 0 on
an empty store.
- An entry recorded before entries carried provenance prints
-in those columns and still replays. - A file the store cannot read (truncated, over the 1 MiB ceiling, or from a
newer schema) is counted separately on stderr, treated as a miss by a run,
and removed by
clear. - Files the runner never wrote are left alone.
These commands read the file store only. With a custom cache.store they
exit 2 and say so.
e2e upgrade
Upgrades e2e to the latest release, a dist-tag, or one version. It also
moves the @e2e-dev/* packages in the same package.json, and installs
them with the project’s package manager. e2e update is the same command.
npx e2e runs the installed version. When that version has no upgrade
command yet, run npx e2e@latest upgrade one time.
- The project. The nearest
package.jsonfrom the working directory up that listse2e. - Sibling versions. Each
@e2e-dev/*package gets its release under the same dist-tag (latestfor a named version), when that release’s peer ranges accept the new versions. Otherwise it gets its newest release that does. When none does, the command exits 2. A deprecated release is never picked, and a prerelease only whene2egets one. An integration follows the engine it plugs into. - The package manager. The one the project’s lockfile or
packageManagerfield names, or pnpm at a pnpm workspace root. A workspace member with neither uses its workspace root’s. A lockfile above a project that no workspace lists doesn’t count. - The install. The new specs go into
package.json, each in the section it was in, then oneinstallresolves them together. pnpm gets--no-frozen-lockfile, and YarnYARN_ENABLE_IMMUTABLE_INSTALLS=false, so the lockfile can change in CI too. Ctrl-C reaches the package manager; the command waits for it to stop. - A failed install.
package.jsonand any lockfile the install changed go back as they were.node_modulescan keep some new versions until the next install. - The saved range. A named version or a prerelease is pinned exactly. A
dist-tag keeps the
^or~of the old spec. - Left alone. A package linked with
workspace:,file:,link:,catalog:, a path, or a git URL, and an@e2e-dev/*package the registry doesn’t have. Whene2eitself is linked, the command exits 2. - Nothing to do. When every installed version already matches, the
command installs nothing. Without
node_modules, as under Yarn Plug’n’Play, it compares the specs inpackage.jsoninstead. - The registry.
npm_config_registrywhen the package manager sets it, elsehttps://registry.npmjs.org/. - The agent skill. A copy in
.agents/skills/e2eor.claude/skills/e2eis updated to the installed version’s after the install, and when everything was already up to date. A.claude/skills/e2esymlink to the.agents/skillscopy follows it. Files the new skill no longer ships stay. A symlink to a copy outside the project isn’t written through, and withoutnode_modules, as under Yarn Plug’n’Play, the command namese2e initinstead.
e2e feedback
Sends one report about e2e itself to the e2e team: something broke, the docs
or the skill misled, or a capability is missing. A coding agent may send one
when it hits a problem with e2e, not with the app under test. It first shows
the user the --dry-run output and gets their agreement.
--message takes at most 2000 characters, --command and --agent 200, and
--task, --expected, --actual, --repro, and --approach 1500.
- A bug needs
--repro. Without it, the command sends nothing and prints a prefilled GitHub issue instead, where the e2e team can ask for a reproduction. Add a minimal one and submit it yourself. - Outdated releases are refused. Before sending, the command asks the npm
registry for the latest e2e release. When yours is older, it exits 2 and
sends nothing, since the problem may be fixed already. Upgrade with
e2e upgrade, check that it still happens, and send it again, or pass--allow-outdated. - Where it goes. One
e2e_feedbackevent, to the same PostHog project as telemetry, with the anonymous machine facts every telemetry event carries. - Opting out. Feedback is sent only when you run the command, so a saved
e2e telemetry disabledoes not stop it.E2E_TELEMETRY_DISABLED=1andDO_NOT_TRACK=1do: with either set, the command exits 2 and sends nothing. So a CI job or a fleet that opted out sends nothing, even when an agent runs it.E2E_TELEMETRY_DEBUG=1prints the event like--dry-run.
Before sending, the value of every environment variable whose name holds
KEY, TOKEN, SECRET, PASS, _PWD, CREDENTIAL, AUTH, COOKIE, or
PRIVATE becomes <secret:NAME> when it is six or more characters long. That
is the minimum for a config secret; a shorter one would rewrite ordinary
words. Bearer tokens, URL credentials, private keys, JWTs, and common API key
shapes become <redacted>. Do not paste app data, page content, or test
files.
e2e telemetry
Shows whether anonymous usage telemetry is on, and switches it. Telemetry lists every property sent.
E2E_TELEMETRY_DISABLED=1andDO_NOT_TRACK=1turn telemetry off without touching the file.E2E_TELEMETRY_DEBUG=1prints every event to stderr and sends nothing.- The first command on a machine, other than
e2e initande2e telemetry, prints a notice one time. In CI nothing is printed and no file is written.
Exit codes
- Precedence for a mixed run is
130 > 4 > 3 > 2 > 1 > 0. The report keeps every individual result; precedence affects only the process exit code. - Argument parsing failures exit 2 with a one-line error on stderr.
Diagnostics
- A message that rejects a name suggests the nearest valid one.
- Keys from other runners’ configs (
testDir,baseURL,webServer,projects) say where that fact lives here. - A failing
importin the config or a test file ends with the install or add command for the project’s package manager, or the subpaths the package exports. - A model credential the provider rejects names the variable the provider reads.
