@verirun/cli
v0.2.5
Published
Record a browser workflow, replay it, and capture the Supabase / Vercel / Railway logs your backend produced.
Downloads
1,331
Readme
verirun
Record a browser workflow, replay it, and capture the Supabase, Vercel + Railway logs
your backend produced during the replay. Plus verirun map: a static
interaction graph of your React/TypeScript app — every page, what you can click
on it, and which backend calls those clicks make — without running anything.
Install
npm install -g @verirun/cliThat installs the CLI and, in a postinstall step, downloads the Chromium build
Playwright drives. verirun is then on your PATH. To also record in Firefox or
WebKit: npx playwright install firefox webkit. To skip the browser download
(you already have Playwright browsers, or you are in CI): set
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 before installing.
verirun operates on the directory you run it from: it reads and writes
.verirun/ at the root of that repo (the nearest ancestor with a .git or, if
none, package.json; otherwise the current directory). cd into the project
you want to test before recording or running.
Help
verirun --help and verirun <command> --help work everywhere. A verirun(1)
man page ships with the package: a global npm install -g @verirun/cli puts it on
your MANPATH (man verirun) on macOS, Linux, and WSL / Git Bash. Native
Windows (cmd, PowerShell) has no man — use --help, or man -l the file
under a POSIX shell. From a npm link checkout: man -l man/verirun.1.
Account
record and run need a connected Verirun account. First run on a machine:
verirun loginThis prints a short code and a link. Open the link, sign in (a 7 day free trial starts automatically if you are new), and enter the code. The CLI then holds a device token for that machine.
verirun whoami # account, plan, subscription status
verirun logout # disconnect this machine| | |
| --- | --- |
| ~/.verirun/config.json | Holds the device token. Per user, not per project. Restricted to your account (0600, or an icacls ACL on Windows). |
| ~/.verirun/state.json | The last server-signed lease (Ed25519) plus a local tamper check. Editing it, or a lease not signed by Verirun's key, is ignored and forces a fresh online check. |
| $VERIRUN_LICENSE_KEY | A raw key for CI, used instead of verirun login. |
| $VERIRUN_LICENSE_SERVER | Point the CLI at a different server. |
| $HTTPS_PROXY / $HTTP_PROXY / $NO_PROXY | Honoured for every request the CLI makes. Needed on networks that force traffic through a proxy — Node does not read these on its own, so without them the CLI cannot connect where your browser can. |
| $NODE_EXTRA_CA_CERTS | Path to your organisation's CA bundle. Needed when a proxy inspects TLS, which otherwise fails certificate verification. |
Behind a proxy? If verirun login cannot connect on a network where your
browser works, that is almost always a proxy: set HTTPS_PROXY (and
NODE_EXTRA_CA_CERTS if the proxy inspects TLS). The CLI names the underlying
error — ENOTFOUND, ECONNREFUSED, a certificate failure — so you do not have
to guess which it is.
Revalidation. Each successful check hands back a short-lived lease signed by
the license server; the CLI verifies it against a public key built into the
binary, so a fake or patched server cannot mint one. A lease is trusted for ~3
days (by its own signed timestamp), so most runs need no network. After that the
CLI re-checks. An explicit "expired", "canceled", "revoked" or "seat limit"
answer stops record/run immediately. A network failure never counts as
invalid: the CLI keeps working off the last lease, silently for 15 days, then
with a "connect to the internet" notice every run, and stops at 30 days offline
until it can revalidate. The server can also refuse very old CLI versions.
Hardware binding. Each check sends a fingerprint of this machine — a hash of
the identifier your OS assigns its own installation (/etc/machine-id on Linux,
IOPlatformUUID on macOS, MachineGuid on Windows), plus platform, architecture
and OS user. The raw values never leave the machine. Because it is anchored on
the install rather than on your network hardware, docking a laptop, changing
Wi-Fi, toggling a VPN or renaming the machine all leave it unchanged.
On a system that exposes no such identifier the CLI falls back to hashing the physical MAC addresses and hostname, which can shift when the network changes and would then take a second seat.
Your plan includes a set number of machine seats; connecting a new machine past that is blocked until you release one from https://get-verirun.duely.in/account. Reinstalling the OS issues a new identifier, so that does count as a new machine — release the stale one.
verirun --help, verirun --version, verirun log and verirun agent do not require an account.
verirun map does.
verirun record <name>
verirun record checkout --url https://app.example.comOpens a headed browser. Perform the workflow by hand — click, type, navigate.
Close the browser to finish; the step sequence is saved to
.verirun/workflows/<name>.json.
| Flag | Meaning |
| --- | --- |
| -u, --url <url> | URL to open at. Without it the browser starts blank and you navigate yourself. |
| -b, --browser <name> | chromium (default), firefox, or webkit. |
| --channel <name> | Record in an installed browser instead of bundled Chromium: chrome, msedge, chrome-beta, msedge-dev, … Saved into the workflow file so run reuses it. |
| --default-browser | Record in your OS default browser. Chrome / Edge → that install (--channel); Firefox → Playwright's Firefox; Safari, Brave, Arc, Vivaldi, Opera → nearest engine (bundled Chromium or WebKit) with a warning. Falls back to bundled Chromium if detection fails. Overrides -b / --channel. The resolved browser/channel is saved to the workflow, so run reuses it. |
| --user-data-dir <dir> | Record in a persistent profile directory, so the session is already logged in and stays logged in for later runs. The real browser using that profile must be closed first. |
| --test-id-attribute <attr> | Attribute preferred for test-id selectors. Defaults to data-testid; also settable as testIdAttribute in config. |
| -f, --force | Overwrite an existing workflow of the same name. Without it, recording onto an existing name errors. |
Name rules. <name> becomes a filename: letters, digits, dot, dash,
underscore; must start with a letter or digit; 64 chars max. Two Windows rules
are enforced on every platform, so a workflow you record stays usable for a
teammate who checks the repo out on Windows: no trailing dot (Windows drops it,
colliding with the bare name), and no reserved device stem — con, prn, aux,
nul, com1–com9, lpt1–lpt9, at any extension. Names are not sanitised —
an invalid name errors rather than being rewritten.
Selectors. Recording goes through Playwright's codegen engine, so each step
gets its stable selector — getByTestId, then getByRole with an accessible
name, falling back to CSS only when nothing better exists. A step stores three
views of its target:
{
"index": 3,
"action": "fill",
"params": { "text": "[email protected]" },
"selector": "internal:role=textbox[name=\"Email\"i]",
"locator": { "kind": "role", "body": "textbox", "options": { "name": "Email" } },
"description": "getByRole('textbox', { name: 'Email' })"
}selector is the raw string replay feeds to page.locator(), locator is its
structured form, description is for you to read. Replay uses selector, so
what you recorded is what runs.
Blur quirk. A text field is only committed when it loses focus. If you type into a field and close the browser without clicking or tabbing away, that last value is not recorded. Tab out before closing.
Dialogs. Answer alert / confirm / prompt boxes in the recording
browser as you normally would. Codegen notes that a step opened one but not
which button you pressed, so after you close the browser record asks, per such
step, [O]K / [c]ancel (Enter = OK; without a terminal every answer is OK). The
answer is saved on the step and run replays it:
{ "index": 4, "action": "click", "selector": "…", "dialog": { "action": "dismiss" } }action is accept (OK) or dismiss (Cancel). For a prompt(), add
"promptText": "…" — without it, accepting submits the prompt's default value.
Every dialog the step opens gets the same answer.
Nothing recorded. If you close the browser without doing anything, nothing is saved and the command says so.
Recording or replaying as a signed-in user
Point both commands at a Chrome profile that is already logged in:
verirun record checkout --channel chrome --user-data-dir "$HOME/.verirun-profile"
verirun run checkout --channel chrome --user-data-dir "$HOME/.verirun-profile"On Windows pass an absolute path instead — cmd and PowerShell don't expand ~
inside an argument like this, and verirun never expands it either, so a literal
~ just creates a directory called ~ and the run starts logged out:
verirun run checkout --channel chrome --user-data-dir "$env:USERPROFILE\.verirun-profile"Create that profile once, by hand, and sign in there — identity providers block sign-in in an automation-launched browser:
# macOS / Linux
google-chrome --user-data-dir="$HOME/.verirun-profile" --password-store=basic# Windows (PowerShell)
& "$env:ProgramFiles\Google\Chrome\Application\chrome.exe" `
--user-data-dir="$env:USERPROFILE\.verirun-profile"--password-store=basic (plus --use-mock-keychain on macOS) is not optional:
Playwright always launches Chrome with those flags, and cookies written under
your OS keyring can't be decrypted without them — the run starts logged out with
no error. Both switches are Unix-only, so on Windows they're ignored — there just
create the profile as the same Windows user that runs verirun. The Windows path
is untested: if a run starts logged out despite a signed-in profile, that's the
first thing to suspect. On every platform, don't reuse your everyday Chrome
profile; use a dedicated one. Close the profile before recording or running, since
Chrome locks it.
To check it took, reopen the profile on its own — if that window is signed in, verirun will be too.
verirun run <name>
verirun run checkoutReplays the recorded steps — headless, in the same browser kind the workflow was
recorded in — then captures logs for the wall-clock window the replay occupied
and writes .verirun/runs/<name>-<timestamp>/logs.json.
| Flag | Meaning |
| --- | --- |
| --slow-mo <ms> | Pause before each action. Defaults to 500 ms when --headed so you can follow the run by eye, and to 0 otherwise. --slow-mo 0 watches at full speed; a bigger number follows a fast form more easily. |
| --headed | Show the browser window instead of running headless. |
| --timeout <ms> | Per-action timeout, milliseconds. Non-negative. Defaults to Playwright's 30000. |
| --channel <name> | Replay in an installed browser: chrome, msedge, … Overrides the channel saved in the workflow. Chromium family only. |
| --user-data-dir <dir> | Replay in a persistent profile directory, so it runs already logged in. The profile must not be open in another browser. |
| --cdp <url> | Attach to a browser already running with remote debugging and drive it. See below. |
| --user <id> | Keep only log entries whose message or raw payload contains this string, case-insensitive — e.g. a user's email. Overrides config.userFilter. Recorded in logs.json as "filter": { "user": … }; entries and the per-source counts are post-filter. If everything is filtered out, run warns. |
| --dialog <answer> | How to answer a browser dialog opened by a step with no saved dialog answer: accept (OK, the default) or dismiss (Cancel). A step's own dialog always wins. |
Where the user filter runs. For Supabase it goes into the log query's
WHERE (matched against the rendered message and the JSON of metadata), so
only that user's rows leave the project — this matters on a busy database. Plain
identifier needles (emails, UUIDs, usernames) qualify; a needle with quotes,
spaces or other symbols isn't spliced into SQL and is filtered client-side
instead (a dim note says so), as is a source whose backend rejects the WHERE.
For Railway it's passed as a quoted phrase to the GraphQL log filter, same
idea. For Vercel there is no server-side option — a Log Drain just pushes,
with no content predicate — so it's always filtered client-side after delivery.
Volume there is bounded by the ~seconds-long run window, not your user count; if
it's still too much, point the drain at one project or a staging environment, or
set a sampling rate on the drain in Vercel. Every source is also run through the
client-side match as the canonical pass.
Target selection is first match of: --cdp, then --user-data-dir, then
--channel (or the workflow's saved channel), then a fresh Playwright browser
of the workflow's kind.
Running against a browser you already have open
google-chrome --remote-debugging-port=9222 # keep this window open
verirun run checkout --cdp http://localhost:9222run attaches over CDP, reuses the existing context (cookies, auth, open tab),
replays, and on finish disconnects without closing your browser. Only
Chromium-family browsers expose CDP. If the workflow's first step is a
navigate it drives the current tab there; otherwise it acts on whatever tab is
frontmost.
Replay behaviour
- Steps replay off the raw
selector, so the recorded target is reproduced exactly. - Supported actions:
navigate,click,dblclick,fill,press(modifier bitmask + key are recombined),check,uncheck,select,setInputFiles,openPage,closePage. - Assertions added in codegen (
assert*) are skipped with a warning — they do not change which actions run. - An action the replayer does not recognise stops the run.
- A step that fails does not abort log capture: the run is marked
failed, whatever logs already landed are still written, and the exit code is non-zero. - Popups and newly opened tabs become the active target as they open. A workflow that drives several tabs at once is not supported yet.
- Browser dialogs (
alert,confirm,prompt,beforeunload) are answered automatically: with the step's saveddialogif it has one, otherwise with--dialog(defaultaccept). Each one is printed under its step and listed inlogs.jsondialogs. With--cdp, only tabs the run itself drives or opens are touched.
Sequencing
Start every configured log source → replay → collect from each source for the
[start, end] window → dispose every source. Dispose always runs, including
after a replay failure or a source that failed to start. If one source fails to
start, the run aborts before replay; if one fails during collection, it is
warned about and contributes no entries.
logs.json
{
"schemaVersion": 1,
"workflow": "checkout",
"status": "ok",
"window": { "startedAt": "2026-08-31T13:01:50.998Z", "endedAt": "2026-08-31T13:01:51.499Z" },
"steps": { "total": 12, "replayed": 12 },
"filter": { "user": "[email protected]" },
"dialogs": [
{ "step": 7, "type": "confirm", "message": "Delete this item?", "action": "accept", "at": "2026-08-31T13:01:51.102Z" }
],
"sources": [{ "id": "vercel", "name": "Vercel", "entries": 8 }],
"entries": [
{
"source": "vercel",
"stream": "lambda",
"timestamp": "2026-08-31T13:01:50.000Z",
"level": "info",
"message": "POST /api/checkout 200",
"raw": {}
}
]
}statusisokorfailed. Onfailed,stepsalso carriesfailedAt(the step index) and the file carries a top-levelerrorstring.filteris present only when--user/config.userFilterwas in effect;entriesand everysources[].entriescount are then post-filter.dialogslists every browser dialog the replay answered, in order: thesteprunning when it opened, itstypeandmessage, the prompt'sdefaultValue(prompts only), theactiontaken, and thepromptTextsubmitted to an accepted prompt. Empty when none opened.entriesmerges every source, sorted bytimestamp.streamis the sub-stream within a source (lambda,edge,postgres_logs, …).rawis the provider's untouched payload (for Supabase that now includes the row'smetadata, so a--userfilter can match a nested email field).- With no log sources configured,
runstill replays and warns;entriesis empty.
verirun log [source]
verirun log # overview: which sources are configured
verirun log supabase # connect Supabase logs (interactive)
verirun log vercel # connect Vercel logs (interactive)
verirun log railway # connect Railway logs (interactive)All three are connect flows, not pages of instructions.
verirun log supabase — if nothing is configured it opens
https://supabase.com/dashboard/account/tokens, asks you to paste the token you
create there, works out the project ref (from the supabase CLI if it's
installed, otherwise one prompt), writes both to .verirun/config.json, and
runs a test query to confirm it works. Already connected → one-line status.
verirun log railway — Railway has no HTTP drain, but its GraphQL API
serves point-in-time deployment logs, so this works like Supabase. Two things:
(1) create a token on the page it opens (https://railway.com/account/tokens);
(2) paste the URL of the service you want logs from — verirun pulls the
project / service / environment IDs out of it. The newest deployment of that
service is resolved on every run, so it survives redeploys. (Pin a specific one
with railway.deploymentId in config.) Logs are already scoped to that service
- environment, so there's no separate project filter. Already connected → one-line status.
verirun log vercel — same shape as the other two: it opens
https://vercel.com/account/settings/tokens, you paste a token, and it picks
the project. If the repo has a .vercel/project.json (i.e. you have run
vercel link), the project and team are read from it and you are asked
nothing else; otherwise it lists your teams and projects and you choose. It
then resolves the newest ready production deployment and reads its runtime logs
to confirm the token works. No tunnel, no drain, no second terminal.
On each run it re-resolves the newest ready deployment, so a redeploy doesn't
break the config. Pin one with vercel.deploymentId, or read preview
deployments instead with vercel.environment: "preview".
Vercel's runtime-log API is the easy path, but if it isn't available to you, the
original Log Drain flow is still there under verirun log vercel --drain: a
tunnel, a local listener, and a drain pointed at it (4-step checklist → tunnel →
--listen → add the drain with Sources: Edge + Serverless, Format: JSON,
Endpoint: <tunnel>/drain → paste the signing secret). Config keys
listenerPort, listenerPath, drainSecret, drainGraceMs, project belong
to this mode only. verirun uses drain mode whenever vercel.token is absent,
so the two never collide.
Non-interactive shells get the env/config values to set instead of a prompt.
| Flag | Meaning |
| --- | --- |
| --reconnect | Run the connect flow again and replace what was saved. Any source. |
| --test | Query the last 5 minutes and print what came back, or the error. Needs a connected source. Non-zero exit if it errored. |
| --drain | vercel only. Set up the old push-based Log Drain instead of the API. |
| --listen | vercel drain mode only. Start the Log Drain listener now and keep it running until Ctrl+C, printing each delivery as timestamp level stream message. Binds listenerPort + listenerPath from config (defaults 4319 / /drain). |
An unknown <source> errors. A flag meant for another source is ignored.
Each verirun log <source> connect flow writes its own block of
.verirun/config.json, restricted to your user account — mode 0600 on macOS and
Linux, and an icacls ACL granting only you on Windows, since mode bits mean
nothing there. If that cannot be applied the flow warns rather than leaving you
to assume the token is protected. The overview is read-only. A secret supplied via
$VERIRUN_SUPABASE_ACCESS_TOKEN / $VERIRUN_VERCEL_TOKEN /
$VERIRUN_VERCEL_DRAIN_SECRET / $VERIRUN_RAILWAY_TOKEN is never written to
the file.
verirun map
Reads this repository's React/TypeScript source and builds an interaction graph: one node per page, the interactive elements on it, the pages each one navigates to, and the backend calls each one makes. Nothing is executed — no browser, no server, no recording.
verirun map # per-page summary
verirun map --json # the whole graph
verirun map --report # graph + coverage.md into .verirun/map
verirun map --bfs /account --depth 2 # what is reachable from one page
verirun map --reach # how much of each page was capturedRoutes come from a react-router <Routes> declaration or a Next.js App
Router app/**/page.tsx tree ([id] → :id, route groups dropped, layout.tsx
applied to its subtree). Components a page renders from another file are
followed, so a page whose real UI lives in a client component is not reported
empty. Import aliases are read from tsconfig.json; add more with
--alias @/=src.
$ verirun map
6 pages · 6 backend effects · 31 edges (40 files)
/ — Page, 0 element(s), 10 nav, 0 effect
/account — AccountPage, 5 element(s), 1 nav, 4 effect
/login — LoginPage, 2 element(s), 2 nav, 3 effect--bfs takes a route path (/account), a page id (page:/account) or a page
component name, and reports each node once at its shortest distance, with the
element that gets you there:
$ verirun map --bfs /account --depth 2
page:/account
└─ nav "verirun" → page:/
└─ effect "revoke" [if window.confirm("Revoke this terminal's access? …")] → effect:POST ?
opens confirm "Revoke this terminal's access? …" — the answer decides what the click does
└─ nav "Log in" → page:/login
# frontier at depth 2: page:/login, page:/signupBrowser dialogs. An element whose handler calls the native alert(),
confirm() or prompt() (bare, or on window / globalThis; a locally
imported confirm is a custom modal, not a dialog) carries
dialogs: [{ type, message, messageStatic, gates, … }] in --json. gates is
true when the handler reads the answer, so OK and Cancel lead to different
outcomes. An early if (!window.confirm(…)) return; also shows up as the
guardSource of every backend call after it, which is how you see that Cancel
skips the POST. The per-page summary counts the elements that open a dialog,
--bfs prints each one under its edge, and coverage.md lists them all. A
workflow step that clicks one answers it with its dialog field (see
verirun run); workflow check warns when that field is
missing (see below).
Limits, honestly. It is static analysis: a target computed at runtime shows
as unresolved, an endpoint passed through a variable as ?, and Supabase
.auth.* / .storage.* calls are not yet classified as effects. --reach
tells you what fraction of each page it actually attached, so you can see when
it is missing something rather than guessing.
verirun workflow check <name>
Validates a workflow file without running it — JSON shape, schema version, known
actions, a selector where one is needed, the required params per action, and
warnings for the things that replay tolerates but you probably didn't mean
(a bare role=button that matches five elements, an assert* step that replay
skips, a first step that isn't navigate, and a navigate after the first
step — walking the app by URL skips the UI, so a dead link or a button that
stopped submitting still passes).
verirun workflow check login
verirun workflow check login --json # machine-readable findings
verirun workflow check ./some/file.jsonIf verirun map --report has saved .verirun/map/interaction-tree.json,
check also warns about a step that clicks an element the graph says opens a
browser dialog but has no dialog answer. Replay would press OK, which is
usually what you want, but when the handler reads the answer, Cancel is a
different path and should be chosen on purpose. Matching is by exact selector, so it catches
workflows written from map --json. A selector that elements without a dialog
also match (a bare role=button) is skipped rather than guessed at. Re-run
map --report when the app changes; the saved graph goes stale.
Static only: it never opens a browser, so it cannot tell you whether a selector
matches the live page. Only verirun run can. Non-zero exit if anything is an
error.
This exists because a workflow doesn't have to come from record. verirun map
--json gives every element a selector, a selectorConfidence
(high/medium/low) and a suggested action, which is enough for you — or
an agent — to write .verirun/workflows/<name>.json directly:
{
"schemaVersion": 1,
"name": "login",
"createdAt": "2026-09-20T00:00:00.000Z",
"browser": "chromium",
"steps": [
{ "index": 0, "action": "navigate", "params": { "url": "https://app.example.com/login" }, "signals": [] },
{ "index": 1, "action": "fill", "selector": "role=textbox[name=\"Email\"i]", "params": { "text": "[email protected]" }, "signals": [] },
{ "index": 2, "action": "click", "selector": "role=button[name=\"Log in\"i]", "params": {}, "signals": [] }
]
}Actions and required params: navigate (url, absolute), click /
dblclick, fill (text), press (key), check / uncheck, select
(values), setInputFiles (files). Everything but navigate needs a
selector, which is handed to page.locator() verbatim. A step that opens a
browser dialog can carry "dialog": { "action": "accept" | "dismiss",
"promptText"?: "…" }; without it run accepts.
A recorded workflow is ground truth; a written one is a guess until run
confirms it. Don't overwrite a recording with a generated file — use a new name.
verirun agent
Prints a single self-contained reference for an AI coding agent: every command
and flag (read from the CLI itself, so it can't drift), the .verirun/ layout,
the logs.json schema, the recommended record-then-run loop, and a snapshot of
this checkout — resolved repo root, configured log sources, recorded
workflows, recent runs.
verirun agent > .verirun/AGENT.md # save it as context for your agent
verirun agent --json # machine-readable form
verirun agent --skill --install # .claude/skills/verirun/SKILL.md
verirun agent --skill --install cursor # .cursor/rules/verirun.mdc
verirun agent --skill --install all # both--skill emits the same knowledge as an agent skill — frontmatter with a
description that tells the agent when to load it, then a shorter body aimed at
deciding which command to reach for. --install writes it where the agent
looks:
| Target | File | Loaded |
|---|---|---|
| claude (default) | .claude/skills/verirun/SKILL.md | automatically, when a task looks like app testing |
| cursor | .cursor/rules/verirun.mdc | when the task matches its description (an agent-requested rule, not always-on) |
| all | both | |
Re-run it after upgrading verirun; the file is generated, not hand-maintained.
For any other agent, paste the output into CLAUDE.md, an AGENTS.md, or pipe it
straight into a prompt. No account needed. The agent then knows to run
verirun run <name>, read exit code + .verirun/runs/<name>-<newest>/logs.json,
and act on status / error / entries without you explaining the tool.
.verirun/ layout
Everything lives at the repo root:
.verirun/
config.json # secrets and endpoints
workflows/<name>.json # recorded step sequence
runs/<name>-<timestamp>/logs.json # captured logs, one directory per runrecord appends .verirun/ to the repo's .gitignore the first time it runs.
If the repo has no .gitignore, it warns instead of creating one — run logs
contain request bodies, tokens and user data, and recorded steps contain
whatever you typed, including passwords.
Config — .verirun/config.json
Read once at startup. Optional; include only the blocks you use. Credentials in it are sent only to the respective provider's own API.
{
"testIdAttribute": "data-testid",
"userFilter": "[email protected]",
"supabase": {
"projectRef": "abcdefghijklmnopqrst",
"accessToken": "sbp_...",
"sources": ["postgres_logs", "edge_logs", "auth_logs"]
},
"vercel": {
"listenerPort": 4319,
"listenerPath": "/drain",
"drainSecret": "whsec_...",
"drainGraceMs": 3000,
"project": "my-app"
},
"railway": {
"token": "...",
"projectId": "...",
"environmentId": "...",
"serviceId": "..."
}
}| Key | Default | Meaning |
| --- | --- | --- |
| testIdAttribute | data-testid | Attribute for test-id selectors. --test-id-attribute overrides. |
| supabase.projectRef | — | Supabase project ref. Required for Supabase capture. |
| supabase.accessToken | — | Personal access token. Required for Supabase capture. |
| supabase.sources | postgres_logs, edge_logs, auth_logs | Log sources to query, one request each. |
| vercel.token | — | Vercel API token (vercel.com/account/settings/tokens). Its presence is what selects API mode over drain mode. |
| vercel.projectId | — | prj_… id of the project to read logs from. Required in API mode. |
| vercel.teamId | — | team_… id, when the project belongs to a team. |
| vercel.deploymentId | — | Pin log capture to one deployment instead of the newest ready one. |
| vercel.environment | production | Which target to resolve the newest deployment from: production or preview. |
| vercel.readMs | 8000 | How long to read the runtime-log stream before giving up. |
| vercel.tailGraceMs | 3000 | Extra window past the run's end to keep, for lines Vercel flushes late. |
| vercel.listenerPort | 4319 | Drain mode. Port the local Log Drain listener binds during a run. |
| vercel.listenerPath | /drain | Drain mode. Path the listener accepts POSTs on. |
| vercel.drainSecret | — | Drain mode. If set, deliveries with a missing or wrong x-vercel-signature (HMAC-SHA1 of the body) are rejected. |
| vercel.drainGraceMs | 3000 | Drain mode. How long collection waits after replay for trailing drain batches. |
| vercel.project | — | Drain mode. Keep only deliveries whose projectName or projectId equals this (a drain carries the whole team's logs). Unset = keep all. If every delivery is filtered out, run warns. |
| railway.token | — | Railway API token (Account/Workspace, from railway.com/account/tokens). Required for Railway capture. |
| railway.deploymentId | — | Pin log capture to one deployment. Overrides the project/environment/service trio. |
| railway.projectId / railway.environmentId / railway.serviceId | — | Together, the target for which the newest deployment is resolved each run (so a redeploy doesn't need a config change). |
| userFilter | — | Keep only log entries mentioning this string, case-insensitive — e.g. a user's email. Pushed server-side into the Supabase query WHERE and the Railway log filter when it's a plain identifier; client-side for Vercel. verirun run --user <value> overrides it per run. |
Environment variables (override the file)
| Variable | Replaces |
| --- | --- |
| VERIRUN_SUPABASE_ACCESS_TOKEN | supabase.accessToken |
| VERIRUN_VERCEL_TOKEN | vercel.token |
| VERIRUN_VERCEL_DRAIN_SECRET | vercel.drainSecret (drain mode) |
| VERIRUN_RAILWAY_TOKEN | railway.token |
Log capture
A source appears only when its config block is present. Run `verirun log
Supabase (pull). After replay, one query per configured source goes to the
Management API's analytics/endpoints/logs.all endpoint, scoped to the run
window. Needs projectRef and an access token — missing either aborts the run
before replay. A source that returns an error is warned about and skipped;
others still run. Timestamps are normalised to ISO 8601.
Railway (pull). After replay, one deploymentLogs GraphQL query to
backboard.railway.com/graphql/v2, scoped to the run window. Needs token and
either deploymentId or projectId + environmentId + serviceId (the newest
deployment of that trio is resolved first). Errors are warned about; the run
continues. Logs are already scoped to one service + environment.
Vercel (pull). After replay, verirun resolves the project's newest ready
deployment (or vercel.deploymentId) and reads
/v1/projects/<projectId>/deployments/<deploymentId>/runtime-logs, keeping the
lines whose timestamp falls in the run window plus tailGraceMs. Needs token
and projectId. The endpoint streams and stays open to tail live lines, so the
read stops once the stream has been quiet for a moment, or after readMs.
Errors are warned about; the run continues. Retention is Vercel's, not ours —
on Hobby, runtime logs are short-lived, which is fine because collection happens
seconds after the run.
During a run, verirun runs a local HTTP listener on listenerPort +
listenerPath; whatever the Log Drain POSTs while it is up is kept. It accepts
both JSON-array and NDJSON bodies, echoes the x-vercel-verify challenge (so
Vercel can confirm the endpoint), and — if drainSecret is set — rejects
unsigned or wrongly signed deliveries. After replay, collection waits
drainGraceMs for late batches, then stops the listener.
One-time setup (verirun log vercel --drain walks you through it):
- Expose the listener port to the internet —
ngrok http 4319, a Cloudflare Tunnel, etc. Note the public URL. - Vercel dashboard → your team's Settings → Log Drains.
- New drain: source Runtime Logs, delivery format JSON, endpoint the
public URL plus your
listenerPath(e.g.https://your-tunnel.ngrok.app/drain). Vercel verifies the endpoint on creation, so the listener must be reachable then — runverirun log vercel --listenin another terminal during setup. - Copy the signing secret into
VERIRUN_VERCEL_DRAIN_SECRETorvercel.drainSecret. - Put
listenerPortandlistenerPathin.verirun/config.json.
A free ngrok URL changes each session; a reserved hostname is worth it if you run this often.
Changelog
0.2.5
verirun runanswers browser dialogs instead of cancelling them. With no handler, Playwright dismissed everyalert/confirm/prompt, so eachconfirm()in a workflow came back as Cancel and the action it guarded quietly never happened. Replay now answers each one: with the step's saveddialog(accept/dismiss, pluspromptTextfor a prompt), otherwise with the new--dialog accept|dismiss(defaultaccept). Each dialog is printed under its step and listed inlogs.jsonunderdialogs. With--cdp, only the tabs the run drives are touched.verirun recordasks which button you pressed. Playwright's recorder notes that a step opened a dialog but not the answer, so after you close the browserrecordasks[O]K / [c]ancelfor each such step and saves it on the step.verirun mapshows the dialogs an element opens.--jsoncarries adialogslist per element (type,message, andgateswhen the handler reads the answer). An earlyif (!window.confirm(…)) return;now shows up as theguardSourceof the backend calls after it, so you can see that Cancel skips them. The summary counts them,--bfsprints them under their edge, andcoverage.mdlists them.verirun workflow checkvalidates thedialogfield. Aftermap --report, it also warns about a step that clicks a dialog-opening element without saying OK or Cancel.
0.2.4
verirun mapreads the import aliases your project actually declares. Only"@/*": ["./src/*"]was understood before; apathsentry resolved againstbaseUrl("@/*": ["*"]) or inherited throughextends— the two shapescreate-next-appand most monorepos emit — silently produced no aliases at all. Every@/…import then resolved to nothing, so the child components a page renders were never analysed and each page was reported as empty: pages listed, zero elements, zero effects, zero edges, no warning.- An unresolvable import alias now says so, and says what to do.
mapprints the exact--aliasflag to re-run with, instead of quietly reporting an app it could not read as an app with nothing in it. A scoped npm package is not mistaken for a broken alias. verirun agent --skill --installnow installs for Cursor too.--install cursorwrites.cursor/rules/verirun.mdcas an agent-requested rule,--install allwrites that and the Claude Code skill; a bare--installstill means Claude Code.verirun record --urlchecks the URL is up before opening a browser. A dev server that is not running produced a raw Node stack trace from Playwright's child process, followed by a message blaming thejsonlcodegen target — pointing at the wrong thing entirely. It now stops withNothing is listening at http://localhost:3000. The message for a codegen exit that genuinely recorded nothing no longer leads with thejsonltheory.
0.2.3
verirun whoamishows this machine's fingerprint, in the same 24-character form the account page lists, so a user with several machines of the same hostname can tell which row is which. The seat-limit message names it too, instead of only saying to go release something.An unlimited seat count (admin keys) no longer prints as a raw integer.
A seat now follows the machine, not the network. The fingerprint is anchored on the identifier the OS assigns its own installation (
/etc/machine-id,IOPlatformUUID,MachineGuid) instead of on MAC addresses and hostname. Docking a laptop, changing Wi-Fi, toggling a VPN or renaming the machine used to produce a new fingerprint, taking a second seat and — on a one-seat plan — locking the user out of the machine they were already using. Where an OS exposes no such identifier the old MAC-based fingerprint remains as a fallback.This shifts every fingerprint once. Machines connected on an earlier version register as new; release the stale entry from the account page.
0.2.2
Networking fixes. On a network that forces traffic through a proxy, the CLI could not connect at all — and said only "Check your connection", which was both unhelpful and often wrong.
- Proxy support.
HTTPS_PROXY/HTTP_PROXY/NO_PROXYare now honoured for every request the CLI makes. Node'sfetchignores these on its own, so on a corporate or campus network the browser worked whileverirun loginfailed with no way to configure around it. - The real error is reported. A failed request now names its cause —
ENOTFOUND(DNS filter or captive portal),ECONNREFUSED/ timeouts (blocked, or a proxy that is down), or a certificate failure, which points atNODE_EXTRA_CA_CERTSfor a proxy that inspects TLS. Previously the cause was caught and discarded. - A server error no longer blames your connection. A 429 now says the server is rate limiting; any other non-2xx says it is server-side.
verirun whoamishows why the server is unreachable, and whether a proxy is in use.- Log collection (Supabase, Vercel, Railway) goes through the same path, so it works behind a proxy too.
Requires Node 18.17+ (was 18), following the undici dependency added for
proxy support.
0.2.1
Windows support pass. Nothing in the CLI was Unix-only by design, but several paths had never been exercised on Windows.
- Workflow names reject the two spellings Windows cannot store: a trailing
dot (Windows drops it, colliding with the bare name) and a reserved device
stem —
con,prn,aux,nul,com1–com9,lpt1–lpt9, at any extension. Enforced on every platform, so a workflow recorded on macOS or Linux stays usable for a teammate on Windows. - Config permissions on Windows —
.verirun/config.json,~/.verirun/and the files in it are now restricted with anicaclsACL granting only the current user. Mode bits are not an access control on Windows, so these held provider tokens and the device token on whatever ACL they inherited. A connect flow that cannot lock the file down now warns instead of implying protection it did not get. verirun log supabasecan find thesupabaseCLI on Windows, where it is a.cmdshim that needs a shell to launch. It previously always fell through to the manual project-ref prompt.- Hardware fingerprint filters Windows virtual adapters (Hyper-V, WSL, VMware, VirtualBox, VPN and loopback), which were being counted as physical and could shift the machine id — consuming an extra seat against the license.
- Docs:
--user-data-dirneeds an absolute path on Windows, since neither cmd, PowerShell nor verirun expands~.
Not verified on Windows. These fixes are from reading the code and the platform rules, not from a Windows test run. The signed-in Chrome profile path in particular is untested there.
0.2.0
verirun map— static interaction graph of a React/TypeScript app: every page, the interactive elements on it, where they navigate, and the backend calls they make. No browser, no recording.--json,--report,--reach,--bfs <node> --depth N. Routes come from react-router<Routes>or a Next.jsapp/**/page.tsxtree.verirun workflow check— validate a workflow file without running it. Together with the selectorsmapemits, a workflow can now be written by hand (or by an agent) instead of recorded.- Vercel logs without a tunnel —
verirun log vercelis now a token and a project, pulled from Vercel's runtime-log API. The old Log Drain flow remains under--drainand is still used automatically for existing configs. verirun agent --skill --install— writes.claude/skills/verirun/SKILL.mdso Claude Code picks the tool up on its own.- Watchable replays —
--headednow paces actions (500ms), tunable with--slow-mo <ms>. verirun record/rundocument how to use a signed-in Chrome profile.
0.1.0
record,run,log(Supabase / Vercel drain / Railway),agent, and the account commandslogin/logout/whoami.
