Skip to content

docs(clawgate): the picker privacy choke point, and why the obvious test can't fail - #283

Merged
ZacxDev merged 1 commit into
mainfrom
docs/clawgate-picker-leak-lessons
Aug 2, 2026
Merged

docs(clawgate): the picker privacy choke point, and why the obvious test can't fail#283
ZacxDev merged 1 commit into
mainfrom
docs/clawgate-picker-leak-lessons

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Captures two lessons from the clawgate extension 1.4.0 element-ref enrichment leak (homelab-infra fcaca875 / PR #276), and corrects a developer note that had been wrong since it was written.

reference/element-references.md

Three factual corrections. The note claimed the enrichment walks previousElementSibling / nextElementSibling and caps "each at 120 chars". Both false: it walks previousSibling / nextSibling — the node variants, so text nodes count, which is a distinct leak surface — and slices at 40 chars, with refText capping at MAX_REF_TEXT_CHARS (80). Verified against the merged code, not restated from intent.

The choke point, recorded. Every page-text read in the walk must go through adjacentTextpageText. Reading .textContent directly is what shipped the leak, and adding a second "is this a text field" predicate is the root cause that has now recurred seven times — six audit rounds on the picker, then again in the enrichment.

New section: the obvious test cannot fail. A native <input> or <textarea> never exposes its current value through textContent — typing updates .value, while .textContent keeps whatever the server rendered. So a canary typed into a form field and picked from a neighbouring element passes under the leaking build too. Three consecutive live captures were burned this way (tasks #147#149), each initially read as a pass. Only contenteditable / role="textbox" surfaces and a <textarea>'s server-rendered initial content actually carry user text.

Documents the fixture that settled it (task #150): a button flanked by two contenteditable divs holding known canaries, plus a second button beside a plain <span> as a positive control — the guard must withhold the canaries and still capture the span, or it's a blanket refusal rather than a fix. Also notes that chrome.commands is unreachable from Playwright, so a hand capture is the only way to prove which build Brave actually loaded.

SKILL.md

The skill had no mention of extension delivery at all, which is part of how this went wrong:

  • The extension does not ship via Flux — merging to trunk deploys nothing. Brave loads a flat copy at ~/clawgate-extension on the workbench, delivered by scripts/sync-clawgate-extension.sh, and does not hot-reload unpacked extensions.
  • A missing .synced-from stamp means a hand-copy. That is exactly how a build which never passed CI ran live for ~11 hours on 2026-08-01 while trunk looked clean — copied straight from a working tree, so clawgate-ci never saw it.
  • Brave routinely leaves newly-added hotkeys unbound on an in-place reload — check brave://extensions/shortcuts.
  • The workbench firewall allowedTCPPorts is a short allowlist, so serve scratch test pages on localhost rather than opening a port for a throwaway.

Scope

Docs only — two files, no code. Unaffected by the test_skill_size.py byte gate, which resolves to scripts/browser-bridge/SKILL.md.

⚠ These files resolve into /nix/store (read-only copies), so they need a home-manager switch to go live — editing the devrc source alone does not update ~/.claude/skills/clawgate/.

…est can't fail

Two lessons from the 1.4.0 element-ref enrichment leak (homelab-infra fcaca875 / PR #276),
plus three corrections to a developer note that had been wrong since it was written.

reference/element-references.md
- Correct the note: the walk uses previousSibling/nextSibling (NODE variants — text nodes
  count), not the Element variants, and slices at 40 chars, not 120 (refText then caps at
  MAX_REF_TEXT_CHARS=80). Both claims were false as written.
- Record the choke point: every page-text read must go through adjacentText → pageText.
  Reading .textContent directly is what shipped the leak. Never add a second "is this a
  text field" predicate — that root cause has now recurred seven times.
- 🔴 New section: most obvious tests of this guard CANNOT fail. A native <input>/<textarea>
  never exposes its current value via textContent (typing updates .value), so a canary typed
  into a form field passes under the leaking build too. Three consecutive live captures were
  wasted this way (tasks #147-#149), each read as a pass. Only contenteditable / role=textbox
  and a textarea's SERVER-RENDERED initial content carry user text. Documents the fixture that
  actually settled it (#150), including the positive control that catches a blanket refusal.

SKILL.md
- The extension does NOT ship via Flux: merging to trunk deploys nothing. Brave loads a flat
  copy at ~/clawgate-extension ON THE WORKBENCH, delivered by sync-clawgate-extension.sh, and
  does not hot-reload unpacked extensions.
- A missing .synced-from stamp means a hand-copy: that is how a build which never passed CI
  ran live for ~11 hours while trunk looked clean.
- Workbench firewall allowedTCPPorts is a short allowlist — serve scratch test pages on
  localhost, don't open a port for a throwaway.
@ZacxDev
ZacxDev merged commit 2a7a069 into main Aug 2, 2026
@ZacxDev
ZacxDev deleted the docs/clawgate-picker-leak-lessons branch August 2, 2026 20:13
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