Skip to content

Instantly share code, notes, and snippets.

@nicholaswmin
Last active February 3, 2026 11:49
Show Gist options
  • Select an option

  • Save nicholaswmin/5ab1cbe4f61fb6580e21221a9f1164cb to your computer and use it in GitHub Desktop.

Select an option

Save nicholaswmin/5ab1cbe4f61fb6580e21221a9f1164cb to your computer and use it in GitHub Desktop.
iOS Safari remote control via WIR

WebKit Inspector - iOS Simulator

Remote control of iOS Safari Web Inspector.
Works via socket, injecting commands following WIR (WebKit Inspector Remote) -
an internal yet not secret protocol that mimics CDP.
Less brittle than Appium, no dependencies.

Quick Start

  1. Boot simulator and open page:

    xcrun simctl boot <UDID>
    open -a Simulator
    xcrun simctl openurl booted "https://example.com"
  2. Find the socket:

    find /private/tmp -name "*webinspectord_sim*" 2>/dev/null
  3. Connect and send commands using WIR Protocol

  4. Execute JS using Runtime.evaluate

Terms

WIR Protocol

Binary plist messages over Unix socket with 4-byte big-endian length prefix.

Connect:

import socket, struct, plistlib

SOCK = "/private/tmp/com.apple.launchd.xxx/com.apple.webinspectord_sim.socket"
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(SOCK)
sock.settimeout(2.0)

def send(msg):
    data = plistlib.dumps(msg, fmt=plistlib.FMT_BINARY)
    sock.sendall(struct.pack('>I', len(data)) + data)

Handshake sequence:

# 1. Register connection
send({'__selector': '_rpc_reportIdentifier:',
      '__argument': {'WIRConnectionIdentifierKey': 'my-client'}})

# 2. Get apps
send({'__selector': '_rpc_getConnectedApplications:', '__argument': {}})

# 3. Get pages (after finding app_id from response)
send({'__selector': '_rpc_forwardGetListing:',
      '__argument': {'WIRConnectionIdentifierKey': 'my-client',
                     'WIRApplicationIdentifierKey': app_id}})

# 4. Setup inspector socket (after finding page_id)
send({'__selector': '_rpc_forwardSocketSetup:',
      '__argument': {'WIRConnectionIdentifierKey': 'my-client',
                     'WIRApplicationIdentifierKey': app_id,
                     'WIRPageIdentifierKey': page_id,
                     'WIRSenderKey': sender_id,
                     'WIRAutomaticallyPause': False}})

Runtime.evaluate

Execute JS in the browser context via CDP wrapped in WIR.

import json

def evaluate(expr):
    # If target_id exists, wrap in Target.sendMessageToTarget
    inner = json.dumps({
        'id': msg_id,
        'method': 'Runtime.evaluate',
        'params': {'expression': expr, 'returnByValue': True}
    })

    socket_data = json.dumps({
        'id': outer_id,
        'method': 'Target.sendMessageToTarget',
        'params': {'targetId': target_id, 'message': inner}
    }).encode()

    send({
        '__selector': '_rpc_forwardSocketData:',
        '__argument': {
            'WIRConnectionIdentifierKey': 'my-client',
            'WIRApplicationIdentifierKey': app_id,
            'WIRPageIdentifierKey': page_id,
            'WIRSenderKey': sender_id,
            'WIRSocketDataKey': socket_data
        }
    })

Gotchas

Find socket dynamically

Path changes on restart; always find, never hardcode.

Connection refused

Web Inspector not enabled: Settings → Safari → Advanced → Web Inspector ON

"No page found"

Page still loading, wait 10-15s and retry.

target_id required

Extract from Target.targetCreated event in setup response; needed to wrap CDP commands in Target.sendMessageToTarget.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment