Skip to content

About

Local-first observability and control bridge for Zen Browser

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

Zen Debug Bridge

Buy me a coffee

☕ Like this project? Support the author on Buy Me a Coffee

Local bridge and WebExtension for monitoring, diagnostics, and explicitly permitted control of Zen Browser/Firefox. The project provides a human or AI agent with structured access to selected tabs—or, after a separate dangerous confirmation, to all available tabs: DOM snapshot, navigation, synthetic actions, console, network, performance, screenshots, and browser events.

Current version: 0.1.7. Command protocol: 1.

The project is local-first:

  • The bridge listens only on 127.0.0.1:18765 by default;
  • HTTP, SSE, and WebSocket are secured with a random 256-bit token;
  • Data is not sent to the cloud by this project's code;
  • The bridge connection itself does not open tabs for the agent;
  • Secure standard mode requires manually granting each tab Observe or Control;
  • A separate persistent setting ALL ACCESS after explicit confirmation grants Control to all existing and new tabs;
  • Regular per-tab grants live only in memory; ALL ACCESS is saved as a setting until the user turns it off or clicks Stop all.

ALL ACCESS — dangerous mode. It is intended for short trusted local debug sessions. It extends the internal grant of Zen Debug Bridge, but does not bypass Firefox limitations: DOM about:/browser UI, foreign extension realms, their private console and storage remain inaccessible.

Important for AI agent: this project is completely independent of Kimi WebBridge, Chrome DevTools bridge, and any third-party browser-control extensions. There is no need to install, run, or check Kimi WebBridge. For normal operation, use only the CLI/HTTP API Zen Debug Bridge from this repository. Selenium, WebDriver BiDi, and Firefox DevTools are needed only for a separate Deep Debug scenario.

Contents

Brief contract for AI agent

If this README is read by an AI, use the following order. It is a safe standard workflow for the project.

1. Working directory

cd ~/Projects/zen-debug-bridge

Do not search for another browser bridge until the user explicitly requests to use a different tool.

2. Check the Bridge and Extension

npm run status
node bridge/cli.mjs call ping
node bridge/cli.mjs call capabilities
node bridge/cli.mjs call get_state

Interpretation:

  • extension_not_connected — bridge is running but the extension is not connected;
  • bridgeStatus: "connected" — transport is working;
  • sessionMode: "off" — no tabs have been granted yet;
  • sessionMode: "observe" — read-only commands are available;
  • sessionMode: "control" — at least one tab has been granted Control;
  • allAccess: true — the user has enabled persistent global Control for all tabs that the WebExtension can see;
  • allAccessTabs — the number of live tabs that have entered the current global-access lifecycle;
  • trackedTabs: 0 — the agent should not attempt to read the list of regular tabs;
  • technicalConsent: true — performance/extension metadata capabilities are enabled.

Never ask the user to send a token in chat. The CLI reads the local token file itself.

3. If Access Is Disabled, Request Manual Grant

The user must open a regular page http:, https: or file:, click the Zen Debug Bridge icon, and select:

  • Observe — reading DOM, screenshot, console, network, and events;
  • Control — everything from Observe plus navigation, opening/closing tabs, and DOM actions.

Do not bypass this check via OS commands, another browser bridge, or changing storage. Manual grant is part of the security model.

If get_state already shows allAccess: true, an additional per-tab grant is not needed: all tabs visible to the extension have effective Control. The AI should not enable ALL ACCESS on its own, change the extension storage for this purpose, or persuade the user to leave the mode enabled. Enabling is done only by a person in Settings → High-risk capabilities, with a separate confirm dialog.

4. Get Available Tabs

node bridge/cli.mjs call list_tabs '{}'

By default, the command returns only explicitly granted tabs and tabs created by the agent. With allAccess: true, every tab available to the WebExtension is tracked, including new tabs. Do not use IDs from an old response after closing or restarting the browser.

5. Take a Snapshot Before Action

node bridge/cli.mjs call snapshot '{"tabId":123}'

Prefer semantic refs of the form 0:e7 over CSS selectors. The ref is bound to a specific frame and the last snapshot of that frame. After navigation, major DOM update, or error STALE_ELEMENT_REF, get a new snapshot.

6. Perform Minimal Action

node bridge/cli.mjs call click '{"tabId":123,"ref":"0:e7"}'
node bridge/cli.mjs call fill '{"tabId":123,"ref":"0:e8","value":"example"}'

After an action, call snapshot, list_console or list_network again to confirm the result. Do not consider the absence of a transport error as proof that the site accepted synthetic input.

7. Session-Level Control Is Needed for New Tabs

node bridge/cli.mjs call open_tab '{"url":"https://example.com","active":true}'

Firefox sometimes returns an intermediate about:blank from tabs.create(). Verify the actual URL with a repeated list_tabs or snapshot.

8. Respect User Authorization Boundaries

Control — including global Control through ALL ACCESS — means technical capability, not universal user consent for any actions. Without a separate explicit request, you cannot:

  • send messages, forms, or comments;
  • delete data;
  • purchase, publish, or confirm transactions;
  • change account settings;
  • disclose tokens, cookies, personal data, or content of other tabs.

9. Ending the Session

The user can click Stop tab or Stop all. In ALL ACCESS mode, the per-tab Stop tab control is intentionally disabled; Stop all immediately turns off global mode, saves allAccess=false, revokes all grants, and clears runtime buffers. The AI should treat SESSION_ACCESS_DENIED, TAB_ACCESS_DENIED, and ALL_ACCESS_ACTIVE as normal boundaries rather than errors to bypass.

Implemented Features

Observation

  • semantic DOM snapshot with accessibility-like tree;
  • refs with frame prefix, for example 0:e12 or 5:e3;
  • traversal of regular DOM and open Shadow DOM roots;
  • snapshots of multiple available frames, including cross-origin frames;
  • optional reading of current form control values;
  • interception of console.debug/log/info/warn/error/trace in page realm;
  • window.error and unhandledrejection;
  • lifecycle network requests, statuses, headers, redirects, timings, and errors;
  • optional capture of text request/response bodies;
  • longtask, Largest Contentful Paint, and layout shift entries;
  • batched DOM mutations;
  • visible-viewport screenshots in PNG/JPEG format;
  • metadata of installed extensions and lifecycle events via management API;
  • diagnosis of sizes, loading, and errors of popups.

Control

  • list and search for available tabs;
  • open a new tab;
  • activate a tab and focus the window;
  • navigate to URL;
  • reload with optional bypass cache;
  • back/forward;
  • close a tab;
  • click by ref or CSS selector;
  • fill for input, textarea, select, and contenteditable;
  • hover;
  • scroll page or element;
  • optional JavaScript evaluation in isolated content-script realm;
  • open about:debugging for manual Inspect.

Integration

  • HTTP command API;
  • WebSocket between extension and bridge;
  • server-side ring buffer events;
  • real-time SSE stream;
  • CLI zdb/bridge/cli.mjs;
  • multiple browser clients with selection by clientId;
  • systemd user unit;
  • ZIP/XPI build without local pairing secret;
  • separate launch scripts for Zen Flatpak Deep Debug.

Architecture

┌──────────────────────────────── Zen / Firefox ────────────────────────────────┐
│                                                                               │
│  page realm ── page-hook.js ── window.postMessage ── content.js (all frames) │
│                                                       │                       │
│                                                       │ runtime messages      │
│                                                       ▼                       │
│  popup/options ─────────────────────────────── persistent background          │
│                                              core + commands + network        │
│                                                       │                       │
└───────────────────────────────────────────────────────┼───────────────────────┘
                                                        │ authenticated WS
                                                        │ /extension?token=…
                                                        ▼
                                           Node.js bridge on 127.0.0.1:18765
                                                        │
                              ┌─────────────────────────┼──────────────────────┐
                              │                         │                      │
                              ▼                         ▼                      ▼
                         CLI / AI agent             HTTP API              SSE clients

Components:

  • page-hook.js operates in the JavaScript realm of the page and wraps the console API;
  • content.js operates in an isolated WebExtension realm for each available frame;
  • background stores grants, ring buffers, and WebSocket connection;
  • Node bridge authenticates clients, routes commands, and stores a second ring buffer of events;
  • CLI is a thin HTTP API client that automatically reads the local token.

Manifest and Browser Permissions

The extension uses Manifest V2 with a persistent background. Firefox extension ID: zen-debug-bridge@local.dev.

Minimum versions in the manifest:

  • desktop Gecko: 140.0;
  • Gecko Android: 142.0.
Permission Purpose
<all_urls> Content script in frames of user-selected websites and network observation
activeTab Context of the active tab after user interaction with the popup
management Metadata/lifecycle of installed extensions with separate opt-in
storage Settings and stable instanceId
tabs Tab metadata, create/update/remove/captureVisibleTab
tabGroups Native purple AI group for tabs created through the bridge
webNavigation Frame enumeration and navigation events
webRequest Request lifecycle, headers, and optional body capture
webRequestBlocking Access to the extended webRequest surface; the project does not block or modify traffic

The content script is declared with run_at: document_start, all_frames: true, and match_about_blank: true. Actual instrumentation remains disabled until the tab receives Observe/Control.

The manifest declares Firefox data collection permissions because data exits from the add-on into a separate local Node process:

  • required: browsingActivity, websiteActivity, websiteContent;
  • optional: technicalAndInteraction.

Optional technical permission is used for performance entries, extension inventory, and passing the browser name/version. The presence of an API permission in the manifest does not cancel the internal model of grants: without Observe/Control, tab data is not collected by the bridge.

Extension Content Security Policy: script-src 'self'; object-src 'none'. Remote scripts are not loaded.

Access Model

Per-Tab Modes

Mode What is Allowed What is Forbidden
off ping, capabilities/state, and popup diagnostics Tab metadata, DOM, telemetry, and management
observe Snapshot, screenshot, console, network, page events, extension inventory Navigation, click/fill/hover/scroll, evaluate, close
control Everything from Observe plus control commands Privileged pages and trusted OS input are still unavailable

Each issued tab has its own mode. sessionMode is equal to the maximum mode among all issued tabs:

  • one Observe tab gives sessionMode=observe;
  • the presence of at least one Control tab gives sessionMode=control;
  • closing the last Control tab reduces the session to Observe;
  • revoking the last tab returns off.

open_tab checks for session-level Control. A new tab created by the agent automatically receives Control and is marked as agentCreated: true. Other commands additionally check the mode of the specific target tab.

ALL ACCESS — Dangerous Global Mode

allAccess=true is an intentional exception to the per-tab model. It can only be enabled in Settings → High-risk capabilities → ALL ACCESS. When transitioning to false → true, the options page shows a separate confirmation with access description; background additionally rejects UI requests without confirmAllAccess=true with code ALL_ACCESS_CONFIRMATION_REQUIRED.

After enabling, the extension:

  1. retrieves a list of all tabs visible to WebExtension through browser.tabs.query({});
  2. remembers the previous mode for each tab (off, observe, or control);
  3. sets an effective control for each tab;
  4. automatically adds each new tab to Control through tabs.onCreated;
  5. reconfigures content instrumentation after URL/status updates;
  6. maintains sessionMode=control while the global mode is active.

Public state fields:

  • allAccess — effective runtime state of the mode;
  • allAccessTabs — number of current tab records for which the previous mode is saved;
  • trackedTabs — number of tabs currently accessible to commands;
  • capabilities.allAccess — machine-readable duplicate of effective capability.

During normal shutdown of ALL ACCESS, background restores saved per-tab modes. Tabs that were Off before enabling or appeared during global mode again become Off; their collected runtime data is cleared. Tabs with previous Observe/Control return to it. Closed tabs are simply removed from bookkeeping.

During ALL ACCESS:

  • badge for each controlled tab shows a red ALL;
  • popup displays ALL ACCESS, disables Observe/Control/Stop tab, and offers Settings or Stop all;
  • a per-tab revoke request returns ALL_ACCESS_ACTIVE because it contradicts the global guarantee;
  • manual grant through the internal API changes the mode that will be restored after shutdown, but effective mode remains Control;
  • Stop all is an emergency switch: it also persistently sets allAccess=false.

ALL ACCESS means "all tabs that Firefox allows this WebExtension to see," not browser process privileges. It does not provide DOM access to browser chrome, most about:/Mozilla-protected pages, private contexts of other extensions or their background console/storage. Private-window tabs fall within scope only if the user separately grants the extension to work in Private Windows. Metadata and browser-level operations with some protected tabs may be accessible even when content-script DOM actions are unavailable.

Grant Lifespan

Normal per-tab grants are not written to browser.storage and are reset upon:

  • restarting Zen;
  • reloading temporary extension;
  • uninstalling/reinstalling the extension;
  • clicking Stop tab or Stop all.

Settings, token, and instanceId are saved separately and can survive reload. allAccess is also a saved setting: if it was left enabled, after restart/reload the extension will again apply Control to all tabs that it sees in the new runtime session. Stop all disables and saves it as false.

Badges

  • OBS — active tab granted Observe;
  • CTL — active tab granted Control;
  • red ALL — ALL ACCESS is enabled and the tab falls under global Control;
  • ! — bridge is not connected, and the tab does not have a grant;
  • empty badge — connected, current tab is Off.

Installation and Launch

Requirements

  • Linux with Zen Browser or compatible Firefox;
  • Node.js >=20.19.0;
  • npm;
  • zip and jq for building;
  • for the described Flatpak workflow — application app.zen_browser.zen.

Installing Node Dependency

cd ~/Projects/zen-debug-bridge
npm install

Runtime dependency is one: package ws.

Launching bridge in the foreground

cd ~/Projects/zen-debug-bridge
npm start

Expected output:

[zen-debug-bridge] listening on http://127.0.0.1:18765
[zen-debug-bridge] token file: /home/<user>/.local/share/zen-debug-bridge/token

The first launch creates:

  • a directory at ~/.local/share/zen-debug-bridge with mode 0700;
  • a token of 32 random bytes, recorded as 64 hex characters;
  • a file named token with mode 0600.

Development Mode

npm run dev

Node restarts the bridge upon changes to server-side files. The extension still needs to be reloaded manually via about:debugging.

systemd User Service

The unit assumes that the repository is located at %h/Projects/zen-debug-bridge. If the path is different, first change WorkingDirectory and ExecStart.

mkdir -p ~/.config/systemd/user
cp systemd/zen-debug-bridge.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now zen-debug-bridge.service
systemctl --user status zen-debug-bridge.service

The unit limits filesystem access through ProtectSystem=strict, makes home read-only, and allows writing only to ~/.local/share/zen-debug-bridge.

Obtaining the Token

npm run token

The command prints the secret. Do not insert it into an issue, README, agent log, or public shell transcript.

Recommended Temporary Installation in Zen Flatpak

Zen Flatpak has an important peculiarity: selecting manifest.json through the file portal may give the sandbox access to that file alone, but not to neighboring JS/CSS files. Symptoms include an empty icon, a tiny white popup, and a background script that does not connect. Therefore, use a single ZIP/XPI located in ~/Downloads for Flatpak.

Build a secure archive without the token:

cd ~/Projects/zen-debug-bridge
npm run build
VERSION="$(jq -r .version extension/manifest.json)"
cp "artifacts/zen-debug-bridge-$VERSION.zip" \
  "$HOME/Downloads/zen-debug-bridge-$VERSION.xpi"

Then:

  1. Open about:debugging#/runtime/this-firefox.
  2. Click Load Temporary Add-on….
  3. Select ~/Downloads/zen-debug-bridge-<version>.xpi.
  4. Open the extension's Settings.
  5. Specify ws://127.0.0.1:18765/extension.
  6. Paste the result of npm run token.
  7. Save the settings and check the status of CONNECTED.

The temporary add-on is removed upon a full browser restart and must be reloaded again.

Unpacked Installation Outside Flatpak

If the browser has normal filesystem access:

  1. Open about:debugging#/runtime/this-firefox.
  2. Click Load Temporary Add-on….
  3. Select extension/manifest.json.
  4. Configure URL/token.

This option is not recommended for Zen Flatpak due to file portal sandboxing.

Local Pairing File

For a fully local development build, the extension can read extension/pairing.local.json:

{
  "bridgeUrl": "ws://127.0.0.1:18765/extension",
  "bridgeToken": "<64-hex-characters>"
}

Properties:

  • The file is ignored by Git;
  • npm run build always excludes it from the standard ZIP;
  • During initialization, a correct local pairing has priority over storage;
  • pairingSource in get_state will be local-file;
  • If the user manually saves the token in Settings, the current runtime may show storage, but after a reload, the local file again has priority.

If you package pairing.local.json into a private XPI, this XPI contains a working secret. Store it with mode 0600, locally only, do not attach to issues, and do not publish as an artifact.

Permanent Installation

The standard ZIP/XPI of this project is not signed. For permanent installation in standard Firefox/Zen, a Mozilla AMO signing process is needed, such as unlisted distribution. Disabling signature enforcement in the main profile is not recommended.

Extension Interface

Popup

Popup displays:

  • connection status;
  • mode of the current tab;
  • title and URL of the active tab;
  • a live AI activity card for commands currently executing through the local bridge, with a minimal recent history;
  • Observe, Control, Stop tab buttons;
  • quick toggles for Console, Network, Performance, DOM mutations;
  • warning about high-risk settings;
  • Reconnect, Settings, Stop all.

Observe/Control are only active for regular http:, https: and file: pages. about:, browser UI, AMO-protected pages, and other privileged documents cannot be instrumented by a normal extension.

When ALL ACCESS is enabled, the popup uses a red mode badge, shows a permanent warning, and blocks per-tab buttons. Stop all remains available and disables ALL ACCESS along with all grants. The switch itself is only on the full Settings page to prevent global access from being accidentally enabled in the compact popup.

The activity card is intentionally evidence-based: the animated state starts only after the extension actually receives a bridge command and completes or fails when that command returns. It stores and renders command metadata only (action, tab ID, timing, outcome), never command arguments, page content, selectors, or form values. It also respects prefers-reduced-motion.

On-page action overlay

Version 0.1.6 adds a visible animation layer directly inside every granted page. It is powered by the bundled local copy of Anime.js; no remote script or page data is used. The overlay has pointer-events: none, so it cannot intercept a click or typed input.

It makes these actions observable while they happen:

  • scanning line while the agent reads a DOM snapshot;
  • element spotlight and animated cursor for hover, click, and text entry;
  • scrolling, navigation, reload, history, screenshot, and tab-transition indicators;
  • neutral inspection indicator for console, network, event, and isolated-realm evaluation calls.

The overlay deliberately uses fixed action labels only. It does not display form values, selectors, URLs, page text, or command arguments. It is limited to web-page document content: Firefox does not allow an extension to draw above Zen's browser chrome or the physical tab strip. Switching a tab remains visible through the native browser UI, then the selected page shows the corresponding in-page cue. Reduced-motion preferences use a brief static indication instead of animated motion.

Native AI tab group

Tabs created through open_tab are also placed into a native purple tab group named AI. This is a persistent visible marker in the Zen/Firefox tab strip, rather than an artificial page overlay. Later AI-created tabs in the same window, including after an extension reload, join that group. The behavior is feature-detected: opening tabs still works normally in builds where native tab groups are unavailable.

Technical data permission

Performance and Extension inventory require Firefox data permission technicalAndInteraction. The request must occur directly from a user click/change handler, so permission cannot be granted by a remote AI command.

After enabling Performance or Extension inventory, the browser displays a system consent dialog. The user must select Allow. Check:

node bridge/cli.mjs call get_state

Expected fields:

{
  "technicalConsent": true,
  "capabilities": {
    "performance": true,
    "extensions": true
  }
}

Browser name/version are sent to the bridge only when this consent is present.

Popup diagnostics

Popup records stages:

  • script-start;
  • first-animation-frame;
  • window-load;
  • render-complete or render-error;
  • after-250ms;
  • window-error;
  • unhandled-rejection;
  • permission-or-settings-error.

Diagnostics include viewport and HTML/body sizes, but not a screenshot of the popup.

node bridge/cli.mjs call list_diagnostics '{"limit":20}'
node bridge/cli.mjs call open_popup_debug '{}'
node bridge/cli.mjs call clear_diagnostics '{}'

open_popup_debug opens the popup as a regular extension tab, which is convenient when there are issues with the toolbar panel size.

Settings

Key Default Range/Condition Purpose
bridgeUrl ws://127.0.0.1:18765/extension Only ws:/wss: Extension WebSocket endpoint
bridgeToken empty 64 hex recommended Pairing secret
reconnect true boolean Automatic reconnect with backoff 1–30 seconds
allAccess false High risk, manual confirm Persistent Control of all existing and new tabs visible to WebExtension
monitorConsole true boolean Page console and JS errors after grant
monitorNetwork true boolean WebRequest lifecycle for granted tabs
monitorMutations false boolean Batch DOM mutations; may be noisy
monitorPerformance false Requires technical consent Long tasks, LCP, layout shifts
monitorExtensions false Requires technical consent Inventory and lifecycle of other add-ons
captureBodies false High risk Textual response bodies
captureRequestBodies false High risk Form/raw request bodies
allowEval false High risk + Control Execution of JS in isolated extension realm
exposeUntrackedTabs false Privacy-sensitive Show titles/URLs of untracked tabs in list/find
redactQueryValues true Recommended not to disable Replace query param values with [redacted]
maxEvents 1000 100…10000 Extension-side limit of each ring buffer
maxBodyBytes 262144 1024…1048576 Limit of one captured body

Changing settings reconfigures tracked frames and forcefully reconnects the WebSocket.

The allAccess=false → true transition requires synchronous user confirmation. The true → false transition does not require confirmation and immediately restores previous per-tab grants. A programmatic UI call without a confirmation marker is rejected. The saved value survives restart; Stop all forcefully saves false.

Reset defaults changes only form values; to apply, click Save.

CLI

CLI is located in bridge/cli.mjs. After npm link, the name zdb is available; without a link, use node bridge/cli.mjs.

node bridge/cli.mjs help

Main CLI Commands

npm run status
npm run token
node bridge/cli.mjs call <action> '<json-args>'
node bridge/cli.mjs events [after-id] [limit]
node bridge/cli.mjs follow
node bridge/cli.mjs screenshot [output-path] [tab-id]

JSON args must be one shell argument:

node bridge/cli.mjs call snapshot '{"tabId":123,"limit":800}'

CLI:

  • automatically reads host/port/token through loadConfig();
  • uses Bearer auth;
  • prints JSON with indentation;
  • returns a non-zero exit code on HTTP/command error;
  • follow prints each SSE event as one JSON string;
  • screenshot decodes data URL and writes PNG/JPEG to disk.

Quick end-to-end example

node bridge/cli.mjs call ping
node bridge/cli.mjs call get_state
node bridge/cli.mjs call list_tabs '{}'
node bridge/cli.mjs call snapshot '{"tabId":123}'
node bridge/cli.mjs call click '{"tabId":123,"ref":"0:e4"}'
node bridge/cli.mjs call list_console '{"tabId":123,"level":"error"}'
node bridge/cli.mjs call list_network '{"tabId":123,"limit":100}'

Full Command Catalog

All commands are called like this:

node bridge/cli.mjs call ACTION 'JSON_ARGS'

HTTP envelope of a successful response:

{
  "ok": true,
  "result": {}
}

On command error, the bridge responds with HTTP 502 or 504:

{
  "ok": false,
  "error": "Tab 123 requires control access; current mode is observe",
  "code": "TAB_ACCESS_DENIED"
}

Diagnostics — Available Even in Off Mode

ping

Args: {}.

Returns pong, timestamp, and public state. The best two-way health check: unlike /health, the command proves that the HTTP bridge, WebSocket, and background extension are responding.

capabilities

Args: {}.

Returns protocolVersion, a full list of actions, and machine-readable limitations.

get_state

Args:

{ "tabId": 123 }

tabId is optional. Returns version, pairing, consent, bridge/session state, trackedTabs, allAccess, allAccessTabs, mode of the specified tab, effective capabilities, and agentActivity. agentActivity exposes only bridge-command metadata for the popup: current action/status, active count, timestamps, outcome code, and up to six recent outcomes.

list_diagnostics

Args: { "limit": 100 }, limit 1…500.

Returns public state and the latest popup diagnostics.

clear_diagnostics

Args: {}. Clears popup diagnostics.

open_popup_debug

Args: {}. Opens popup/popup.html?debug=tab in the active extension tab and returns tabId. The command is intended only for UI extension diagnostics.

Tabs and Navigation

list_tabs — Observe

Args:

{ "includeUntracked": true }

By default, it returns only tracked tabs. With ALL ACCESS, this includes all tabs visible to the WebExtension. In normal per-tab mode, untracked metadata will appear only if both of the following conditions are met:

  • setting exposeUntrackedTabs=true;
  • includeUntracked is not equal to false.

This reveals metadata but does not grant Observe/Control access to the tab.

Each tab contains:

  • id, windowId, index;
  • active, pinned, audible, discarded, status;
  • title, sanitized url, favIconUrl;
  • mode;
  • agentCreated.

find_tab — Observe

Args:

{
  "url": "example.com",
  "active": false,
  "includeUntracked": false
}

You can pass url or query; case-insensitive search by URL and title. active=true selects the active match.

open_tab — session Control

Args:

{
  "url": "https://example.com",
  "active": true,
  "windowId": 1
}
  • default URL: about:blank;
  • active default true;
  • allowed http:, https:, file:, and about: URLs;
  • a string without a scheme gets https://;
  • other schemes are rejected;
  • the created tab automatically receives Control.

You can open about:, but content commands on secure pages are usually unavailable.

activate_tab — tab Control

{ "tabId": 123, "focusWindow": true }

Makes the tab active. focusWindow default true.

navigate — tab Control

{ "tabId": 123, "url": "example.com/path", "active": true }

Uses the same URL rules as open_tab.

reload — tab Control

{ "tabId": 123, "bypassCache": true }

go_back / go_forward — tab Control

{ "tabId": 123 }

close_tab — tab Control

{ "tabId": 123 }

After the tab is closed, the extension removes its grant, cancels body filters, and clears buffered data for that tab.

DOM and Actions

snapshot — tab Observe

{
  "tabId": 123,
  "allFrames": true,
  "frameId": 0,
  "limit": 500,
  "includeFormValues": false
}
  • tabId can be omitted only if the active tab is already tracked;
  • allFrames default true;
  • when allFrames=false is used, frameId, default 0;
  • limit default 500, clamp 20…2000 per frame;
  • includeFormValues=false hides values as [redacted:N chars];
  • password is always returned as [redacted-password];
  • an inaccessible frame returns an object with error, without crashing the entire snapshot.

click — tab Control

{
  "tabId": 123,
  "ref": "0:e7",
  "pointerSequence": true
}

Instead of ref, you can pass selector and frameId. The element is scrolled into the center and receives focus. When pointerSequence=true, synthetic pointer/mouse over/down/up events are sent before .click().

The response always marks trusted:false.

fill — tab Control

{
  "tabId": 123,
  "ref": "0:e8",
  "value": "hello"
}

Supports:

  • input/textarea through native value setter;
  • select — searches for option first by value, then by visible text;
  • contenteditable through selection + execCommand("insertText");
  • synthetic input and change events after changes.

The command replaces the target's current value entirely. There is no separate key-press/Enter command; click the submit button, or use evaluate only when the high-risk setting is explicitly enabled.

hover — tab Control

{ "tabId": 123, "selector": ".menu-item", "frameId": 0 }

Sends synthetic pointer/mouse over/enter/move events. trusted:false.

scroll — tab Control

Scroll to element:

{
  "tabId": 123,
  "ref": "0:e20",
  "block": "center",
  "inline": "nearest",
  "behavior": "smooth"
}

Relative scroll of the window:

{ "tabId": 123, "x": 0, "y": 900, "behavior": "instant" }

evaluate — tab Control + allowEval

{
  "tabId": 123,
  "frameId": 0,
  "code": "({title: document.title, links: document.links.length})"
}

Restrictions:

  • code must be non-empty and less than or equal to 100 KB;
  • executed through browser.tabs.executeScript;
  • realm — isolated-content-script;
  • DOM is visible, but page-defined JS globals are usually not visible;
  • the result must be serializable via WebExtension messaging;
  • setting is disabled by default.

screenshot — tab Observe

{
  "tabId": 123,
  "format": "png",
  "quality": 80,
  "activate": true,
  "restore": true
}
  • captures only the visible area, not full page;
  • format: png default or jpeg;
  • JPEG quality clamp 1…100;
  • Firefox can capture only the active tab window;
  • inactive tabs require activate=true;
  • the previous active tab is restored if restore is not false;
  • API returns dataUrl; CLI screenshot decodes it into a file.

Console and Page Events

list_console — Observe

{
  "tabId": 123,
  "allTabs": false,
  "level": "error",
  "text": "failed",
  "limit": 250
}
  • allTabs=true aggregates all tracked tabs;
  • level is compared exactly after lower-case conversion;
  • text is searched as a case-insensitive substring in JSON entry;
  • limit clamp 1…2000.

clear_console — Observe

{ "tabId": 123 }

To clear all tracked tabs, use { "allTabs": true }.

list_events — Observe

{
  "tabId": 123,
  "allTabs": false,
  "type": "performance.entry",
  "limit": 250
}

Returns extension-side pageEvents: DOM mutations, performance entries, and other content events, except console, which is stored separately.

Network

list_network — Observe

{
  "tabId": 123,
  "allTabs": false,
  "limit": 250,
  "status": "completed",
  "text": "/api/"
}
  • status is usually pending, completed, or error;
  • text filters sanitized URL;
  • limit clamps 1…2000.

get_network_request — Observe

{ "requestId": "12345" }

Returns the full saved record, including headers/body only if they were captured. Additionally checks that the request tab still has Observe.

clear_network — Observe

{ "tabId": 123 }

For all tracked tabs: { "allTabs": true }.

Other Extensions

list_extensions — Observe + setting + consent

{ "query": "ublock" }

Requires:

  • sessionMode >= observe;
  • monitorExtensions=true;
  • technicalConsent=true.

Returns only metadata available to browser.management: id, name, version, description, type, enabled, mayDisable, installType, homepage/options URL, API permissions, and host permissions.

get_extension — Observe + setting + consent

{ "id": "extension@example" }

Does not read storage, private console, or background context of another extension.

open_debugging — session Control

{ "active": true, "extensionId": "extension@example" }

Opens about:debugging#/runtime/this-firefox. extensionId is only returned in the response as a hint; automatic selection of Inspect is not available. Firefox prohibits normal WebExtension scripts from scripting about:debugging.

DOM Snapshot and Refs

Snapshot builds a compact tree of visible meaningful elements:

[0:e1] heading "Example Domain"
[0:e2] p "This domain is for use in documentation examples…"
[0:e3] link "Learn more" href="https://iana.org/domains/example"

Ref format:

<frameId>:<elementRef>
0:e3
5:e12

Background strips the prefix before sending the content command and directs it to the appropriate frame.

Each item may contain:

  • ref;
  • tag;
  • inferred or explicit role;
  • accessible-ish name;
  • direct text;
  • selector hint;
  • disabled/checked;
  • input type, placeholder, and redacted value;
  • link href.

Name is computed from aria-label, aria-labelledby, associated labels, alt, title, placeholder, name or direct text. This is a practical semantic snapshot but not the full Firefox accessibility tree.

Snapshot:

  • skips script/style/noscript/template/meta/link/head;
  • skips invisible, hidden, and aria-hidden=true elements;
  • limits nesting depth to 24;
  • enters open ShadowRoots;
  • cannot enter closed ShadowRoots;
  • clears the previous ref map of this frame when a new snapshot is taken;
  • returns truncated:true if the limit is reached.

CSS selector is first searched in the document, then recursively in open shadow roots. Complex selectors across multiple shadow boundaries are not supported as a single chain, but direct matches within a shadow root will be found.

Console, Network, Performance, and Events

Console Capture

After Observe/Control and monitorConsole=true content script injects page-hook.js into the page realm. Hook wraps:

  • console.debug;
  • console.log;
  • console.info;
  • console.warn;
  • console.error;
  • console.trace.

The original console method is always called. A serialized copy of arguments goes to the bridge.

Serializer:

  • depth up to 3;
  • up to 50 elements in an array/object keys;
  • supports Error, BigInt, Function, and Element summaries;
  • marks circular values;
  • truncates element text;
  • can read data only after enabling the hook — old console entries are not restored.

Also captures window.error and window.unhandledrejection.

Network Lifecycle

Network monitor listens to:

  • onBeforeRequest;
  • onBeforeSendHeaders;
  • onHeadersReceived;
  • onBeforeRedirect;
  • onCompleted;
  • onErrorOccurred.

A record may contain:

  • request/tab/frame/parent IDs;
  • sanitized URL and origin URL;
  • method/type;
  • start/completion timestamps;
  • status/statusCode/statusLine/error;
  • redacted request/response headers;
  • content type, cache flag, IP;
  • TTFB and duration;
  • redirect URL;
  • optional request/response body and truncation flags.

Response capture uses filterResponseData, records a copy up to maxBodyBytes, and passes the original bytes back to the page without modification. The body is saved only for text/JSON/JS/XML/SVG/GraphQL/form-urlencoded content types.

DOM Mutations

When monitorMutations=true MutationObserver collects a 250 ms batch:

  • number of added/removed nodes;
  • number of attribute changes;
  • number of characterData changes;
  • up to 12 sample targets.

Event type: dom.mutations.

Performance

When monitorPerformance=true and technicalConsent=true, the extension creates PerformanceObserver instances for:

  • longtask;
  • largest-contentful-paint;
  • layout-shift.

Event type: performance.entry. Entry contains type, name, startTime, duration and available value, hadRecentInput, size.

Unsupported entry types are silently skipped by the browser.

Extension Lifecycle

When access is at least Observe, monitorExtensions=true, and technical consent is present, the bridge sends:

  • extension.installed;
  • extension.uninstalled;
  • extension.enabled;
  • extension.disabled.

These are lifecycle/metadata events, not console output from another add-on.

Full Event Catalog

Bridge/server can see:

  • bridge.client_connected, bridge.client_disconnected;
  • bridge.extension_ready;
  • access.granted, access.revoked, access.stopped;
  • access.all_access_enabled, access.all_access_tab_added, access.all_access_disabled;
  • tab.created, tab.removed, tab.updated, tab.activated;
  • navigation.committed;
  • console.debug/log/info/warn/error/trace;
  • window.error, window.unhandledrejection;
  • dom.mutations;
  • performance.entry;
  • network.request, network.response_started, network.redirect, network.completed, network.error, network.body;
  • extension.installed/uninstalled/enabled/disabled;
  • popup.diagnostic.

HTTP API

Default base URL:

http://127.0.0.1:18765

Authentication

All /api/* endpoints require:

Authorization: Bearer <token>

Server also understands the token query parameter, but Bearer header is safer: URLs are more likely to end up in logs/history.

Example without printing token in terminal output:

TOKEN="$(node bridge/cli.mjs token)"
curl -s \
  -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:18765/api/status | jq
unset TOKEN

GET /health

Does not require auth and reveals only:

{
  "ok": true,
  "version": "0.1.7",
  "extensionConnected": true
}

This is not proof of a command round-trip; use ping for that.

GET /api/status

Returns the server version, bind address/port, uptime, event-buffer size, and connected clients.

Client includes:

  • stable extension instanceId;
  • timestamps connection/last message;
  • extension ID/version;
  • browser name/version only with technical consent.

POST /api/command

{
  "action": "snapshot",
  "args": { "tabId": 123 },
  "clientId": "optional-specific-browser-instance",
  "timeoutMs": 30000
}

Rules:

  • action is required, string length up to 128;
  • args must be a plain JSON object;
  • clientId is optional;
  • without clientId, the last active client with hello is selected;
  • timeoutMs clamp 250…120000;
  • default timeout is set by ZDB_COMMAND_TIMEOUT_MS;
  • maximum HTTP JSON body size is 1 MiB.

GET /api/events

Query parameters:

  • after — return ID strictly greater than the specified;
  • limit — maximum 2000;
  • type — exact event name filter.
curl -s \
  -H "Authorization: Bearer $TOKEN" \
  'http://127.0.0.1:18765/api/events?after=0&limit=100&type=console.error'

DELETE /api/events

Clears only server-side event buffer. Does not clear extension console/network/page buffers.

GET /api/events/stream

Opens Server-Sent Events stream. The server sends a heartbeat comment every 15 seconds.

Extension WebSocket Protocol

Endpoint:

ws://127.0.0.1:18765/extension?token=<token>

Invalid path/token receives HTTP 401. Maximum WebSocket payload — 16 MiB.

Extension → bridge: hello

{
  "type": "hello",
  "instanceId": "zen-…",
  "extensionId": "zen-debug-bridge@local.dev",
  "extensionVersion": "0.1.7",
  "browserName": "Firefox",
  "browserVersion": "…"
}

Browser fields are equal to null without technical consent.

Bridge → extension: hello acknowledgment

{
  "type": "hello_ack",
  "serverVersion": "0.1.7",
  "clientId": "zen-…"
}

Bridge → extension: command

{
  "type": "command",
  "id": "cmd-…",
  "action": "ping",
  "args": {}
}

Extension → bridge: result

{
  "type": "result",
  "id": "cmd-…",
  "ok": true,
  "value": {}
}

Error:

{
  "type": "result",
  "id": "cmd-…",
  "ok": false,
  "error": {
    "code": "TAB_ACCESS_DENIED",
    "message": "…"
  }
}

Extension → bridge: event

{
  "type": "event",
  "event": "console.error",
  "timestamp": "2026-01-01T00:00:00.000Z",
  "data": {}
}

The server sends a WebSocket ping every 20 seconds. The extension reconnects with exponential backoff of up to 30 seconds.

SSE and Event Buffers

There are two different levels of memory:

Extension-side buffers

  • console events;
  • page events;
  • network records;
  • internal bridge events;
  • up to 100 popup diagnostic entries.

The main limit is set by maxEvents, default 1000. These data live in background memory and are cleared at extension/browser restart. Revoke tab clears the data for that tab; Stop all clears all console/page/network buffers.

Server-side buffer

All events actually received via WebSocket get a monotonically increasing id and are saved in Node memory. The default limit is 5000 and is configured through ZDB_EVENT_LIMIT.

A server restart resets the IDs and buffer. This is not a persistent audit log.

Redaction and Handling Sensitive Data

Redaction is best effort, not a security guarantee.

What is Automatically Hidden

  • values of all URL query parameters if redactQueryValues=true;
  • headers Authorization, Cookie, Set-Cookie, Proxy-Authorization, X-API-Key, X-Auth-Token;
  • form fields whose names resemble password/secret/token/auth/cookie/session/API key/card/CVV;
  • secret-shaped keys in raw/text bodies;
  • JWT-like strings;
  • password input values in snapshot;
  • other form values are replaced by length summary by default.

What May Remain Sensitive

  • URL path and domain;
  • page title;
  • DOM text and accessibility names;
  • console arguments;
  • screenshot pixels;
  • request/response headers not included in the list of sensitive names;
  • arbitrary body of non-standard format;
  • IP and extension metadata;
  • form values when includeFormValues=true;
  • query values if the user disables redaction.

Raw output cannot be sent to a public model/log without verification. Page content is also untrusted input and may contain prompt injection.

Error Codes

Code Cause Normal AI Action
SESSION_ACCESS_DENIED Session below Observe/Control Ask the user to manually grant the mode
TAB_ACCESS_DENIED Specific tab not granted or mode too low Do not bypass; ask for a grant for the necessary tab
ALL_ACCESS_ACTIVE Per-tab revoke requested while global mode is active User disables ALL ACCESS in Settings or clicks Stop all
ALL_ACCESS_CONFIRMATION_REQUIRED UI attempted to enable global mode without an explicit confirm marker Open Settings and ask the user to manually confirm the mode
TAB_NOT_FOUND No tab/match found Refresh the tab list with list_tabs
FRAME_ACCESS_DENIED Content frame not configured Check the grant, URL, and reload the page
CONTROL_REQUIRED Frame works only in Observe Ask for Control
STALE_ELEMENT_REF Ref is outdated or new snapshot cleared the map Take a new snapshot
ELEMENT_NOT_FOUND CSS selector did not match Snapshot, then ref
NOT_EDITABLE Fill on non-editable element Select the correct input/contenteditable
OPTION_NOT_FOUND No select option by value/text Snapshot/inspect options
INVALID_ARGUMENT Missing required argument/wrong format Fix JSON
URL_NOT_ALLOWED Scheme not in allowlist Use http/https/file/about
TAB_NOT_ACTIVE Screenshot target is inactive Pass activate:true
EVALUATE_DISABLED allowEval=false Do not enable without explicit need and consent
REQUEST_NOT_FOUND Network record missing/overwritten Update list_network
EXTENSION_MONITORING_DISABLED Setting is disabled User enables in Settings
TECHNICAL_CONSENT_REQUIRED No system consent User grants permission through UI
UNKNOWN_COMMAND Action not supported Call capabilities
TIMEOUT Extension did not respond in time Check connection, tab state, and repeat read-only check
DISCONNECTED WebSocket closed during command Wait for reconnect, then ping

HTTP-level errors:

  • 401 unauthorized — invalid/missing token;
  • 400 — malformed URL/JSON/body;
  • 413 — HTTP body larger than 1 MiB;
  • 503 extension_not_connected — no WebSocket extension client;
  • 503 client_not_found — unknown clientId specified;
  • 502 — extension command error;
  • 504 — command timeout.

Deep Debug

A regular WebExtension intentionally cannot:

  • read private background console of another add-on;
  • read storage of another add-on;
  • execute code in a foreign extension realm;
  • script browser chrome and about: pages;
  • click DevTools Inspect;
  • receive trusted input.

For privileged diagnostics, launch a separate Zen Remote Agent/WebDriver BiDi instance with a separate profile:

scripts/start-zen-deep-debug.sh

Defaults:

  • profile: $HOME/.zen/zdb-debug-profile inside Flatpak namespace;
  • remote port: 9222;
  • separate browser instance through --no-remote;
  • system/chrome access is disabled.

Variables:

ZDB_ZEN_PROFILE_PATH="$HOME/.zen/another-profile" \
ZDB_REMOTE_PORT=9223 \
scripts/start-zen-deep-debug.sh

Privileged Gecko/system access is enabled only explicitly:

ZDB_SYSTEM_ACCESS=1 scripts/start-zen-deep-debug.sh

scripts/zen-flatpak-wrapper.sh additionally separates host /tmp from Flatpak and is intended for Selenium, which creates temporary profiles.

Deep Debug is a separate layer. It is not used automatically by Zen Debug Bridge CLI commands. For it, an external compatible WebDriver BiDi/Firefox DevTools client is needed, such as Mozilla Firefox DevTools MCP. The Remote Agent does not have built-in auth: keep the port on loopback and use a separate profile without valuable sessions.

Before using, read SECURITY.md.

Limitations

  • Synthetic click/hover/input have event.isTrusted === false.
  • Websites with trusted input checks, CAPTCHA, or anti-bot measures may ignore actions.
  • There is no native keyboard primitive and pressing Enter is not supported.
  • There is no file upload primitive.
  • PDF export is not available.
  • Screenshot captures only the visible viewport.
  • HAR export is not available, although network records can be obtained via JSON commands.
  • Response interception/alteration or request blocking is not possible; webRequestBlocking is not used for mutating traffic.
  • There are no cookie/localStorage/sessionStorage API commands.
  • Download management is not supported.
  • Reading Firefox DevTools console history before a grant is not possible.
  • Reading closed Shadow DOM roots is not supported.
  • Restricted frames may return per-frame errors.
  • evaluate does not work in the page main world.
  • list_extensions sees metadata but does not access the internal execution of the add-on.
  • Protected Mozilla pages and browser UI are not accessible to a regular content script.
  • ALL ACCESS does not change these sandbox/privileged-page restrictions and does not expose private console/storage of other add-ons.
  • Scope ALL ACCESS includes private tabs only if Firefox separately allows this add-on to work in Private Windows.
  • Redaction does not guarantee the removal of all secrets.
  • Event buffers are non-persistent.
  • Multiple HTTP AI clients do not have separate permission scopes: they all use one token and current user grants.

Troubleshooting

Small white square instead of popup

Most likely, Zen Flatpak received only manifest.json through the portal but not sibling files.

Solution:

  1. Create a single archive via npm run build.
  2. Copy/rename it to .xpi inside ~/Downloads.
  3. Remove the old temporary add-on.
  4. Load XPI through about:debugging.

Popup works, bridge is not connected

npm run status
curl -s http://127.0.0.1:18765/health | jq

Check:

  • The bridge process is running;
  • The URL is exactly ws://127.0.0.1:18765/extension;
  • The token matches npm run token;
  • XPI contains the current version;
  • After changing the archive, Reload was pressed;
  • Reconnect is enabled.

response is undefined / error response.state

Fixed starting with 0.1.3. Firefox requires Promise/sendResponse from runtime.onMessage. Install/Reload version 0.1.4 or newer.

Technical data permission was not granted

Fixed in 0.1.4: permissions.request() is now called as the first asynchronous operation in the user-gesture handler.

  1. Reload extension.
  2. Manually enable Performance or Extension inventory.
  3. Select Allow in the system dialog.
  4. Check technicalConsent through get_state.

AI cannot confirm this consent instead of the user.

Connected, but commands are rejected

connected means only transport/pairing. Check:

node bridge/cli.mjs call get_state

If sessionMode=off, the user must grant Observe/Control. If the target tab is missing in list_tabs, the grant was given to another tab.

ALL ACCESS is enabled, but about: or UI Zen cannot be read

This is an expected Firefox limitation, not an incomplete global grant. Check get_state: the combination of allAccess=true, sessionMode=control, and the tab's presence in list_tabs means that the Zen Debug Bridge permission model has granted access. However, the content script still cannot be injected into browser chrome, most about:/Mozilla-protected documents, or pages belonging to other extensions.

Browser-level commands like activate/navigate/close can be used where Firefox allows them. For DOM browser UI, a separate isolated Deep Debug/WebDriver BiDi script is needed; ALL ACCESS does not automatically include it.

After Stop all ALL ACCESS appears again

Starting from 0.1.5, Stop all saves allAccess=false. If the mode returns:

  1. Ensure that version 0.1.5 or newer is displayed in popup/manifest;
  2. Reload the actual private XPI;
  3. Open Settings and check that the checkbox is unchecked;
  4. Do not save the old already open Settings page without reloading it;
  5. Check node bridge/cli.mjs call get_state.

open_tab first returned about:blank

This is a race in the response from browser.tabs.create(). Wait for navigation to complete and check:

node bridge/cli.mjs call list_tabs '{}'
node bridge/cli.mjs call snapshot '{"tabId":<new-id>}'

Snapshot is empty or frame error

  • The page may be privileged/restricted;
  • The grant may have been issued while the content script was being reloaded;
  • The tab could have navigated to a different URL;
  • Frame might not allow injection;
  • Visible meaningful elements might still not have rendered.

Try reloading an ordinary HTTP(S) page, a new snapshot, and allFrames=false for the top frame.

Click executed, but the site did not respond

Response clicked:true means DOM .click() call, not trusted OS click. Get a new snapshot, check console/network. For strict isTrusted, use a separate Deep Debug/WebDriver BiDi script.

Console/network is empty

  • Capture starts only after grant;
  • The corresponding toggle must be enabled;
  • Requests/logs that occurred earlier are not restored;
  • Update the page after enabling;
  • Check the correct tabId or allTabs=true.

EADDRINUSE at startup

Bridge is already running or port is occupied:

npm run status
ss -ltnp '( sport = :18765 )'

Do not start a second server on the same port. For a separate instance, set another ZDB_PORT and corresponding bridgeUrl.

unauthorized

CLI and server read token from one ZDB_DATA_DIR. Extension might have saved an old token. Update Settings or private pairing and Reload. Do not print token in diagnostic log.

Temporary extension disappeared

This is expected after browser restart. Re-load XPI through about:debugging. For permanent installation, a signature is needed.

Bridge environment variables

Variable Default Purpose
ZDB_HOST 127.0.0.1 Bind address; do not use 0.0.0.0 without separate protection
ZDB_PORT 18765 HTTP/WebSocket port, 0…65535 is allowed; 0 is convenient for tests
ZDB_DATA_DIR $XDG_DATA_HOME/zen-debug-bridge or ~/.local/share/zen-debug-bridge Token/runtime data directory
ZDB_EVENT_LIMIT 5000 Server-side event ring size
ZDB_COMMAND_TIMEOUT_MS 30000 Default command timeout

The extension URL must match the actual host, port, and path.

Development, tests, and build

Syntax checks

npm run check

Server, CLI, and all extension JS files are checked through node --check.

Tests

npm test

The suite covers:

  • session-mode downgrade;
  • URL/header redaction;
  • Firefox runtime message Promise responses;
  • order of technical consent request relative to user gesture;
  • HTTP auth;
  • WebSocket command relay;
  • event buffering;
  • rejection of invalid WebSocket token.

Server tests open a random loopback port.

Extension lint

npx --yes web-ext@latest lint \
  --source-dir extension \
  --warnings-as-errors

Standard build

npm run build

Output:

artifacts/zen-debug-bridge-<version>.zip
artifacts/zen-debug-bridge-<version>.xpi

Build script:

  • reads version from manifest;
  • creates temporary archive;
  • excludes source maps, editor backups, .DS_Store;
  • always excludes pairing.local.json;
  • atomically moves the archive to artifacts/ and writes an equivalent .xpi copy for Zen/Firefox.

Local secret absence check:

unzip -Z1 artifacts/zen-debug-bridge-*.zip | \
  grep -Fx 'pairing.local.json' && echo 'ERROR: secret included'

The command should not find the file.

Version Update Checklist

When bumping the version, synchronize:

  • package.json;
  • package-lock.json;
  • extension/manifest.json;
  • SERVER_VERSION in bridge/server.mjs;
  • version-specific examples in the documentation as needed.

After changes:

npm run check
npm test
npm run build

Project Structure

zen-debug-bridge/
├── bridge/
│   ├── cli.mjs                 # CLI HTTP/SSE client
│   ├── config.mjs              # token, host, port, limits
│   └── server.mjs              # HTTP + SSE + WebSocket router
├── extension/
│   ├── background/
│   │   ├── core.js             # state, pairing, grants, WS, redaction
│   │   ├── network.js          # webRequest lifecycle/body capture
│   │   ├── commands.js         # public command implementation
│   │   └── main.js             # runtime/browser event wiring
│   ├── icons/
│   ├── options/                # full settings UI
│   ├── popup/                  # toolbar UI and diagnostics
│   ├── content.js              # frame snapshots/actions/observers
│   ├── page-hook.js            # page-realm console/error capture
│   ├── manifest.json
│   └── pairing.local.json      # optional ignored local secret
├── scripts/
│   ├── build-extension.sh
│   ├── start-bridge.sh
│   ├── start-zen-deep-debug.sh
│   └── zen-flatpak-wrapper.sh
├── systemd/
│   └── zen-debug-bridge.service
├── test/
├── README.md
├── SECURITY.md
└── package.json

Final Reminder

Zen Debug Bridge distinguishes five different concepts:

  1. Server running — Node listens on port.
  2. Extension connected — token and WebSocket are working.
  3. Tab granted — user has allowed a specific browser tab.
  4. ALL ACCESS enabled — user has temporarily replaced the per-tab boundary with global persistent Control.
  5. Capability enabled — corresponding toggle/consent is enabled.

For a command to succeed, every relevant level must be satisfied. connected never implies automatic access to the entire browser; such access exists only when allAccess=true is explicitly visible.

In case of doubt, use the safe sequence:

npm run status
node bridge/cli.mjs call ping
node bridge/cli.mjs call get_state
node bridge/cli.mjs call capabilities
node bridge/cli.mjs call list_tabs '{}'

And only then perform actions within the user-granted mode.

☕ Support the author

If this project is useful to you, you can support the author — it really helps to keep working on it:

https://buymeacoffee.com/chamomilewoods

Buy Me a Coffee

About

Local-first observability and control bridge for Zen Browser

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages