Skip to content
Back to skills

Firefox Remote Debug

ASecurity

Use when something is needed out of a page open in Firefox on this device -- its JSON, its DOM, a value only the loaded page holds -- or when a debugger socket, `adb forward`, `evaluateJSAsync` or a `longString` reply is involved. For a page you go to yourself rather than one the operator has open, use headless-browser.

  • 3 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 25, 2026
ai-agentsjavascriptpythongojavashelldebugging

Works with

  • cli

Security analysis

A100/100

Scanned September 25, 2026

npx -y skills add nev3rfail/harness.apk --skill firefox-remote-debug --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Firefox Remote Debug?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Firefox Remote Debug
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nev3rfail-firefox-remote-debug/badge)](https://www.skillsdirectory.com/skills/nev3rfail-firefox-remote-debug)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: firefox-remote-debug
description: Use when something is needed out of a page open in Firefox on this device -- its JSON, its DOM, a value only the loaded page holds -- or when a debugger socket, `adb forward`, `evaluateJSAsync` or a `longString` reply is involved. For a page you go to yourself rather than one the operator has open, use headless-browser.
---

# Reading a Firefox page on this device

Firefox exposes its remote debugging protocol on an abstract unix socket, and
that socket lets you evaluate JavaScript in a live page and read the result back.
It is the way to get a payload the page already holds without a share link, a
screenshot, or the operator reading anything off the screen.

This is for a page the operator already has open. For a page *you* go to, that is
`headless-browser`.

## What does not work from in here

Both of these are refused, and both look like a bug rather than a policy:

```
open("/proc/net/unix")                              -> Permission denied
connect("\0org.mozilla.firefox/firefox-debugger-socket") -> Permission denied
```

The app's uid may neither list the socket nor connect to it. Do not spend time on
socket-name guessing or on `LD_PRELOAD` tricks: the path is `adb`, which runs
with a uid that is allowed both.

## The route

The operator turns the socket on once, in Firefox: **Settings -> Advanced ->
Remote debugging via USB**. The label says USB; the toggle is what creates the
socket, and USB is not involved.

Getting adb pointed at this device is `self-adb`, and it is a prerequisite
rather than a step here: `127.0.0.1:5555` answers only once `tcpip 5555` has
been pinned, and that property resets on every reboot. Follow that skill first,
then the two calls this one needs:

```sh
D="adb-mdns -s 127.0.0.1:5555"
$D shell 'cat /proc/net/unix | grep firefox'
$D forward tcp:6080 localabstract:org.mozilla.firefox/firefox-debugger-socket
```

The socket's name carries the package, so it is `org.mozilla.fenix/...` for
Nightly. Several transports can be connected at once, so always pass `-s`. After
the forward, `127.0.0.1:6080` is an ordinary TCP socket any language can open.

## The protocol

One packet is `<byte-length>:<json>`, where the length counts UTF-8 bytes of the
JSON that follows. The server sends an unsolicited hello from `root` on connect,
and pushes console and navigation events at any time -- so never treat the next
packet as your reply. Match on `from` and `type`.

```
-> {"to":"root","type":"listTabs"}
<- {"from":"root","tabs":[{"actor":"server0.conn0.tabDescriptor1","url":"...","selected":true}]}

-> {"to":"server0.conn0.tabDescriptor1","type":"getTarget"}
<- {"from":"...","frame":{"consoleActor":"server0.conn0.child2/consoleActor3", ...}}

-> {"to":"...consoleActor3","type":"evaluateJSAsync","text":"JSON.stringify(...)"}
<- {"from":"...consoleActor3","resultID":"1"}
<- {"from":"...consoleActor3","type":"evaluationResult","result":<string or grip>}
```

**A large result does not arrive whole.** It comes as a grip -- `{"type":
"longString", "actor": ..., "length": N, "initial": "<first slice>"}` -- and the
rest is asked for a slice at a time. Derive the first offset from the length of
`initial` rather than assuming its size:

```python
parts, pos = [grip["initial"]], len(grip["initial"])
while pos < grip["length"]:
    end = min(pos + 32768, grip["length"])
    send({"to": grip["actor"], "type": "substring", "start": pos, "end": end})
    parts.append(await_reply()["substring"]); pos = end
send({"to": grip["actor"], "type": "release"})
```

A short result is a plain JSON string instead of a grip. Handle both.

## Client

```python
import socket, json

class RDP:
    def __init__(self, port=6080):
        self.s = socket.create_connection(("127.0.0.1", port), timeout=20)
        self.buf = b""
        self.recv()                      # the root hello

    def recv(self):
        while b":" not in self.buf:
            self.buf += self._read()
        head, rest = self.buf.split(b":", 1)
        n = int(head)
        while len(rest) < n:
            rest += self._read()
        self.buf = rest[n:]
        return json.loads(rest[:n].decode())

    def _read(self):
        d = self.s.recv(65536)
        if not d:
            raise EOFError
        return d

    def send(self, packet):
        b = json.dumps(packet).encode()
        self.s.sendall(str(len(b)).encode() + b":" + b)

    def ask(self, packet, match):
        self.send(packet)
        while True:
            p = self.recv()
            if p.get("error") or match(p):
                return p

    def evaluate(self, console, text):
        return self.ask(
            {"to": console, "type": "evaluateJSAsync", "text": text},
            lambda p: p.get("type") == "evaluationResult",
        ).get("result")
```

## Getting at the payload

Prefer what the page already holds over asking the network again:

- A global, or a framework's cache -- a query client, a store, `__NEXT_DATA__`.
  Walk to it once, stash the result as a string on `window`, then read that
  string. Nothing is re-fetched and nothing new is sent anywhere.
- A credentialed `fetch` from inside the page works, but it is a fresh request
  with the operator's session, and this session's permission rules may refuse
  it. Reach for it only when the page kept nothing.

## Common mistakes

| Mistake | What is true |
|---|---|
| Connecting to the abstract socket from here | Refused for the app's uid. Forward it with `adb` and use TCP |
| Reading `/proc/net/unix` to find the name | Refused too. Read it through `adb shell` |
| Treating the next packet as the reply | Events interleave. Match on `from` and `type` |
| Expecting a big string in one reply | It is a `longString` grip; page it with `substring` |
| Re-fetching the endpoint by reflex | The page usually still holds it, and a fetch spends the operator's session |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.