Axonpack

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-212

There 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 and enabled on. The tool reads a development bundle only. A release build has nothing that answers it.
  • expo start running. The tool finds the app through Metro, on ports 8081 to 8090. Pass --port for another one. It talks to Metro on 127.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 managerCommand
npmnpx devtools <command>
yarnyarn devtools <command>
pnpmpnpm exec devtools <command>
bunbunx devtools <command>
  • With bun, use bunx. bun devtools runs a devtools.ts file 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 devtools can miss the local install and fetch an unrelated npm package named devtools. 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.

CommandWhat it prints
devtools appsThe apps running on Metro, and whether they have devtools
devtools topicsWhat 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> --helpEvery 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:

FlagWhat 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.
--countOnly how many rows match.
--group-by <field>How many rows have each value of a field, most first.
--fullStrings whole. Without it, a string over 2 KB is cut and says so.
--jsonJSON 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:

FlagWhat 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.
--forceAttach 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> --full

One 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.

TopicA row isColumns by defaultEvery field
networkA request, socket or event stream since launch. The last 1,000id method code state ms urlid t kind method code close state ms url error type size msgs source request response
consoleA console line since launch. The last 500, with repeats folded into one rowid level textid t level text count source crash
performanceA long task, slow interaction, mark or measure. Kept only while the Performance tab records, and it starts pausedid kind name msid t kind name ms handler detail
storageA key in a registered store. id is <store>/<key>. Rows have no time, so --since finds noneid type valueid t store key type size value error
queryA query (id is its hash) or a mutation (mutation-<n>) in the TanStack Query cache, last updated firstid type status observers errorid t type key status observers variables data error
navigationA move the navigator made. The last 200id action from toid t action from to path params container noop
reduxA dispatched action and what it changed in the state. The last 200id type changedid t type action changed changes
crashA captured crashid kind name messageid t kind name message route previous seen
ai-usageA query or action an agent made, with the reply it got. The last 100id command matched total msid t command topic action matched total ms size error cut reply
  • In network, code is the HTTP status only, and a socket's close code is close (- on an HTTP row). state is error for a 4xx or 5xx answer, a request that got no answer and a socket that failed, so --where state=error lists every failure. --where 'code>=400' lists only the 4xx and 5xx answers.
  • storage reads 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 --grep or a filter on value reads every key once. Values stay read after that, as they do in the tab.
  • redux keeps the action and what it changed, never the whole state.
  • A tab the app has no setup for (no router, no QueryClient, no store with devtoolsReduxEnhancer(), no storage adapters) is an off topic.

Output

  • Tab-separated, with a header row. A value with a tab or a line break in it has them written as \t and \n, so a row stays on one line. - is an empty value, and t prints 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 pressed ends an empty performance list while that tab is paused.
  • --json prints 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

CodeWhen
0It answered. A topic that is off, or a query that matched nothing, is still a 0.
1An error: reply (a bad flag, an unknown topic or field, no such row), Node older than 22, or no reply in 5 s.
2No 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 network

From 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, so send and replay refuse 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 Authorization header 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:

devtools.ts
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 withDevtools in metro.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 apps shows ? 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. --force connects 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 reads action where 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 get or --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.
  • apps and topics are 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 q12

A 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/skills

What to run for a common problem

You wantRun
What failed on screendevtools 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 answerdevtools network --where 'error?' --fields id,method,url,error
Only HTTP 4xx and 5xx answersdevtools network --where 'code>=400'
Requests still waitingdevtools network --where state=pending
The slowest requestsdevtools network --sort -ms --limit 10 --fields id,method,ms,url
A crashdevtools crash, then devtools get crash <id> --full
To watch while someone reproduces itRun the list once, then rerun it with the # next: --after <id> line it printed. Repeat
A stored valuedevtools storage --grep <key>, or devtools get storage <store>/<key>
Queries that faileddevtools query --where status=error
How the user reached this screendevtools navigation --limit 10
What changed the Redux statedevtools redux --where 'type~cart/'
How a screen handles a failing endpointdevtools 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 appdevtools 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

MessageWhat to do
no app found on Metro ports 8081 to 8090Start expo start and open the app, or pass --port for Metro on another port.
... has no devtools agentMount <DevtoolsProvider> with enabled on, and run a development bundle. A release build has no agent.
no reply from ... in 5 sThe app's JavaScript may be paused at a breakpoint. Resume it and run the command again.
"network --limit 8" isn't a topicThe 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 --yesAdd --yes if you mean to send it. GET, HEAD and OPTIONS need none.
still sending ... after 5 sThe 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.

On this page