Skip to content

feat(macos): capture URL from Firefox-based browsers via accessibility tree - #134

Open
rrogerc wants to merge 2 commits into
ActivityWatch:masterfrom
rrogerc:firefox-url-capture-upstream
Open

feat(macos): capture URL from Firefox-based browsers via accessibility tree#134
rrogerc wants to merge 2 commits into
ActivityWatch:masterfrom
rrogerc:firefox-url-capture-upstream

Conversation

@rrogerc

@rrogerc rrogerc commented Jul 24, 2026

Copy link
Copy Markdown

What

Captures the current page URL from Gecko-based browsers (Firefox, Firefox Developer Edition/Nightly, Zen, LibreWolf, Waterfox, Floorp) on macOS, filling the gap where Chromium browsers and Safari already report URLs via their scripting interfaces. Gecko has no scripting interface for tabs, but exposes the loaded page's URL on the AXURL attribute of the window's AXWebArea accessibility node — the same mechanism other macOS time trackers use.

How

On each window event for a known Gecko browser, a breadth-first search of the focused window's accessibility tree finds the first AXWebArea node and returns its AXURL. The top-level document's web area sits shallow in Gecko's tree and is reached before any iframe web areas, so the first hit is the current page. The traversal is bounded (384 elements) so a pathological tree can't stall the watcher; in testing the web area is found within the first ~70 elements.

No new permission is required — the watcher already holds Accessibility for window titles. Runs in-process via the HIServices C API (no AppleScript).

Relation to #109

Same goal, different mechanism. #109 reads the URL bar's displayed text through a System Events AppleScript path (combo box 1 of group 1 of toolbar "Navigation" of ...), which depends on the toolbar layout (breaks in browsers with customized UIs such as Zen), returns the displayed/typed text rather than the loaded URL (Firefox hides the scheme by default), and requires a separate Automation permission grant for System Events. Reading AXURL off the web area avoids all three: it is the canonical loaded-page URL, layout-independent, and covered by the existing Accessibility permission.

Testing

  • Compiles warning-free against the arm64-apple-macosx12.0 target.
  • Running in production for two days on macOS 26 (arm64) with Zen: ~82% of browser window events carry a URL (the rest are new-tab pages and transient windows); spot-checked URLs are byte-exact matches of the loaded page.
  • Verified against live windows (including non-frontmost ones): the web area is found after examining 44–67 elements.

Limitations

  • Private windows are not blanked like the Chrome incognito branch: Gecko does not expose private-browsing state in the accessibility tree. Their window titles carry a "Private Browsing" suffix that title rules and --exclude-titles can match — and as of the second commit, a matching exclusion rule clears the URL as well as the title (previously the Chromium/Safari branches retained their URLs on excluded windows too). (Same situation as the existing Safari branch, which cannot detect private windows either.)
  • If the browser's accessibility engine is disabled (e.g. accessibility.force_disabled=1), no URL is recorded and behavior falls back to title-only, as today.

…y tree

Chromium browsers and Safari already report the active tab URL through
their scripting interfaces, but Gecko-based browsers (Firefox, Zen,
LibreWolf, ...) have no such interface. Read the URL from the AXURL
attribute of the window's AXWebArea accessibility node instead, the
same mechanism used by other macOS time trackers. This needs no new
permission: the watcher already holds Accessibility for window titles.

The tree walk is breadth-first and bounded (384 elements); in practice
the web area is reached within the first ~70 elements.

Private windows are not blanked like the Chrome incognito branch, since
Gecko does not expose private-browsing state via accessibility; their
window titles carry a Private Browsing suffix that title rules and
--exclude-titles can match.
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

Adds URL capture for recognized Gecko-based browsers on macOS.

  • Defines a list of supported Firefox-family application names.
  • Traverses the focused window’s accessibility tree breadth-first to find an AXWebArea and read its AXURL.
  • Caps processing at 384 accessibility elements and adds the extracted URL to window-event data.

Confidence Score: 3/5

This PR should not merge until excluded Gecko windows also suppress their captured URLs.

The new branch writes private Gecko URLs into heartbeat data before exclusion processing, while exclusion replaces only the title, allowing sensitive URLs to be persisted despite a matching privacy rule.

aw_watcher_window/macos.swift

Security Review

Gecko private-window URLs can still be persisted when title-exclusion rules match because those rules redact only the title, not the newly populated URL.

Important Files Changed

Filename Overview
aw_watcher_window/macos.swift Adds bounded accessibility-tree URL extraction for Gecko browsers, but matching title-exclusion rules do not redact the captured URL.

Sequence Diagram

sequenceDiagram
    participant G as Gecko Browser
    participant W as Window Watcher
    participant A as ActivityWatch Server
    G->>W: AX window event
    W->>G: Traverse AX tree
    G-->>W: AXWebArea AXURL
    W->>W: Apply title exclusion only
    W->>A: POST heartbeat with title and URL
Loading

Reviews (1): Last reviewed commit: "feat(macos): capture URL from Firefox-ba..." | Re-trigger Greptile

// note: private windows are not hidden here (unlike the Chrome incognito
// branch) — Gecko does not mark them in the accessibility tree, and their
// window titles carry a "Private Browsing" suffix for rules to match
data.url = geckoURL(window: axElement)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Excluded windows retain private URLs

When a Firefox private window matches --exclude-title or an --exclude-titles rule, this assignment retains its full URL while the later exclusion logic replaces only data.title, causing the private-session URL to be persisted in the ActivityWatch heartbeat.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in e99e221: when title exclusion matches, the URL is now cleared as well. This also closes the same pre-existing gap in the Chromium and Safari branches, whose URLs were likewise retained when an exclusion rule matched the title.

Title exclusion replaced only data.title, so a browser window matching
--exclude-title or an --exclude-titles pattern still reported its URL,
which identifies the page at least as precisely as the title. Clear the
URL as well; this also closes the same pre-existing gap for the
Chromium and Safari branches.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant