The CLI
One binary, thirty-one command families, and a contract designed so that both a human and an agent can use it without either one guessing: a frozen exit taxonomy, a machine channel that owns stdout, and a schema you can print offline.
The first four commands
squiggle init # wire the SDK into an existing appsquiggle init --mcp # …and register the MCP server in .mcp.jsonsquiggle up # relaunch WITH the agent injected — and prove itsquiggle doctor # is the whole plane actually green?squiggle flow login # drive itinit detects Expo or bare RN, your package manager and your entry file, installs the SDK, plants the import, writes EXPO_PUBLIC_SQUIGGLE_WS_URL so the app dials this machine, scaffolds a first flow at .squiggle/flows/smoke.yaml, and names the adapters your package.json implies. It is idempotent — a second run is a no-op — and it makes conservative edits with a printed-instruction fallback for anything it does not recognise.
Driving the app
- flow · flows · watch · bench
- Run one flow, shard a directory across several simulators, re-run on every file save, or benchmark against the last comparable run. See Writing flows and Speed & delivery.
- record · replay · diff · reproject
- Capture a session, replay it (optionally asserting a flow against the recorded timeline), compare two recordings behaviourally, or re-fold recordings into the store. See Record & replay.
- crawl
- Autonomous exploration — prints a coverage and crash report plus a discovered-flow seed, and exits non-zero if it observed a crash.
--completekeeps going until every reachable screen is visited, writes them to the screen memory the Inspector’s Screen map draws, and lists the declared screens it could not reach;--from <route>starts at one screen. - consumer
- The standalone interactive graph watcher, which also asserts that the consumer’s materialised graph equals the app’s.
Off-screen targets fail, unless you ask for a scroll
A tap or type whose target measures outside the viewport fails, naming the rect, the viewport and the clamped point. The alternative was never “nothing happens”: the driver clamps an out-of-bounds point into the viewport — it has to, an out-of-bounds HID event can take the simulator down — and then presses whichever control sits at that edge and reports success.
--auto-scroll scrolls the target into view and delivers once more, marking the step scrolled into view. It is off by default because it moves the screen on a step that never asked to, which is wrong for any flow whose subject is the scroll position — and because the explicit scrollUntilVisible is the better thing to have in a committed flow. It is a comparability key for bench, so auto-scroll runs are never averaged with runs that never scrolled.
A flow that needs it every time should say so in the file rather than depend on the flag being typed: config: { autoScroll: true }, which is honoured by the CLI, Studio and the MCP server alike. See run requirements.
The flags you will reach for
flow --fresh- Relaunch the app, agent injected, before the run. A failed run leaves the app wherever it stopped, and the next run then fails for that reason rather than its own, often with a selector error that points at the wrong file. Reach for this whenever the previous run failed.
flow --record·--record-to <file>·--attach-pr- Record the screen for the run, into the store’s videos (pruned) or a path you choose (never pruned), and attach the recording to this branch’s pull request.
--attach-prneeds--record. flow --inline <yaml>·flow -- Run a flow given as a string or on stdin, with no file on disk.
flow --continue-on-fail·--junit <file>·--events- Report every step instead of stopping at the first hard failure; write JUnit XML so CI renders the run as native test results; stream one NDJSON line per phase and step on stderr.
flow --needs-agent fail·--print-prompt·--redrive <goal|all>- For a flow with
agent:steps: make NEEDS AGENT exit 1 instead of 6, print only the prompt to paste into your agent, or ignore a step’s recording so it needs an agent again.flows --agenticand--scriptedrun one kind. See Agentic flows. flows --clone-from <udid>·--keep-clones- Clone a simulator to top up the shard pool, and leave the clones behind afterwards if you want to reuse them.
up --if-needed·--udid·--no-animations·--capture-net- Skip the relaunch when the app is already up and injected; pick the simulator when several are booted (otherwise the first); turn UIKit animations off for deterministic replay; install the pass-through network protocol that network-idle waits and stubbing need.
When a flow fails, it shows you the screen
A failing run captures the device screen to .squiggle/runs/<runId>.png and puts it under the failing step — drawn inline in a terminal that has graphics (iTerm2, kitty, WezTerm, Ghostty, VS Code), and named as a path everywhere else. The step list tells you a tap did not find its target; the screen tells you the app was on a login wall, or mid-crash-dialog, which is usually the whole answer.
The path is printed either way, and that is the point rather than a fallback: an image cannot be grepped, copied, or opened in a diff tool, and in a CI log the path is the only half that survives. So the PNG is written whether or not anything can render it, and a failed CI run keeps the frame as an artifact. Passing runs capture nothing. SQUIGGLE_NO_IMAGES=1 keeps the caption and drops the picture.
Reading the data
- perf
- Sixteen subcommands:
sql,top,saved,detector-stats,report,insights,watch,snapshot,diff,export,gc,profile,heap,heapdiff,crashesandsymbolicate. See Performance. - product
- Ten:
overview,screens,usage,affinity,releases,users,sessions,retention,frictionandfunnel. See Product analytics. - history
- Recent runs from any surface with their outcomes and durations, and
history rerun <id>to replay one from the argv recorded with it. - probe
- Reads a running app from outside it, over the CDP that Metro’s inspector already exposes. No rebuild, no device, no licence. It reaches both Hermes runtimes: the JS one and the Reanimated UI runtime. That matters most on React Native macOS, which has no Flipper and no Xcode-grade JS tooling, and where a native sampler’s stacks stop at the interpreter.
probecounts renders per component, counts live animations exactly, finds thesetIntervalbusy-loop that only macOS has, and prices CPU as a cputime delta rather than atopsample. Every leaf is free exceptreplay, which drives the app.
Making the app drivable
- legibility
- Scores how agent-drivable the app is and names the exact source lines to fix: interactive controls with no stable selector, screens addressable only by generated text, duplicated handles, missing roles, hit targets under the 44 pt minimum.
--fixinserts the suggestedtestIDs as a parser-driven byte-offset splice — not a regex — applied back-to-front per file so one insert cannot shift the next one’s offset.--dry-runfirst;--sariflands them as PR annotations on those lines; and--min-scoreis a floor a team raises over time rather than a demand for perfection, because a “must be 100” gate gets disabled in week one. - doctor
- Connectivity, handshake, graph health, route tracking — and injection, named capability by capability, because in-process delivery, direct scroll and settle-gated waits degrade silently rather than failing. It also compares the app’s SDK version against the CLI: a skew is a warning, a protocol mismatch is a failure. And it reports an agent kit whose skills were generated by a different build than the one you are running — a project with no kit installed reports nothing.
--fixrepairs what can be repaired;--bundlewrites a redacted support bundle where every string that leaves is a version, a boolean, an enum, or a path rewritten relative to your project root.
Agents
- agent
- Seventeen subcommands, mostly the offline half — schema, briefings, deny-rail config, KPIs, corpus, gate-only
validate— so code review and CI need no device.agent runexecutes a program against the booted app.agent kitgenerates the harness;agent rulesemits an AGENTS.md fragment generated from the command catalogue, so it cannot describe a CLI that no longer exists. - benchmark
- Score an agent against a versioned suite of mobile-UI journeys, graded by re-observation rather than by a human reading screenshots.
--referenceruns each task’s oracle and its counter-example at zero model turns;--agenthands the prompt to any MCP-speaking command and scores the end state through the same path. - mcp
- The stdio MCP server. See Agents & MCP.
- gate relaunch · gate mcp
- Device-level acceptance gates for the failure classes a unit suite structurally cannot see — because the bug is a process being replaced, or a server launched over real stdio.
The contract
Exit codes are frozen and published, which is what lets an agent or a CI script branch on an integer rather than parse prose:
- 0
- Success. Warnings allowed.
- 1
- The run under test failed — a flow, a suite, a gate, a diff.
- 2
- Usage error, or an expected failure with a human message.
- 3
- Licence required or expired.
- 4
- Timeout — the
--timeoutbudget elapsed. - 5
- Device unavailable, or held by another run.
- 6
- Needs an agent: an
agent:step could not replay its recording, and no agent was attached. Not a failure — no step failed — and--needs-agent failmakes it 1.
squiggle explain E_NOT_INJECTED — or squiggle explain 5 — turns any of them into an explanation plus a runnable fix, which is what makes an error in a CI log self-serve. Eight codes are catalogued, including the licence ones: squiggle explain E_LICENSE_EXPIRED works when nothing else does, because explain is one of the commands that never needs a licence.
Output rules
- A machine channel owns stdout
- With
--jsonor--events, stdout carries the payload alone and every human line moves to stderr. That is what makes piping safe. - Status is never colour alone
- Every verdict carries a glyph as well as a hue, with an ASCII fallback chosen by one degradation decision behind
--no-color,--no-unicode,NO_COLORandCI. One rule covers colourblind readers, terminals that mangle Unicode, and log files where colour is stripped. - Truncation announces itself
- Every capped list says what it elided and how to ask for the rest. A silently elided tail reads as “that is all there is”.
- Empty states name the next command
- Rather than printing
0 rows.
squiggle perf sql "select * from spans limit 5" --json | jq '.rows[]'squiggle flow checkout --json | jq '.steps[] | select(.status=="fail")'squiggle product funnel run checkout --jsonConfig and profiles
{ // the app under test, when this repo drives exactly one "bundleId": "com.acme.app", "flowsDir": ".squiggle/flows", "shards": 4, // named environments — --profile ci merges profiles.ci over everything above "profiles": { "ci": { "relay": false, "shards": 4 }, "local": { "relay": true, "udid": "…" } }}The config schema is generated from the config type, so a missing key is a compile error rather than a stale schema nobody notices. Same for the flow schema, which is generated from the parser’s own vocabulary — put a $schema header on your YAML and your editor autocompletes it, and an agent validates a flow before a run instead of failing after one.
squiggle agent schema flow # print the flow schema offlinesquiggle agent schema intent # the intent-program schemasquiggle --schema-json # the whole command tree, as dataA flow can carry its own run settings
Four run settings can live in the flow’s own config: block — scrollMode, fastSettle, stepDelayFloorMs, transitionSettleMs — because for those, the right value is a property of the flow rather than of the machine. A flow that tests scrolling needs the gesture loop’s real physics whether or not the suite was launched with --direct-scroll.
Running one flow, a flag you actually passed wins — a one-off is what a flag is for. Running several (squiggle flows), the flow wins even against that flag, because one flag cannot be right for every flow in a suite. Either way the run says which happened, since a flag silently failing to apply would be worse than not having the feature. --ignore-flow-config turns the block off. Full rules in Flows.
Licensing and housekeeping
squiggle activate --trial # start a trialsquiggle activate <key> # activate a purchased seatsquiggle license # which of the four states you are in, and whysquiggle update --check # channels, and --to <version> to roll backsquiggle uninstall --dry-run # every path we wrote, with sizes, before deletingsquiggle telemetry status # anonymous, on by default — one command turns it offsquiggle completions zsh --install # tab-completion for zsh, bash or fishActivation writes a signed grant to ~/.squiggle/license.json that the CLI, the MCP server and the desktop app all read — one activation, three surfaces. The grant is honoured offline until it expires, so a licence check never adds latency and never fails a run mid-flight. license and activate are never gated: they have to work when nothing else does.
Telemetry is anonymous and on by default — and nothing is ever sent before the one-time first-run notice has been shown on your machine. squiggle telemetry off, DO_NOT_TRACK=1 or SQUIGGLE_TELEMETRY=0 turns it off, and squiggle telemetry prints the exact event that is sent, built by the same code the dispatcher uses. A policy you can print is a fact where a paragraph is a claim.
Every command
Thirty-one top-level commands, eighty-one leaves. The table below is the whole surface — but it is a copy, and the binary is the original: squiggle --schema-json prints this tree with every flag, type and default, offline and in milliseconds, and a test fails the build when a command reads a flag it does not declare. If the two ever disagree, the binary is right.
Bring-up and health
up- Relaunch the app with the agent dylib injected, and prove it by round-tripping the dylib's Mach-O UUID. Run this first.
doctor- Connectivity and graph-health smoke test, with a named remedy per failure.
--fixapplies the safe ones;--bundlewrites a support archive. init- Wire Squiggle into an existing app — dependencies, the Babel plugin, the entry import, the environment, and a first flow.
consumer- The headless graph consumer in watch mode. Materialises the same graph the app holds and asserts parity; records perf until interrupted.
gate relaunch·gate mcp- The two device-level acceptance gates: that the SRP, AX and push planes all survive the app process being replaced, and that the MCP surface a client actually gets is the one we think we ship.
Driving
flow·flows- Run one semantic flow, or a whole suite sharded across every booted simulator. Both rewrite the file in place when self-healing repairs a selector, so the repair arrives as a reviewable diff.
watch- Re-run a flow on every file change, against the warm session — no reconnect, no relaunch, no cold start between iterations.
bench- Run a flow and compare its speed against the last benched run, with per-phase deltas.
--historyprints the recent trend. crawl- Autonomous, seeded, deny-filtered and bounded exploration. Returns coverage, crashes, and a flow seed for what it discovered.
record·replay·diff- Capture a live session to
.sqrec; replay it with no device attached; and diff two recordings behaviourally — render counts, route timeline, network, spans.
perf — sixteen leaves over one SQLite file
sql·saved·top·report- Read-only SQL over
perf.db; named queries shared with the desktop; the live current-readings strip; and a human summary of a session. insights·watch·detector-stats- A session’s findings with each component’s
file:lineas a link; the same, followed live, each new finding printed once; and how often each detector fires on flow runs that failed an assertion versus clean ones — the evidence for which detectors are worth gating on. snapshot·diff- Freeze a session's shape as a labelled baseline, then compare against it — with a comparability check, so two runs that are not alike say so instead of reporting a meaningless delta.
profile·heap·heapdiff- Hermes CPU profiles and heap snapshots over the Metro inspector, and a growth-by-constructor diff of two snapshots with the top retainers of the biggest grower.
crashes·symbolicate- Ingest native
.ipsreports from DiagnosticReports, and symbolicate stored JS and native stacks. export·gc- Chrome-trace export for
chrome://tracingor Perfetto, and the retention sweep.reprojectre-folds recordings and checks the store still agrees with the canonical log.
probe — ten leaves over Metro’s inspector
targets·eval·reload- List the runtimes Metro exposes: the JS runtime and the separate UI (worklet) runtime. Evaluate an expression in either one; every other leaf started as a one-line
eval. Reload over the Metro message WebSocket, becauseGET /message?type=reloadanswers 200 and reloads nothing on a macOS dev client.--portpicks the Metro server (default8082), and it may come before the expression:probe eval --port 8081 '<expr>'. A probe pointed at the wrong port reports confidently on the wrong app, so check it. renders·anims·timers- Render counts and milliseconds per component, before and after a run, from
<SquiggleProfiler>anduseRenderReasonin@squiggle.sh/react-native(development builds only). The animation census: rAF calls per second divided by the display rate is the number of live animations, as an integer, where CPU% swings by fifteen points. And the timer cliff:setIntervalbelow about 20 ms busy-loops on React Native macOS, and one empty interval costs 38.7% of a core. cpu·cell·frames- CPU as a cputime delta over a fixed window, with the load average beside it. One atomic cell that samples animations, CPU, frames and load over the same window, because two separate commands let something start in between. The frame-stream ledger prices a streaming pipeline as a number. Any probe that would perturb the app refuses while a run is in progress.
replay- Take a Flow Studio replay with no mouse, so a measurement is twenty samples instead of two. It is the only leaf that acts, so it needs the
driveentitlement.
product — ten leaves
overview, screens, usage, affinity, releases, users, sessions, retention, friction and funnel — the same readers the desktop Product screen and the product_* tools use, over the same event stream. Funnel definitions live in the project's config sidecar, so one definition serves all three surfaces.
agent — seventeen leaves
run·validate·branch- Run an intent program against the booted app, check one without running it, and explore alternative continuations from a point in the journal.
journal·watch·stats- What the agent did and why, live or after the fact, plus KPIs over the journal — turns, heals, escalations, cost.
briefing·amend·schema·kit·rules- The methodology as artefacts: the mode briefing with this app's learned amendments, the amendments alone so they can be diffed in a PR, the published JSON schemas, the full Claude Code harness, and an
AGENTS.mdfragment for the repo. config·eval·library- The deny rail and run budgets; the eval corpus replayed case by case; and the saved flow library — the app's learned verb vocabulary.
export·skill·patterns- The flywheel: the corpus as JSONL training data, with the basis of every reward on the record; a recurring escalation class proposed as a reviewable
SKILL.md, promotable only on a measured benchmark win and revocable afterwards; and learned handlers exported or imported as a bundle, filtered field by field in both directions.
benchmark — the scored suite
list, run --reference, run --agent <cmd> and history. The reference run is the CI-shaped one — every task's oracle plus its counter-example, at zero model turns — and results ratchet within one agent on one suite version, because a reference score and an agent score are not comparable and neither are two different task sets. See Agents & MCP.
The rest
legibility scores how agent-drivable the app is and names the source lines to fix. mcp serves the tool surface over stdio. history lists recent runs and how to re-run one. explain decodes an error or exit code. license, activate, update, uninstall, licenses, telemetry and completions are the productization set above. redact shows what would have to come out of an artifact before it may leave your machine; it is a dry run unless you pass --write.