Axonpack
Reference

devtools CLI

Every command, action, flag, topic, output line, error and exit code of the devtools command line tool.

The devtools command line tool reads what the devtools recorded in a running app, and can mock, block or send a request in it. This page lists everything it takes and prints. Use with AI agents shows how to use it, with a snippet for your agent.

Running it

Package managerCommandWhere
npmnpx devtools <command>The app's project folder
yarnyarn devtools <command>The app's project folder
pnpmpnpm exec devtools <command>The app's project folder
bunbunx devtools <command>The app's project folder

bun devtools runs a devtools.ts file first if the folder has one. In a monorepo, or outside the app's folder, npx devtools can miss the local install and fetch an unrelated npm package named devtools. npx @axonpack/expo-devtools <command> always runs this one. Below, devtools stands for whichever form you use.

NeedsWhy
Node 22 or newerIts built-in WebSocket sends the Origin header Metro asks for. Older Node exits with code 1.
A development bundleThe app in a dev build or Expo Go, with <DevtoolsProvider> mounted and enabled on.
expo start runningOn this computer. The tool talks to Metro on 127.0.0.1, ports 8081 to 8090 unless --port.

Commands

CommandWhat it prints
devtools appsOne row per app and device on Metro: app, name, device, devtools, metro, note.
devtools topicsOne row per topic in the picked app: topic, rows (off when the app turned it off), actions (comma-separated, - for none), about.
devtools <topic> [flags]The topic's rows, newest first, with the query flags.
devtools get <topic> <id>One row as indented JSON.
devtools <topic> <action> [args] [flags]Changes something in the app. See Actions.
devtools <command> --helpThe help for that command. devtools, devtools help and devtools --help print the main help. devtools network --help adds the network actions, and devtools network <action> --help prints only them.

apps and topics are probes: the AI Usage tab doesn't list them. In apps, the devtools column is the package version in the app, - when the app doesn't have it, and ? when it can't be checked without --force.

Flags

A flag is --name value or --name=value. A value can start with a dash, as in --sort -ms. -h is the only short flag.

Query flags

The same on every topic. get takes --full, --json and --out.

FlagValueDefaultWhat it doesExample
--greptext, or /regex/flagsnoneRows holding it in any value, bodies included. Text ignores case. Keys are not searched.--grep timeout
--wherea clause, repeatablenoneRows where the clause holds. Every --where must hold.--where 'code>=400'
--sincea timenoneRows at or after it.--since 10m
--untila timenoneRows at or before it.--until 2026-11-03T10:00:00Z
--limitwhole number from 150Rows to print, or groups with --group-by.--limit 20
--aftera row idnoneOnly rows newer than it, oldest first.--after fetch-1762172310455-212
--beforea row idnoneOnly rows older than it.--before console-1762172290871-80
--sortfield, or -fieldnewest firstSorts by that field, - for descending. Rows without a value go last. No cursors.--sort -ms
--fieldscomma list of fieldsthe topic's columnsThe columns to print.--fields id,code,url
--countnoneoffPrints only how many rows match.--count
--group-byfieldnonePrints how many rows have each value, most first.--group-by code
--fullnoneoffStrings whole. Without it, a string over 2,048 characters is cut.--full
--jsonnoneoffJSON lines, one object per row, with no # lines. get prints one line.--json
--outfile pathnoneWrites the reply whole (as with --full) to the file, and prints one line saying so. With get, the file holds the JSON alone.--out response.json

App flags

Needed only when more than one app is running, or for an app on another port.

FlagValueDefaultWhat it doesExample
--appExpo slugnoneThe app with this slug, whole, any case. Without an Expo manifest, the app id.--app my-app
--devicetextnoneThe app on a device whose name holds this text, any case.--device pixel
--portwhole number8081 to 8090Look on this Metro port only.--port 8082
--forcenoneoffAttach to an app that can't share its debugger and has no withDevtools to ask through. It closes a DevTools window open on the app.--force
--help, -hnoneoffPrint the help for the command instead of running it.network --help

apps takes --port, --force and --json. topics takes the app flags and --json. An action takes the app flags and its action flags.

Where clauses

<field><op><value>, or <field>?.

OpHolds when the valueExample
=equals it, ignoring casemethod=get
!=does not equal it. A row with no value holds toolevel!=log
> >= < <=compares as a number. The value must be a numberms>1000
~contains it, ignoring case, or matches a /regex/flagsurl~/v1/
!~does not contain it or match it. A row with no value holds toourl!~analytics
?has a value: not empty, not missing (<field>?, no value after it)error?

A plain value with commas is any of them: method=get,post. A /regex/ is never split on commas. An object value is matched as its JSON.

Times

--since and --until take a number and a unit, ms, s, m, h or d (30s, 1.5h), counted back from now on the device's clock, or any date the JavaScript Date reads, such as an ISO time. A row with no time, such as every storage row, is left out by either.

Topics

id and t are on every topic: id is what get, --after and --before take, and t is the time --since reads, printed as an ISO time. Fields in bold are the columns shown by default. A --fields name that doesn't exist answers with the topic's fields.

network

HTTP requests, WebSockets and server-sent event streams since launch, the last 1,000. get adds a socket's frames as messages and a stream's events as events. A request's initiator lists each frame as a file with its line and column, and keeps frames from libraries only with --full.

--where state=error lists every request that failed: a 4xx or 5xx answer, a request that got no answer, and a socket that failed. --where 'code>=400' lists only the 4xx and 5xx answers. A socket has no code; how it closed is in close.

FieldValue
idThe row's id
tWhen it started
kindhttp, ws or sse
methodThe HTTP method
codeThe HTTP status. Empty for a socket
closeA socket's close code, 1000 for a normal close. Empty for HTTP
statepending, success or error (a 4xx or 5xx answer, or no answer); a socket's connecting, open, closing, closed or error
msHow long it took
urlThe URL
errorWhy it failed
typeThe response's MIME type (HTTP)
sizeThe response body's size in bytes (HTTP)
msgsHow many frames or events a socket or stream has
sourceThe name of the WebView that made it. Empty for the app's own
requestThe request body (HTTP)
responseThe response body (HTTP)

console

Console lines since launch, the last 500. A line repeated in a row is one row with a count.

FieldValue
idThe row's id
tWhen it was logged
levellog, info, warn, error, debug, input and result (the > prompt) or crash
textThe whole line as text
countHow many times it repeated
sourcenative for the app, or the name of the WebView that logged it
crashThe crash report's id, when the line is a crash

performance

Long tasks, slow interactions, marks and measures, newest first. Kept only while the Performance tab records, and it starts paused. Memory and frame-rate samples are not rows. An empty list while the tab is paused ends in # paused: the Performance tab records only after record is pressed.

FieldValue
idThe row's id
tWhen it happened
kindlongtask, interaction, mark or measure
nameIts name
msHow long it took
handlerHow long an interaction's handlers ran
detailThe detail a mark or measure was given

storage

Every key of every registered store, by store then key. Rows have no time. Each query lists every store's keys again, so keys the app wrote since show up, and keeps the values already read. A list then reads the values on its page, and --grep or a --where, --sort or --group-by on value, type, size or error reads every key once. A value read earlier is not read again.

FieldValue
id<store>/<key>
tAlways empty
storeThe store's id
keyThe key
typejson-object, json-array, string, number, boolean, buffer, empty, absent, or unread before its value is read
sizeThe value's size in UTF-8 bytes
valueThe value as text, after redaction
errorWhy reading it failed

query

The TanStack Query cache: queries and mutations, the one updated last first. off with no QueryClient in config.query.client.

FieldValue
idThe query hash, or mutation-<n>
tWhen it last updated, or when the mutation was submitted
typequery or mutation
keyA mutation's key
statusIts status
observersHow many observers a query has
variablesA mutation's variables
dataIts data
errorIts error

The moves the navigator made, the last 200. off with no Expo Router or React Navigation found.

FieldValue
idThe row's id
tWhen it moved
actionThe action's type
fromThe route it left
toThe route it landed on
pathThe destination's URL path
paramsThe destination's params
containerThe navigation container that moved
noopTrue when the action changed nothing

redux

Dispatched actions, the last 200, each with what it changed. Never the whole state. off with no store using devtoolsReduxEnhancer().

FieldValue
idThe row's id
tWhen it was dispatched
typeThe action's type
actionThe whole action
changedThe state paths it changed, comma-separated
changesHow many paths it changed

get adds each change: its path, whether it was added, removed or changed, and the value before and after.

crash

Captured crashes, newest first. get adds the stack, the breadcrumbs and the device. off when crash.enabled is false.

FieldValue
idThe row's id
tWhen it happened
kindjs-fatal, js-error, unhandled-rejection, react-render or native-exception
nameThe error's name
messageThe error's message
routeThe screen on top when it happened
previousTrue for a crash read back from the previous launch
seenTrue once the report was shown

ai-usage

The queries and actions agents made, the last 100, each with the reply it got. A query of ai-usage keeps no reply. --where 'action?' lists the actions alone.

FieldValue
idq<n>
tWhen the app answered
commandThe query or action as you would type it after devtools. A masked header shows as [redacted], a body over 200 characters as its start and length
topicThe topic it asked about
actionThe action's name. Empty for a query
matchedRows it matched
totalRows the topic held
msHow long the app took
sizeThe whole reply's length in characters
errorThe message of an error: reply
cutTrue when only the first 64 K characters of the reply were kept
replyThe reply as the agent got it

Actions

devtools <topic> <action> [args] [flags] changes something in the app. Only network has actions; devtools topics lists each topic's actions. Query flags (--where, --json, --out and the rest) don't go with an action, and action flags don't go with a query: either is an error before the tool looks for an app. Every action, refused ones too, is listed in the AI Usage tab, marked as an action.

Action flags

FlagValueTaken byWhat it does
--statuswhole number, 100 to 599overrideThe mocked status. Default 200.
--bodytextoverride, send, replayThe body. Default empty for override. Not for a GET or HEAD request.
--body-filefile pathoverride, send, replayRead the body from this file on the computer, as UTF-8 text. Not with --body.
--content-typeMIME typeoverrideThe mocked content-type. Default application/json.
--header'Name: value', repeatablesend, replayA request header. In replay it replaces a header of the same name, any case.
--yesnonesend, replayNeeded for any method but GET, HEAD and OPTIONS. The app checks it, so a call through the window relay needs it too.
--allnoneunoverrideRemove every override instead of one.

network actions

Overrides match the exact URL the app requests, scheme and query included, as the Network tab's own overrides do. There are no patterns or prefixes: copy the URL from a row. They are the same overrides the tab makes, so they show in its Overrides view and in React Native DevTools. They apply to fetch and XMLHttpRequest, and last until the app reloads or they are removed.

ActionArgsFlagsReply
override<url>--status, --body or --body-file, --content-type# network · override on <url>: <status>, <type>, <n> chars. The app's requests to that exact URL get it, without reaching the network
block<url>none# network · blocking <url>: the app's requests to that exact URL fail as if offline
unoverride<url>, or none--all (with no <url>)# network · removed the override on <url>, or # network · removed <n> overrides
overridesnonenone# network · <n> overrides, then the columns url action status type size (- for a block)
send<METHOD> <url>--header, --body or --body-file, --yesThe new row, as below
replay<id>--header, --body or --body-file, --yesThe new row, as below

send sends the request from inside the app, through the same function as the Network tab's Sandbox, and so through the app's own fetch. Overrides and network conditions apply to it, and it is a row in the log like any other request. The method is case-insensitive.

replay takes a logged HTTP request and sends it again as the Sandbox would: the same method, URL, headers and body, with --header and --body on top. The app's own headers go with it, including ones the tool shows you only as [redacted]. A header that network.redactHeaders removed before the request was logged goes as the text [redacted]; pass it again with --header.

Both answer with the new row, in the network columns plus error:

# network · my-app · iPhone 16 Pro · sent POST https://api.example.com/v1/notes as fetch-1762172310455-215. devtools get network fetch-1762172310455-215 shows it whole
id	method	code	state	ms	url	error
fetch-1762172310455-215	POST	201	success	312	https://api.example.com/v1/notes	-

replay starts replayed <id> as <new id>. A request that failed (blocked, offline, no answer) is still a row, with state error, and exit code 0. When the request is not in the log (recording is paused, or network.redact dropped it) the reply is # network · sent <METHOD> <url>: <status> in <ms> ms. Not in the log: recording is paused, or the app's network redact config dropped it, and its body is not shown.

While the request is in flight the app answers # wait: sending <METHOD> <url> as <id>, and the tool asks again every 200 ms within the 5 seconds. A request that takes longer carries on in the app. Running the command again would send it again, so the error points at ai-usage instead, where the action is listed once it ends.

Output

Tab-separated, with a header row. In a cell, - is an empty value, a tab or line break is written as \t, \n or \r, t is an ISO time, and a string over 2,048 characters ends in ... [cut at 2048 of <n> chars, --full for all].

LineWhen
# <topic> · <app> · <device> · <n> of <total> matchFirst line of a list. Ends in · showing <n> when --limit cut rows off.
# <topic> · <app> · <device> · <n> of <total> match · <k> values of <field>First line of --group-by, above <field> and count columns.
<n>The whole reply to --count.
# next: --after <id>Last lines of an unsorted list: the cursor for the next poll. A row still in flight holds it back.
# more: --before <id>When the limit cut older rows off: the cursor for the next page.
# <topic> is off: <reason>The whole reply for a topic the app turned off. Exit code 0.
# <topic> <id>First line of get, above the indented JSON.
# <topic>: wrote <n> chars to <file>The whole reply with --out. With get it starts # <topic> <id>, and the file holds the JSON alone.
# paused: the Performance tab records only after record is pressedLast line of an empty performance list while the tab is paused.
# topics · <app> · <device>First line of topics.
# <topic> · <app> · <device> · <what it did>First line of an action's reply. See Actions.
# ?: checking would close a DevTools window open on the app. --force checks anyway.Under apps, when a row shows ?.
# wait: <what>Never printed. The app says it is still reading, or an action is still sending; the tool asks again every 200 ms.

--json prints none of the # lines, so it has no cursors. Each row is one JSON object with the chosen fields; --group-by gives { "<field>": value, "count": n }. Strings are still cut unless --full. With no rows it prints nothing.

Errors

The app's own errors are one error: ... line on stdout. The tool's own errors are one error: ... line on stderr. Both exit with the code shown.

From the app

MessageCode
unknown topic "<name>". Topics: <list>1
"<text>" isn't a topic. Pass the topic and each flag as separate arguments: devtools <text>1
unknown field "<name>" in <topic>. Fields: <list>1
no <topic> row "<id>", it may have aged out of the buffer1
bad --limit <n>: use a whole number from 11
bad --where "<clause>": use <field><op><value> with = != > >= < <= ~ !~, or <field>?1
bad --where "<clause>": unknown op "<op>". Ops: = != > >= < <= ~ !~1
bad --where "<clause>": <op> needs a number1
bad regex <pattern>: <why>1
bad --since "<value>": use 30s, 10m, 2h, 1d or an ISO time (and --until)1

From the app, for an action

MessageCode
<topic> has no action "<name>". Actions: <list>1
<topic> has no actions, only queries1
<topic> is off: <reason>1
usage: devtools <topic> <action> <args>, such as usage: devtools network unoverride <url> | --all1
<topic> <action> takes no --<flag>. usage: devtools <topic> <action> <args>1
bad --<flag>. usage: ..., for a value of the wrong kind (only through the window relay)1
"<url>" is not a whole URL. Use the exact URL the app requests, https:// and query included1
bad --status <value>: use 100 to 5991
no override on <url>. devtools network overrides lists them1
bad method "<method>": use GET, POST, ...1
<METHOD> can change data on the server, so it needs --yes. GET, HEAD and OPTIONS go without1
a GET request has no body, so drop --body (and HEAD)1
bad --header: use 'Name: value'1
no network row "<id>", it may have aged out of the buffer1
<id> is a WebSocket. replay sends HTTP requests only1
<METHOD> <url> failed: <why>, only when the request is also not in the log1

From the tool

MessageCode
devtools needs Node 22 or newer, and this is Node <version>.1
unknown flag <flag>. See devtools --help1
--<flag> needs a value1
bad --limit <value>: use a whole number from 1 (and --port)1
usage: devtools get <topic> <id>. See devtools get --help1
usage: devtools apps. See devtools apps --help (and topics)1
--status, --body, --body-file, --content-type, --header, --yes and --all go with an action, such as devtools network override <url>. See devtools network --help1
query flags such as --where, --json and --out don't go with an action, and <topic> <action> is one. See devtools <topic> --help1
pass --body or --body-file, not both1
can't read --body-file <path>: <why>1
no reply from <app> on <device> in 5 s. Its JS may be paused at a breakpoint: resume it, then run the command again.1
still <what> after 5 s. The app keeps going, so run the same command again to pick up where it stopped.1
still <what> after 5 s. It carries on in the app, and devtools ai-usage --where 'action?' shows how it ended. (an action)1
could not reach Metro on port <port>1
could not attach to <socket url>1
the debugger socket closed (<code>: <reason>)1
the app threw: <message>1
no DevTools window is open on this page, or another error from the window relay1
no app found on Metro ports 8081 to 8090. Open the app in a dev build or Expo Go with `expo start` running, or pass --port. (port <n> with --port)2
no app matches <flags>. Running:, then a --app <slug> --device '<device>' line per app2
<n> apps match. Pick one:, then a --app <slug> --device '<device>' line per app2
<app> on <device> can't share its debugger (React Native below 0.86, or Expo started headless), and metro.config.js has no withDevtools to ask through its DevTools window, so attaching could close one open on it. Pass --force to attach anyway.2
<app> on <device> has no devtools agent. It needs @axonpack/expo-devtools with <DevtoolsProvider> mounted, in a dev build or Expo Go.2

In apps, an app that couldn't be asked shows why in its note column instead, such as no reply in 5 s: JS may be paused at a breakpoint.

Exit codes

CodeMeaning
0It answered, including a topic that is off, a query that matched nothing, and a sent request that failed.
1An error: a bad command or flag, an app's error: reply, Node older than 22, a socket error, or no answer in time.
2No app to ask: none running, none or several matching, no devtools in it, or it needs --force.

Timing

LimitValue
Each command5 seconds for the app to answer, waits included.
Asking againEvery 200 ms while the app answers # wait:.
Metro reads2 seconds for each read of /status, /json/list and the manifest.

What it reads

Only what the stores keep, after every redaction in the config. The authorization, cookie and set-cookie headers and a page's document.cookie are always [redacted], in every output, and filters run on the masked row. An action's reply goes through the same masking. The only changes the tool can make are the actions. See Redaction and Older React Native.

On this page