browser-confirm
Confirm an Opus-flagged true-positive chain in a real browser (live via Caido proxy or mock_backend). Run when opus-deep-audit marked verdict=true_positive or chain queued by cc-taint-route. Writes fi
SKILL.md.claude/skills/browser-confirm/SKILL.md
GitHubbrowser-confirm
Purpose
Static reasoning + headless mock are necessary but not sufficient. A finding ships only after we see it fire in a real browser. This skill runs that final proof.
Inputs
targets/<name>/opus/<chain_id>.mdwithverdict: true_positive.targets/<name>/opus/<chain_id>.jsonwithtriage: runtime(cc-taint-adversarial flow).targets/<name>/findings/_queue_browser_confirm.jsonl— high-confidence runtime chains routed here bycc-taint-route. Each line:{chain_id, triage, confidence, chain, opus_record, queued_at}. Process top-to-bottom; treat as a worklog.targets/<name>/findings/_queue_mock_run.jsonl— medium/low-confidence runtime chains. Same schema as above. Try headlessmcp__tlx__mock_runfirst (cheaper, no real browser). Ifmock_runconfirms the sink fires, promote to live-mode follow-up; if it doesn't, demote toverdict: undeterminedand queue forautoresearch-loop.memory.mdfor default mode preference.
Queue processing order
- Drain
findings/_queue_browser_confirm.jsonlin live mode (or mock if memory.md / target sensitivity says so). - Drain
findings/_queue_mock_run.jsonlviamock_run→ selectively promote to live/mock browser. - Pick up any remaining
opus/<id>.md verdict: true_positivechains not already in either queue (legacy advisor pipeline).
After confirming each chain, append its chain_id to the appropriate
status.json.phases.browser_confirm.confirmed_ids / failed_ids and
leave the queue file in place — a future re-run that sees a
findings/<chain_id>/confirmed.json should skip it.
Modes
Live mode (default)
- Make sure chrome-devtools MCP is running with Caido proxy
(
caido-captureshould already have set that up). navigate_page(url=<entry_url from chain>). 2a. DOMLogger++ injection (T2.1). Immediately after navigation, inject the source/sink hook payload:
Hooks are idempotent (the payload guards viaevaluate_script(script=<contents of bin/domlogger_payload.js>)window.__TLX_DOMLOGGER_INSTALLED). All source reads + sink calls that fire during the rest of the run get appended towindow.__TLX_DOMLOGGER_EVENTS. 2b. Inline-handler enum (T2.4). Once:
Returns a JSON object — append toevaluate_script(script=<contents of bin/eventlistener_enum.js>)findings/<chain_id>/event_handlers.json. Cross-reference with the static-chain handler list to spot static-only or runtime-only handlers.
2c. DOM verification contract (2026-05). When the PoC HTML is
being authored (by report-finding or by hand), it MUST emit
data-verify-* attributes on the trigger element and set
data-verify-result="<canary>" from inside the sink-triggering
code. After the PoC fires, read the contract with:
evaluate_script(script=<contents of bin/poc_verify_contract.js>)
Returns JSON {fired, trigger, source, sink, result_canary, evidence, entries[]}. Write to
findings/<chain_id>/verify_contract.json. The structured
fired boolean is the load-bearing verification signal — prefer
it over screenshot+LLM-judge whenever the PoC emits the contract.
See bin/poc_verify_contract.js
for the attribute schema. Why this exists: a deterministic DOM
contract replaces LLM-judged screenshots, which historically
over-confirmed both visual coincidences and outright failures.
3. Inject the PoC payload via the channel the chain identifies:
- URL parameter / fragment
- form input + submit
postMessagefrom a controlled origin (useevaluate_scriptto do it from the same tab as origin-spoof)
- Watch sink:
list_console_messagesfor our canary stringlist_network_requestsfor a callback to our beacon hostevaluate_scriptto read the DOM for our injected nodeevaluate_script(script="JSON.stringify(window.__TLX_DOMLOGGER_EVENTS||[])")and write tofindings/<chain_id>/domlogger.jsonlso passive source/sink events are captured even when the explicit canary check misses.
take_screenshotof the moment of triggering.
Mock mode (fallback)
Uses the SAME chrome-devtools MCP as live mode — just points at the mock_backend localhost URL instead of the target. Benefit: installed Chrome extensions (DOMLogger++, Caido browser, etc.) still load, so the same source/sink instrumentation runs against the mock surface.
mock_start(session_id=<chain_id>, target_folder="targets/<name>/sources/")→ returnshttp://127.0.0.1:<port>/.- chrome-devtools
new_page(url=<mock_url>)(ornavigate_pageif a tab already exists). DOMLogger++ extension fires automatically. - Inject DOMLogger payload + handler enum scripts exactly as in live mode (steps 2a / 2b above) — covers any DOM that the extension misses.
- Trigger the chain analogously (URL param, form submit, postMessage).
mock_confirm(session_id, chain_id)— TLX hook observes server-side sink calls and persists.- Inject
bin/poc_verify_contract.jsviaevaluate_script— same DOM contract reader as live mode (step 2c). Write result tofindings/<chain_id>/verify_contract.json. In mock mode the contract is the primary verification signal since there's no network beacon to corroborate. take_screenshotof the trigger moment.mock_stop(session_id).
Outputs
targets/<name>/findings/<chain_id>/confirmed.jsontargets/<name>/findings/<chain_id>/screenshots/*.pngtargets/<name>/findings/<chain_id>/network.har(live mode only)targets/<name>/findings/<chain_id>/domlogger.jsonl(live mode, T2.1)targets/<name>/findings/<chain_id>/event_handlers.json(live mode, T2.4)targets/<name>/findings/<chain_id>/verify_contract.json(DOM verification contract, 2026-05). Iffired==trueANDresult_canarymatches the PoC's canary, the confirmation is considered deterministic —confirmed.json.dom_contract_match: true.
Failure modes
- Sink doesn't fire → demote chain to
verdict: undetermined, queue forautoresearch-loop. - Live target has CSP that blocks our payload → record the CSP in
findings/<id>/csp_notes.md; markbypass_needed: true. Don't attempt CSP bypass without user OK. - Target blocks our IP → switch to mock mode; log.
Result block
"phases": { "browser_confirm": { "status": "done", "ts": "<iso>",
"confirmed_ids": [...], "failed_ids": [...], "mode_used": "live|mock" } }