Notes → iOS Web Inspector protocol
What iOS Safari's Web Inspector protocol does not tell you
Written 10 September 2026. Verified against CrocProbe 1.0.8.
Safari on iPhone speaks WebKit's remote inspector protocol, and ios-webkit-debug-proxy carries it over USB to a local port. None of what follows is documented. Every item on this page was established by probing a real iPhone on iOS 26.6, and it is written down here so that the next person can skip the probing.
CSS.enable poisons the session
Send CSS.enable to the page target and the target stops answering everything, including DOM calls that worked a second earlier. It does not merely fail. Nothing sent after it gets a reply.
Never send it. Computed styles are still available: call Runtime.callFunctionOn against the element and read getComputedStyle inside the function.
Frame targets acknowledge and never reply
The inspector channel is multi-target. Every command must be wrapped in Target.sendMessageToTarget, and every reply arrives inside Target.dispatchMessageFromTarget. The proxy does not unwrap either direction for you.
Only the page target answers. A page with iframes announces many targets. One news site announced fifteen, and eleven frame-* targets arrived before the single page-* target. Every frame target acknowledged the command envelope and then never replied.
A client that latches the first Target.targetCreated it sees therefore hangs on any page carrying iframes, which is most of the web. Wait for the target whose type is page and send commands to that one.
A background tab accepts commands and never runs them
iOS suspends JavaScript in tabs that are not on screen. Runtime.evaluate against a background tab is acknowledged at the envelope level and then silently never executes. There is no error. The command hangs indefinitely.
Only the tab actually showing on the phone is inspectable. The page is still listed, the session still opens, and the envelope is still acknowledged, so every signal a client normally reads as alive says yes. This is the single most confusing failure in the whole workflow. If you write a client, make its timeout message say so, because a generic timeout sends the reader to the wrong place.
The user-facing side of this problem has its own page: connected, listed, and running nothing.
DOM.getDocument is the real enable step
DOM.enable and DOM.getBoxModel do not exist. Both answer was not found.
DOM.getDocument is the de-facto enable step. It builds the backend's node map, and until it has been called once, DOM.requestNode, the bridge from a Runtime objectId to a DOM nodeId that every selection depends on, fails with an empty error message. Call it once before any node lookup.
DOM.requestChildNodes answers {} and delivers the nodes as a separate DOM.setChildNodes event. Wait for the event, not the reply.
Two related points come from reading WebKit's own InspectorDOMAgent rather than from probing. Calling DOM.getDocument again discards every node id issued before it, and using an old id afterwards fails with Missing node for given nodeId, or, for DOM.requestChildNodes, with nothing at all. And DOM.setChildNodes is sent once per document read: a second DOM.requestChildNodes for the same node answers {} and sends no event, because the backend assumes you kept the first delivery.
Smaller things that also cost an afternoon
- It is not Chrome DevTools. Page.navigate and Page.reload return -32601. Navigation goes through evaluated JavaScript. Page.snapshotRect does work, and that is how screenshots are taken.
- Inspector.enable is required before the element picker. Without it, DOM.setInspectModeEnabled returns OK and silently delivers no taps.
- Inspect mode blocks scrolling and does not swallow the tap. While it is armed the page cannot be panned, and the tap still reaches the page, so links navigate. Arm it for exactly one pick and disarm it the instant it fires.
- Only one inspector connection per page. Opening a second session evicts the first. If you poll pages for liveness by opening a session, skip any page that already has one open, or you will evict the working session on a timer.
- Navigation swaps the target. A cross-document navigation invalidates the current target. A later Target.targetCreated of type page is the replacement, and later commands go to it.
- Inspectability is fragile. Lock the screen or background Safari and the page list empties instantly, even though the device stays visible.