Use with AI agents
An AI agent, or you, reads what the app recorded from a terminal, mocks a response or sends a request from the app. Requests, logs, crashes, storage and every other tab, filtered down to the rows you need.
An AI agent working on your app can't see the phone. The package comes with a command line tool,
devtools, that reads what the devtools recorded in the running app: every request with its
headers and bodies, the console, crashes, stored values, screens, the TanStack Query cache, Redux
actions and performance events. The agent runs a command and gets plain text back, filtered down
to the rows it asked for. It can also mock a response or send a request
from inside the app. You can run the same commands yourself.
npx devtools network --where state=error --since 10m# network · my-app · iPhone 16 Pro · 2 of 214 match
id method code state ms url
fetch-1762172310455-212 POST 401 error 184 https://api.example.com/v1/session/refresh
fetch-1762172290871-198 GET 500 error 92 https://api.example.com/v1/orders
# next: --after fetch-1762172310455-212There is nothing to set up beyond the provider: the tool ships inside the package. Every query and action an agent makes, and the reply it got, is listed in the AI Usage tab.
Requirements
- Node 22 or newer on the computer. On an older Node the tool stops and names the version it needs.
- The app open in a dev build or Expo Go, with
<DevtoolsProvider>mounted andenabledon. The tool reads a development bundle only. A release build has nothing that answers it. expo startrunning. The tool finds the app through Metro, on ports 8081 to 8090. Pass--portfor another one. It talks to Metro on127.0.0.1, so run it on the computer that runs Metro.
Running it
npx devtools <command>Run it in the app's project folder, where it runs the copy you installed. Each package manager has its own form:
| Package manager | Command |
|---|---|
| npm | npx devtools <command> |
| yarn | yarn devtools <command> |
| pnpm | pnpm exec devtools <command> |
| bun | bunx devtools <command> |
- With bun, use
bunx.bun devtoolsruns adevtools.tsfile first if the folder has one, and the example app keeps its devtools config in a file of that name. - In a monorepo, or outside the app's folder,
npx devtoolscan miss the local install and fetch an unrelated npm package nameddevtools.npx @axonpack/expo-devtools <command>always runs this one.
The rest of this page writes devtools for whichever form you use.
Commands
Every flag, field, output line and error message is listed in the devtools CLI reference.
| Command | What it prints |
|---|---|
devtools apps | The apps running on Metro, and whether they have devtools |
devtools topics | What you can query in the app, with how many rows each has |
devtools <topic> [flags] | A topic's rows, newest first |
devtools get <topic> <id> | One row, whole |
devtools network <action> | Mock, block or send a request. See below |
devtools <command> --help | Every flag of one command, with examples |
apps
$ devtools apps
app name device devtools metro note
my-app My App iPhone 16 Pro 3.8.0 8081 -
admin Admin Pixel 8 - 8082 -One row per app and device. app is the Expo slug, which --app takes. device is what --device
matches part of. devtools is the version of this package in the app, - when the app doesn't
have it, and ? when it can't be checked (see Older React Native). The
command exits with 2 when no app is running.
topics
$ devtools topics
# topics · my-app · iPhone 16 Pro
topic rows actions about
network 214 override,block,unoverride,overrides,send,replay HTTP, WebSocket and SSE since launch, newest first. state=error is every failed request; get shows frames and events
console 87 - console lines since launch (last 500), newest first; repeats fold into one row
query off - TanStack Query cache: queries (id is the query hash) and mutations, last updated first
...actions lists what a topic can change in the app. off means the app turned that feature off, or
never set it up. Asking an off topic answers with
one line that says why, such as # query is off: no QueryClient in config.query.client.
A topic's rows
devtools network --where state=error --since 10m
devtools network --grep timeout --fields id,method,url,error
devtools network --where 'method=post,put' --sort -ms --limit 10
devtools network --group-by code
devtools console --where 'level=error,warn' --limit 20
devtools console --where 'level=error' --count
devtools storage --grep token
devtools navigation --after <id from "# next:">Every topic takes the same flags:
| Flag | What it does |
|---|---|
--grep <text> | Rows holding this text in any value, bodies included, any case. '/regex/i' for a regular expression. |
--where <clause> | <field><op><value>. Ops: = != > >= < <= ~ !~. ~ means contains, or takes a /regex/. Text matches ignore case, except a /regex/ without i. a,b,c means any of them. <field>? means the field has a value. Repeat --where and every clause must hold. |
--since <time> | 30s, 10m, 2h, 1d or an ISO time, on the device's clock. |
--until <time> | The same, for the other end. |
--limit <n> | How many rows to print. Default 50. |
--after <id> | Only rows newer than this one, oldest first. Poll with the # next: line. |
--before <id> | Only rows older than this one. Page with the # more: line. |
--sort <field> | -<field> for descending. Without it, newest first. A sorted list has no cursors. |
--fields <a,b,c> | The columns to print. Without it, the topic's own. A wrong name answers with the list of every field. |
--count | Only how many rows match. |
--group-by <field> | How many rows have each value of a field, most first. |
--full | Strings whole. Without it, a string over 2 KB is cut and says so. |
--json | JSON lines, one object per row, with no # lines. With no rows it prints nothing. |
--out <file> | Write the reply whole to a file, and print one line saying so. With get, the file holds the JSON alone. |
When more than one app is running, these pick one:
| Flag | What it does |
|---|---|
--app <slug> | The app's Expo slug, as devtools apps shows it. Any case. |
--device <text> | Part of the device name, any case. |
--port <n> | Look on this Metro port only. |
--force | Attach to an app that can't share its debugger. See Older React Native. |
If the flags still match several apps, the tool lists them with the flags that pick each one, and exits with 2.
get
devtools get network <id>
devtools get network <id> --out response.json
devtools get crash <id> --fullOne row as indented JSON, everything the tab has on it. Take the id from a list's id column. A
request comes with its headers, bodies and timing, a socket with its frames, a stream with its
events, a crash with its stack and breadcrumbs. A request's initiator lists each frame as a file
with its line and column, and leaves out frames from libraries unless you pass --full.
Strings over 2 KB are cut unless you pass --full. --out <file> writes the row's JSON whole to a
file, with nothing else in it, and prints # network <id>: wrote <n> chars to <file>. --json
prints it on one line. A row that has aged out of the buffer answers with an error.
Topics
Each topic is one tab. A row is whatever that tab lists, and id and t (the time) work on every
topic. devtools <topic> --fields x lists a topic's fields too, through the error it answers with.
| Topic | A row is | Columns by default | Every field |
|---|---|---|---|
network | A request, socket or event stream since launch. The last 1,000 | id method code state ms url | id t kind method code close state ms url error type size msgs source request response |
console | A console line since launch. The last 500, with repeats folded into one row | id level text | id t level text count source crash |
performance | A long task, slow interaction, mark or measure. Kept only while the Performance tab records, and it starts paused | id kind name ms | id t kind name ms handler detail |
storage | A key in a registered store. id is <store>/<key>. Rows have no time, so --since finds none | id type value | id t store key type size value error |
query | A query (id is its hash) or a mutation (mutation-<n>) in the TanStack Query cache, last updated first | id type status observers error | id t type key status observers variables data error |
navigation | A move the navigator made. The last 200 | id action from to | id t action from to path params container noop |
redux | A dispatched action and what it changed in the state. The last 200 | id type changed | id t type action changed changes |
crash | A captured crash | id kind name message | id t kind name message route previous seen |
ai-usage | A query or action an agent made, with the reply it got. The last 100 | id command matched total ms | id t command topic action matched total ms size error cut reply |
- In
network,codeis the HTTP status only, and a socket's close code isclose(-on an HTTP row).stateiserrorfor a 4xx or 5xx answer, a request that got no answer and a socket that failed, so--where state=errorlists every failure.--where 'code>=400'lists only the 4xx and 5xx answers. storagereads values the way the tab does. Each query lists every store's keys again, behind a# wait:line, so keys the app wrote since show up. A list then reads the values on the page it returns, and a--grepor a filter onvaluereads every key once. Values stay read after that, as they do in the tab.reduxkeeps the action and what it changed, never the whole state.- A tab the app has no setup for (no router, no
QueryClient, no store withdevtoolsReduxEnhancer(), no storage adapters) is anofftopic.
Output
- Tab-separated, with a header row. A value with a tab or a line break in it has them written as
\tand\n, so a row stays on one line.-is an empty value, andtprints as an ISO time. - The first line is
# <topic> · <app> · <device> · <n> of <total> match. It ends in· showing <n>when the limit cut rows off. # next: --after <id>is the cursor for the next poll. Run the same command with it to get only what is new.# more: --before <id>pages to the older rows the limit cut off.# paused: the Performance tab records only after record is pressedends an emptyperformancelist while that tab is paused.--jsonprints one JSON object per line and no#lines, so it has no cursors. With no rows it prints nothing.- An error is one line that starts with
error:.
Polling and paging
Without a cursor a list is newest first. With --after <id> it reads forward in time from that row,
oldest first, so polling with each # next: line never skips a row. A request still in flight holds
the cursor back, so the next poll shows how it ended. --sort turns the cursors off, since a sorted
list is not a run of time.
Exit codes
| Code | When |
|---|---|
0 | It answered. A topic that is off, or a query that matched nothing, is still a 0. |
1 | An error: reply (a bad flag, an unknown topic or field, no such row), Node older than 22, or no reply in 5 s. |
2 | No app found, several apps match, the app has no devtools in it, or it needs --force. |
The 5 second timeout
Each command gives up after 5 seconds. If the app never answered, the message says its JavaScript may be paused at a breakpoint: resume it and run the command again.
A topic that has to read first, such as storage listing a store's keys or reading its values,
answers with a # wait: line while it reads. The tool asks again every 200 ms within the same 5
seconds, and prints only the answer it ends on. If the read takes longer, the error says what the
app is still doing, and running the same command again picks up where it stopped.
Mocking and sending requests
The network topic takes actions too. They work on the same overrides as the Network tab, and send
through the same code as its Sandbox, so whatever an agent does shows in the tab and in React Native
DevTools.
Mock a response
devtools network override https://api.example.com/v1/me --status 500 --body '{"error":"down"}'# network · my-app · iPhone 16 Pro · override on https://api.example.com/v1/me: 500, application/json, 16 chars. The app's requests to that exact URL get it, without reaching the networkFrom then on the app's own requests to that URL get the mock, and its rows in devtools network
show it. --body-file <path> reads a longer body from a file, and --content-type sets the type
(default application/json). devtools network block <url> makes the requests fail as if offline,
devtools network overrides lists what is active, and devtools network unoverride <url> or
--all removes it. Overrides last until the app reloads.
An override matches the exact URL the app requests, scheme and query included, as the tab's own
overrides do. There are no patterns or prefixes, so copy the URL from a devtools network row.
Send a request
devtools network send GET https://api.example.com/v1/me --header 'Accept: application/json'# network · my-app · iPhone 16 Pro · sent GET https://api.example.com/v1/me as fetch-1762172310455-215. devtools get network fetch-1762172310455-215 shows it whole
id method code state ms url error
fetch-1762172310455-215 GET 200 success 392 https://api.example.com/v1/me -The request goes out from inside the app, through its own fetch, so overrides and network
conditions apply and it is a row in the log. The reply is that row, ready for devtools get network <id>. devtools network replay <id> sends a logged request again, as the Sandbox would, with the
app's own headers; --header and --body change it on the way. Every action, its arguments and
flags are in the CLI reference.
Safety rules
- Anything but GET, HEAD and OPTIONS needs
--yes. A POST, PUT, PATCH or DELETE can change data on the server, sosendandreplayrefuse it without--yes, with one line saying why. The app checks this, not only the tool, so a call through the DevTools window relay needs it too. - Replies are masked like every other reply. The new row goes through the same redaction, so an
Authorizationheader you sent shows as[redacted]. A replay sends the app's own headers to the same URL, but the agent only ever sees them masked. - Every action is recorded, refused ones too, in the AI Usage tab.
- Development only, like the rest of the tool.
Redaction
Everything the tool reads comes from the same stores the tabs read, and those redact before they
keep anything. So every redaction setting you have holds for the tool too:
network.redactHeaders and network.redact,
storage.redactKeys and storage.redact,
navigation.redact and
crash.redact.
On top of those, the tool always masks the authorization, cookie and set-cookie headers and a
page's document.cookie, as [redacted]. That holds in lists, in get, with --full and with
--out, and no flag turns it off. Filters run on the masked row, so --grep and --where can't
find a masked value either.
Storage values are shown as the tab shows them, SecureStore included. To keep one out, redact it in the config:
storage: {
adapters,
redactKeys: ['authToken', /^session\./],
},The Console, Query and Redux tabs have no redaction setting, so an agent reads what the app logged, cached or dispatched, as the tabs show it.
Older React Native
On React Native 0.86 and later, with Expo's dev server, an app takes more than one debugger at a time, so the tool connects beside an open React Native DevTools window and leaves it alone.
On React Native below 0.86, or with Expo started headless, an app takes one debugger, and a second connection closes the DevTools window open on it. There:
- With
withDevtoolsinmetro.config.js, the tool asks through the DevTools window when one is open, on that window's own connection. When no window is open it connects directly, since there is nothing to close. - Without it, the tool can't tell whether a window is open.
devtools appsshows?in the devtools column with a note, and a line under the table:# ?: checking would close a DevTools window open on the app. --force checks anyway.A query stops with exit code 2.--forceconnects anyway and closes any window open on the app.
A window opened some other way than through Metro (j in the terminal, or Open DevTools in the dev
menu) doesn't take calls, so the tool doesn't know about it, and connecting closes it.
The AI Usage tab
Every query and action an agent makes is listed in the AI Usage tab, on the device and in React Native DevTools, with the reply it got. The reply is taken after every redaction, so the tab shows exactly what left the app.
- A row shows the command as you would type it after
devtools, the time, how many rows it matched of how many the topic held, and how long the app took. An action readsactionwhere a query shows its match count. An error shows in red. - Tap a row for the topic, the reply's size and any error, then the reply itself: a JSON tree for
getor--json, text otherwise. Copy copies the reply. - Record and Clear sit in the toolbar. The tab keeps the last 100 queries, and up to 64 K characters of each reply.
appsandtopicsare not listed: they are probes, not queries.aiUsage: { disabledByDefault: true }opens the tab paused. Agents can still query and act while it is paused; what they do is just not kept.
The tab is also a topic, so you or another agent can check what was asked and answered:
devtools ai-usage --since 1h --where 'topic=network'
devtools ai-usage --where 'action?'
devtools get ai-usage q12A query of ai-usage is listed without its reply, which would otherwise repeat every earlier reply.
Every control is in the AI Usage tab reference.
Tell your agent about it
Paste this into your project's AGENTS.md or CLAUDE.md, or whatever file your agent reads first:
## Reading the running app
This app uses @axonpack/expo-devtools. With the app open in a dev build or Expo Go and
`expo start` running, read what it recorded from the terminal (Node 22+):
- `npx @axonpack/expo-devtools topics` lists what you can query, with row counts.
- `npx @axonpack/expo-devtools network --where state=error --since 10m` lists failed requests.
- `npx @axonpack/expo-devtools console --where 'level=error,warn' --limit 20` lists recent errors.
- `npx @axonpack/expo-devtools get network <id>` shows one request whole: headers, bodies, timing.
- `npx @axonpack/expo-devtools <topic> --help` lists every flag, with examples.
- `npx @axonpack/expo-devtools network --help` lists the actions: mock or block a URL, send or replay
a request from the app. Anything but GET, HEAD and OPTIONS needs `--yes`.
Pass the topic and each flag as separate arguments. Output is tab-separated, and `#` lines are a
summary or a cursor: rerun with the `# next: --after <id>` line to get only new rows. Exit code 2
means no app, or more than one: pick one with `--app <slug>` or `--device <name>`.The axonpack agent skill sends your agent to these docs, so it learns the tool from this page and
the CLI reference for the current release.
Agent skill has the command for each package manager.
npx skills add axonpack/skillsWhat to run for a common problem
| You want | Run |
|---|---|
| What failed on screen | devtools network --where state=error --since 5m, then devtools console --where 'level=error,warn' --since 5m, then devtools get network <id> on the row that matters |
| Requests that got no answer | devtools network --where 'error?' --fields id,method,url,error |
| Only HTTP 4xx and 5xx answers | devtools network --where 'code>=400' |
| Requests still waiting | devtools network --where state=pending |
| The slowest requests | devtools network --sort -ms --limit 10 --fields id,method,ms,url |
| A crash | devtools crash, then devtools get crash <id> --full |
| To watch while someone reproduces it | Run the list once, then rerun it with the # next: --after <id> line it printed. Repeat |
| A stored value | devtools storage --grep <key>, or devtools get storage <store>/<key> |
| Queries that failed | devtools query --where status=error |
| How the user reached this screen | devtools navigation --limit 10 |
| What changed the Redux state | devtools redux --where 'type~cart/' |
| How a screen handles a failing endpoint | devtools network override <exact url> --status 500 --body '{"error":"down"}', open the screen, read devtools console, then devtools network unoverride --all |
| Whether an endpoint answers from the app | devtools network send GET <url>, then devtools get network <id> for the body |
Performance has rows only while its tab records, and it starts paused, so press record in the Performance tab first.
When it doesn't answer
| Message | What to do |
|---|---|
no app found on Metro ports 8081 to 8090 | Start expo start and open the app, or pass --port for Metro on another port. |
... has no devtools agent | Mount <DevtoolsProvider> with enabled on, and run a development bundle. A release build has no agent. |
no reply from ... in 5 s | The app's JavaScript may be paused at a breakpoint. Resume it and run the command again. |
"network --limit 8" isn't a topic | The whole command reached the tool as one argument. Pass the topic and each flag as separate arguments. |
2 apps match. Pick one: | Add one of the --app and --device pairs it lists. |
... can't share its debugger ... | See Older React Native. |
POST can change data on the server, so it needs --yes | Add --yes if you mean to send it. GET, HEAD and OPTIONS need none. |
still sending ... after 5 s | The request carries on in the app. devtools ai-usage --where 'action?' shows how it ended. |
Limits
- It acts on the network only. Mocking, blocking, sending and replaying requests are the only changes it makes. Nothing can be cleared or navigated from the terminal.
- It sees what the tabs keep. A row that aged out of a tab's buffer is gone for the tool too, and Performance has rows only while its tab records.
- Development only. A release build installs nothing the tool can talk to.