Playwright MCP server undetected by anti-bots and captchas: AI agent browses the web on anti-detect stealth Firefox, Python, undetected browser automation, scraping, computer use.
This report summarizes scanner evidence for feder-cr/invisible_playwright_mcp, a Python project with 2,746 GitHub stars. SynthScan examined 64,383 lines of code across 326 source files, recording 1450 pattern matches distributed across 13 syntactic categories. The legacy adjusted pattern signal is 37.9 (High pattern signal); it is not an authorship estimate.
Use the findings to ask whether maintainers can explain intent, verify dependencies, simplify unnecessary scaffolding, and focus review effort. Risk Payload v1 adds ownership, supply-chain, over-engineering, and review-effort vectors when available. These are prompts for human verification, not probabilities, security findings, or measured maintenance costs.
Use each vector to guide manual verification of the underlying findings.
Can maintainers explain intent, assumptions, and trade-offs? 1052 finding(s).
Are imports declared and package/API names verified? 299 finding(s).
Are wrappers, guards, and repeated structures necessary? 1004 finding(s).
Which changes need focused human review? 1450 finding(s).
Run additional scans to build a history of the legacy pattern signal. Score changes are not a measure of AI adoption or code quality.
Groups findings by configured severity weight. Severity is not a confidence estimate, security rating, or proof that a package is missing. Verify imports against the project's manifests and inspect each finding in its local design context.
This horizontal bar chart shows where configured pattern scores accumulate across directories. Treat the bars as locations to inspect, not as evidence of authorship or code quality. Use the file-level findings to decide whether a directory needs a focused ownership, dependency, complexity, or review check.
The scanner identified 1450 distinct pattern matches across 13 syntactic categories. Each entry below represents a discrete location in the source code where the engine recorded a detector match to inspect in context. Expand any category row to inspect the individual file paths, line numbers, code snippets, and the lexical context (CODE, COMMENT, or STRING) in which each match was detected.
Reading the findings table: The Severity column indicates the configured finding weight (CRITICAL / HIGH / MEDIUM / LOW). The Context column identifies whether the match occurred inside executable code, an inline comment, or a string literal — legacy comment-context matches receive a ×1.5 score weight. The ⚡ bolt icon marks clustered matches: three or more patterns within a 10-line window, each receiving an additional ×1.5 density multiplier under the legacy scoring model.
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| HIGH | tests/test_the_browser_workspace.py | 286 | assert "const watched = () => stage.pinned || stage.focus;" in code, ( | CODE |
| HIGH⚡ | tests/test_a_screen_says_what_it_is_showing.py | 38 | this.attrs = {}; this.hidden = false; this.title = ''; this._text = null; | CODE |
| HIGH⚡ | tests/test_a_screen_says_what_it_is_showing.py | 46 | appendChild(n){ this.kids.push(n); this._text = null; return n; } | CODE |
| HIGH⚡ | tests/test_a_screen_says_what_it_is_showing.py | 46 | appendChild(n){ this.kids.push(n); this._text = null; return n; } | CODE |
| HIGH⚡ | tests/test_a_screen_says_what_it_is_showing.py | 54 | return null; }; | CODE |
| HIGH⚡ | tests/test_a_screen_says_what_it_is_showing.py | 60 | if(c) e.className = c; if(x != null) e.textContent = x; return e; }; | CODE |
| HIGH⚡ | tests/test_a_screen_says_what_it_is_showing.py | 61 | const stage = {fleet: [], focus: 'b-two', pinned: null, grid: 1, turn: 0}; | CODE |
| HIGH⚡ | tests/test_a_screen_says_what_it_is_showing.py | 62 | const watched = () => stage.pinned || stage.focus; | CODE |
| HIGH | tests/test_a_screen_says_what_it_is_showing.py | 125 | out.push({w, h, arn: Number(box.style.getPropertyValue('--arn')), | CODE |
| HIGH | tests/test_a_screen_says_what_it_is_showing.py | 168 | kids: veil.kids.map(k => k.className || k.tag)}; | CODE |
| HIGH | tests/test_a_screen_says_what_it_is_showing.py | 311 | veilId: veil ? veil.id : null})); | CODE |
| HIGH | tests/test_a_screen_says_what_it_is_showing.py | 345 | out[id] = dot ? dot.title : null; | CODE |
| HIGH⚡ | tests/test_web_service.py | 323 | "let turnsEnded = 0, busyNow = false, queued = null, vanished = false;", | CODE |
| HIGH⚡ | tests/test_web_service.py | 327 | "function setQueued(t){ queued = t || null; }", | CODE |
| HIGH⚡ | tests/test_web_service.py | 327 | "function setQueued(t){ queued = t || null; }", | CODE |
| HIGH⚡ | tests/test_web_service.py | 328 | "function orphan(kind, text){ said.push([kind, text]); }", | CODE |
| HIGH⚡ | tests/test_web_service.py | 330 | "async function door(path, init){ posted.push(JSON.parse(init.body).text);", | CODE |
| HIGH | tests/test_web_service.py | 1083 | `setQueued(null)` - so somebody who had typed a follow-up while the agent | STRING |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 113 | " listeners:[], addEventListener(k, fn){ this.listeners.push([k, fn]); },", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 117 | "globalThis.orphan = (k, t) => said.push(t);", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 118 | "globalThis.ask = (path) => { asked.push(path); };", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 119 | "globalThis.queued = null;", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 211 | "let handler = null, sent = [];", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 214 | " dispatchEvent(e){ this.events.push(e.type); }, focus(){ this.focused = true; }};", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 216 | "globalThis.send = (t) => sent.push(t);", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 219 | " closest: sel => sel === '.eg' ? eg : null};", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 221 | "const other = {closest: () => null};", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 249 | " appendChild(k){ this.kids.push(k); }, append(...k){ this.kids.push(...k); },", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 258 | "let answer = null;", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 265 | " out.down = box.kids.map(k => k.t || k.cls);", | CODE |
| HIGH⚡ | tests/test_every_control_says_what_it_does.py | 267 | " out.empty = box.kids.map(k => k.t || k.cls);", | CODE |
| HIGH | tests/test_every_control_says_what_it_does.py | 70 | "globalThis.busyNow = false; globalThis.queued = null; paint(); seen.idle = go.dataset.mode;", | CODE |
| HIGH | tests/test_every_control_says_what_it_does.py | 321 | "let jump = null, said = [];", | CODE |
| HIGH | tests/test_every_control_says_what_it_does.py | 322 | "const btn = {hidden:false, set textContent(v){ said.push(v); },", | CODE |
| HIGH | tests/test_ui_drive.py | 188 | window.clicked.push(index + 1); | CODE |
| HIGH | tests/test_ui_drive.py | 728 | "() => { return document.querySelector('#late') !== null; }", | CODE |
| HIGH | tests/test_ui_drive.py | 753 | " return ids.filter(function (id) { return document.getElementById(id) !== null; }).length; }" | CODE |
| HIGH⚡ | tests/test_the_answer_pane_draws_markdown.py | 45 | constructor(tag){ this.tag = tag; this.kids = []; this.className = ''; this._text = null; } | CODE |
| HIGH⚡ | tests/test_the_answer_pane_draws_markdown.py | 51 | if(this._text != null){ this.kids.push(new Txt(this._text)); this._text = null; } | CODE |
| HIGH⚡ | tests/test_the_answer_pane_draws_markdown.py | 51 | if(this._text != null){ this.kids.push(new Txt(this._text)); this._text = null; } | CODE |
| HIGH⚡ | tests/test_the_answer_pane_draws_markdown.py | 52 | this.kids.push(n); return n; | CODE |
| HIGH⚡ | tests/test_the_answer_pane_draws_markdown.py | 59 | get(){ return undefined; }, | CODE |
| HIGH⚡ | tests/test_the_answer_pane_draws_markdown.py | 62 | createElement: t => { MADE.push(t); return new Node(t); }, | CODE |
| HIGH | tests/test_the_transcript_follows_the_agent.py | 84 | const $ = id => id === 'jump' ? jump : null; | CODE |
| HIGH | tests/test_the_transcript_follows_the_agent.py | 86 | let turn = { kids: [], appendChild(n){ this.kids.push(n); } }; | CODE |
| HIGH | tests/test_the_old_name_hands_over_its_data.py | 281 | " getItem(k){ return k in store ? store[k] : null; },", | CODE |
| HIGH | tests/test_the_old_name_hands_over_its_data.py | 311 | " getItem(k){ return k in store ? store[k] : null; },", | CODE |
| HIGH | tests/test_the_stage_of_screens.py | 43 | const watched = () => stage.pinned || stage.focus; | CODE |
| HIGH | tests/test_the_stage_of_screens.py | 79 | shots.push({w, h, bw, bh, cut, drawn: Math.round(drawn), above, | CODE |
| HIGH | tests/test_the_stage_of_screens.py | 175 | " answers.push(stage.grid);" | CODE |
| HIGH | tests/test_the_stage_of_screens.py | 277 | "const stage = {fleet: %s, focus: 'main', pinned: null, grid: 2, turn: 0};\n" | CODE |
| HIGH | tests/test_the_stage_of_screens.py | 325 | "const watched = () => stage.pinned || stage.focus;\n" | CODE |
| HIGH⚡ | tests/test_the_stage_of_screens.py | 337 | "\nprocess.stdout.write(JSON.stringify({big: box.dataset.big || null," | CODE |
| HIGH⚡ | tests/test_the_stage_of_screens.py | 337 | "\nprocess.stdout.write(JSON.stringify({big: box.dataset.big || null," | CODE |
| HIGH | tests/test_the_split_between_the_panes.py | 38 | getItem: k => (k in KEPT ? KEPT[k] : null), | CODE |
| HIGH⚡ | tests/test_the_session_column.py | 957 | "let asked = [], drawn = 0, made = null, stopped = 0;", | CODE |
| HIGH⚡ | tests/test_the_session_column.py | 960 | "globalThis.ask = async (path, body) => { asked.push(body); ", | CODE |
| HIGH⚡ | tests/test_the_session_column.py | 966 | "const btn = {dataset:{}, replaced: null, replaceWith(n){ this.replaced = n; }};", | CODE |
| HIGH⚡ | tests/test_the_session_column.py | 967 | "let onkey = null;", | CODE |
| HIGH | tests/test_the_session_column.py | 581 | assert "STORE + 'queued.' + (who || here)" in code, ( | CODE |
| 116 more matches not shown… | ||||
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | tests/test_engine.py | 46 | def test_a_download_in_flight_is_described_with_its_progress_and_runs_once(): | CODE |
| LOW | tests/test_engine.py | 64 | def test_settle_answers_as_soon_as_the_download_says_what_it_is(): | CODE |
| LOW | tests/test_engine.py | 80 | def test_a_given_binary_is_ready_at_once_and_never_downloaded(): | CODE |
| LOW | tests/test_engine.py | 91 | def test_a_failed_download_is_said_with_its_reason_and_can_be_retried(): | CODE |
| LOW | tests/test_engine.py | 110 | def test_an_unknown_total_is_described_in_bytes_so_far(): | CODE |
| LOW | tests/test_engine.py | 128 | def test_abandon_stops_a_download_whose_loop_swallows_exceptions(): | CODE |
| LOW | tests/test_engine.py | 155 | def test_run_in_the_calling_thread_drives_the_listener_phase_by_phase(): | CODE |
| LOW | tests/test_engine.py | 173 | def test_describe_before_anything_started_asks_to_call_again(): | CODE |
| LOW | tests/test_engine.py | 179 | def test_the_first_instant_of_a_download_reads_as_starting_not_as_zero(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 123 | async def test_the_workspace_is_read_from_the_server_like_any_other_client(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 140 | async def test_the_workspace_asks_the_one_question_that_starts_nothing(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 178 | async def test_an_unreadable_answer_says_so_instead_of_drawing_an_empty_room(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 204 | async def test_a_pane_asks_for_its_own_browser_and_not_for_the_focused_one(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 229 | async def test_the_live_pane_still_asks_for_the_focused_browser_when_none_is_named(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 243 | async def test_the_page_cannot_open_close_or_redirect_a_browser(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 265 | async def test_looking_at_a_pane_tells_the_agent_nothing(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 293 | def test_the_workspace_has_no_strip_and_no_pump_of_its_own(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 331 | def test_the_page_declares_no_identifier_twice(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 373 | def test_no_top_level_declaration_uses_a_name_declared_later(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 429 | def test_every_handler_the_page_wires_up_exists(): | CODE |
| LOW | tests/test_the_browser_workspace.py | 464 | def test_the_answer_measure_is_inside_the_range_every_source_agrees_on(): | CODE |
| LOW | tests/conftest.py | 211 | def _the_server_remembers_nothing_from_the_last_test(): | CODE |
| LOW | tests/test_readme_offers_both.py | 89 | def test_both_ways_in_are_offered(): | CODE |
| LOW | tests/test_readme_offers_both.py | 120 | def test_the_offer_comes_within_the_first_screen(): | CODE |
| LOW | tests/test_readme_offers_both.py | 140 | def test_the_check_fails_on_the_pages_as_they_used_to_be(): | CODE |
| LOW | tests/test_readme_offers_both.py | 166 | def test_a_comment_inside_a_fence_is_not_a_heading(): | CODE |
| LOW | tests/test_readme_offers_both.py | 198 | def test_the_two_ways_are_offered_as_one_choice(): | CODE |
| LOW | tests/test_readme_offers_both.py | 213 | def test_the_offer_says_what_each_one_costs_the_reader(): | CODE |
| LOW | tests/test_readme_offers_both.py | 231 | def test_no_tool_name_that_does_not_exist(): | CODE |
| LOW | tests/test_readme_offers_both.py | 248 | def test_no_hand_written_count_of_tools(): | CODE |
| LOW | tests/test_one_place_reads_a_saved_file.py | 32 | def test_both_halves_read_through_the_one_reader(tmp_path, monkeypatch): | CODE |
| LOW | tests/test_one_place_reads_a_saved_file.py | 53 | def test_both_halves_delete_through_the_one_eraser(tmp_path, monkeypatch): | CODE |
| LOW | tests/test_one_place_reads_a_saved_file.py | 76 | def test_erasing_something_that_is_not_there_is_not_a_failure(tmp_path, monkeypatch): | CODE |
| LOW | tests/test_one_place_reads_a_saved_file.py | 90 | def test_a_file_that_will_not_parse_reads_as_nothing_saved(tmp_path, monkeypatch): | CODE |
| LOW | tests/test_one_place_reads_a_saved_file.py | 113 | def test_the_saved_session_is_json_an_outside_reader_can_use(tmp_path, monkeypatch): | CODE |
| LOW⚡ | tests/test_the_suite_reads_no_real_session.py | 50 | def test_importing_a_test_module_cannot_reach_the_real_directory(): | CODE |
| LOW⚡ | tests/test_the_suite_reads_no_real_session.py | 58 | def test_and_neither_can_a_test_body(): | CODE |
| LOW⚡ | tests/test_the_suite_reads_no_real_session.py | 65 | def test_the_real_directory_is_named_after_the_package(): | CODE |
| LOW | tests/test_the_suite_reads_no_real_session.py | 92 | def test_the_server_starts_each_test_holding_nothing(): | CODE |
| LOW | tests/test_the_suite_reads_no_real_session.py | 108 | def test_the_conftest_imports_nothing_the_light_jobs_do_not_have(): | CODE |
| LOW | tests/test_the_suite_reads_no_real_session.py | 145 | def test_a_run_that_did_not_ask_for_an_engine_cannot_reach_one(): | CODE |
| LOW | tests/test_the_suite_reads_no_real_session.py | 220 | def test_the_guard_arms_exactly_when_the_engine_tests_are_deselected(): | CODE |
| LOW | tests/test_the_suite_reads_no_real_session.py | 256 | def test_only_one_place_knows_where_the_interface_is_stopped(): | CODE |
| LOW | tests/test_the_suite_reads_no_real_session.py | 309 | def test_the_brake_module_still_carries_all_three_defences(): | CODE |
| LOW⚡ | tests/test_agent.py | 67 | def test_answer_of_falls_back_for_non_text_content(): | CODE |
| LOW⚡ | tests/test_agent.py | 77 | def test_mcp_tools_to_openai_shape(): | CODE |
| LOW⚡ | tests/test_agent.py | 83 | async def test_run_task_drives_tools_then_returns_final_answer(): | CODE |
| LOW | tests/test_the_interface_reaches_a_real_server.py | 88 | async def test_the_idle_pane_is_answered_by_a_server_that_really_refused(live): | CODE |
| LOW | tests/test_the_interface_reaches_a_real_server.py | 133 | async def test_the_workspace_is_answered_by_the_server_and_not_by_the_interface(live): | CODE |
| LOW | tests/test_the_interface_reaches_a_real_server.py | 177 | async def test_the_client_really_is_a_separate_process(live): | CODE |
| LOW | tests/test_real_cli.py | 21 | async def test_the_loop_reads_a_data_url_heading(): | CODE |
| LOW | tests/test_llm.py | 5 | def test_key_arg_wins_then_env_then_error(): | CODE |
| LOW | tests/test_llm.py | 12 | def test_model_arg_env_default(): | CODE |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 108 | def test_the_frame_takes_the_shape_of_the_window_it_shows(): | CODE |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 144 | def test_the_four_states_are_four_different_pictures(): | STRING |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 191 | def test_a_screen_says_how_old_its_picture_is(): | STRING |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 227 | def test_a_screen_is_built_as_a_frame_with_its_name_on_it(): | STRING |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 254 | def test_an_empty_stage_says_what_to_do_about_being_empty(): | STRING |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 292 | def test_a_screen_is_named_by_its_action_and_described_by_its_state(): | STRING |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 322 | def test_the_dot_marks_the_browser_the_agent_is_actually_working_in(): | STRING |
| 845 more matches not shown… | ||||
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| MEDIUM⚡ | tests/test_cli_surface.py | 99 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_cli_surface.py | 101 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_cli_surface.py | 186 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_cli_surface.py | 188 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_cli_surface.py | 316 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_cli_surface.py | 318 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_cli_surface.py | 352 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_cli_surface.py | 354 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_web_service.py | 88 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_web_service.py | 90 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_web_service.py | 198 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_web_service.py | 200 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_web_service.py | 404 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_web_service.py | 406 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_web_service.py | 521 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_web_service.py | 523 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_web_service.py | 1051 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_web_service.py | 1053 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 165 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 167 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 259 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 261 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 316 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 318 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 359 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 361 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 399 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_openrouter_only.py | 406 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_agent_loop.py | 228 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_agent_loop.py | 230 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_agent_loop.py | 379 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_agent_loop.py | 381 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_agent_loop.py | 436 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_agent_loop.py | 438 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_agent_loop.py | 74 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_agent_loop.py | 76 | # -------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 54 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 56 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 105 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 107 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 149 | # ── what the mutation test asked for ──────────────────────────────────────── | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 227 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 229 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 309 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 311 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 491 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM⚡ | tests/test_key_isolation.py | 493 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_key_isolation.py | 341 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/test_key_isolation.py | 343 | # --------------------------------------------------------------------------- | COMMENT |
| MEDIUM | tests/mcp_server/test_no_interaction_by_script.py | 231 | # ── the other half: what the model can SEE ────────────────────────────────── | COMMENT |
| MEDIUM⚡ | tests/mcp_server/test_who_is_browsing.py | 39 | # ── nothing said ──────────────────────────────────────────────────────────── | COMMENT |
| MEDIUM⚡ | tests/mcp_server/test_who_is_browsing.py | 60 | # ── a seed, no profile ────────────────────────────────────────────────────── | COMMENT |
| MEDIUM⚡ | tests/mcp_server/test_who_is_browsing.py | 88 | # ── a profile ─────────────────────────────────────────────────────────────── | COMMENT |
| MEDIUM | .github/workflows/publish-wiki.yml | 1 | # ───────────────────────────────────────────────────────────────────────────── | COMMENT |
| MEDIUM | src/invisible_playwright_mcp/mcp/actions.py | 959 | # ── files, picked the way a person picks them ────────────────────────────── | COMMENT |
| MEDIUM | src/invisible_playwright_mcp/mcp/actions.py | 1360 | # ── evaluate, and the one thing it must not be used for ───────────────────── | COMMENT |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | tests/test_engine.py | 7 | CODE | |
| LOW | tests/test_the_browser_workspace.py | 14 | CODE | |
| LOW | tests/conftest.py | 41 | CODE | |
| LOW | tests/test_readme_offers_both.py | 21 | CODE | |
| LOW | tests/test_one_place_reads_a_saved_file.py | 24 | CODE | |
| LOW | tests/test_the_suite_reads_no_real_session.py | 25 | CODE | |
| LOW | tests/test_the_interface_reaches_a_real_server.py | 42 | CODE | |
| LOW | tests/_sessions.py | 21 | CODE | |
| LOW | tests/test_a_screen_says_what_it_is_showing.py | 17 | CODE | |
| LOW | …test_the_extension_archive_holds_only_the_extension.py | 10 | CODE | |
| LOW | tests/test_a_failed_selector_says_what_to_do.py | 25 | CODE | |
| LOW | tests/test_cli_surface.py | 14 | CODE | |
| LOW | tests/test_cli_surface.py | 23 | CODE | |
| LOW | tests/test_publishing_surfaces.py | 51 | CODE | |
| LOW | tests/test_the_transcript_is_what_was_said.py | 20 | CODE | |
| LOW | tests/_cli_brake.py | 28 | CODE | |
| LOW | tests/test_no_tracked_file_carries_a_control_byte.py | 21 | CODE | |
| LOW | …s/test_the_product_surface_is_what_the_product_uses.py | 68 | CODE | |
| LOW | tests/test_one_conversation_one_server.py | 28 | CODE | |
| LOW | tests/_loop.py | 18 | CODE | |
| LOW | tests/test_web_service.py | 13 | CODE | |
| LOW | tests/test_every_control_says_what_it_does.py | 16 | CODE | |
| LOW | tests/test_a_workflow_runs_on_something_that_happens.py | 32 | CODE | |
| LOW | tests/test_openrouter_only.py | 23 | CODE | |
| LOW | tests/test_engine_first.py | 13 | CODE | |
| LOW | tests/test_ui_drive.py | 34 | CODE | |
| LOW | tests/test_one_reader_of_a_tool_result.py | 32 | CODE | |
| LOW | tests/test_the_answer_pane_draws_markdown.py | 25 | CODE | |
| LOW | tests/test_the_gate_says_which_tree_it_judged.py | 30 | CODE | |
| LOW | tests/test_the_prompt_says_each_thing_once.py | 21 | CODE | |
| LOW | tests/test_the_wiki_samples_name_real_fields.py | 22 | CODE | |
| LOW | tests/test_a_rule_is_written_where_it_is_applied.py | 21 | CODE | |
| LOW | tests/test_the_floor_carries_what_the_tools_need.py | 22 | CODE | |
| LOW | tests/test_the_page_is_assembled_from_its_files.py | 9 | CODE | |
| LOW | tests/test_the_contrast_floors_are_computed.py | 29 | CODE | |
| LOW | tests/test_the_bundle_holds_only_the_server.py | 11 | CODE | |
| LOW | tests/_page.py | 15 | CODE | |
| LOW | tests/test_agent_loop.py | 44 | CODE | |
| LOW | tests/test_the_transcript_follows_the_agent.py | 47 | CODE | |
| LOW | tests/test_every_tool_reads_as_english.py | 18 | CODE | |
| LOW | tests/test_the_old_name_hands_over_its_data.py | 22 | CODE | |
| LOW | tests/test_env_file.py | 23 | CODE | |
| LOW | tests/test_the_codex_plugin_really_installs.py | 23 | CODE | |
| LOW | tests/test_the_stage_of_screens.py | 26 | CODE | |
| LOW | tests/test_what_is_imported_is_declared.py | 28 | CODE | |
| LOW | tests/test_the_page_can_be_read.py | 16 | CODE | |
| LOW | tests/test_release_pages.py | 47 | CODE | |
| LOW | tests/test_the_version_describes_the_code.py | 25 | CODE | |
| LOW | tests/test_the_split_between_the_panes.py | 16 | CODE | |
| LOW | tests/test_the_suite_declares_asyncio_once.py | 18 | CODE | |
| LOW | tests/test_the_session_column.py | 23 | CODE | |
| LOW | tests/test_the_event_stream_wire_format.py | 21 | CODE | |
| LOW | tests/test_the_plugin_really_installs.py | 49 | CODE | |
| LOW | tests/test_the_page_meets_the_craft_floor.py | 13 | CODE | |
| LOW | tests/test_key_isolation.py | 27 | CODE | |
| LOW | tests/test_version_is_not_taken.py | 49 | CODE | |
| LOW | tests/test_the_page_tells_the_truth.py | 8 | CODE | |
| LOW | tests/test_a_reconnection_keeps_what_you_typed.py | 21 | CODE | |
| LOW | tests/mcp_server/test_addressing.py | 10 | CODE | |
| LOW | tests/mcp_server/test_no_interaction_by_script.py | 27 | CODE | |
| 62 more matches not shown… | ||||
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | pyproject.toml | 21 | "Programming Language :: Python :: 3.12", | COMMENT |
| LOW | pyproject.toml | 41 | # url, and both are read off what page.goto() answers. Until 0.13.2 that | COMMENT |
| LOW | pyproject.toml | 61 | # downloaded into the package's redirected AppData and could not be | COMMENT |
| LOW | pyproject.toml | 81 | # | COMMENT |
| LOW | pyproject.toml | 101 | # resize border off the screen and two borders wider than the work area, | COMMENT |
| LOW | pyproject.toml | 121 | # faster and needs no second package for CSS selection (lxml wants | COMMENT |
| LOW | pyproject.toml | 141 | # major this cannot use, `import invisible_playwright_mcp` fails for everybody with a | COMMENT |
| LOW | pyproject.toml | 161 | # Italian word list to check the tool descriptions. An import that relies on a | COMMENT |
| LOW | pyproject.toml | 201 | testpaths = ["tests"] | COMMENT |
| LOW | tests/test_readme_offers_both.py | 21 | from __future__ import annotations | COMMENT |
| LOW | tests/test_publishing_surfaces.py | 81 | #: No `.cursor-plugin/plugin.json` since 2026-09-13 night: Cursor's own docs say | COMMENT |
| LOW | tests/test_publishing_surfaces.py | 101 | #: is held identical to the README's by scripts/check_content.py, which scans | COMMENT |
| LOW | …s/test_the_product_surface_is_what_the_product_uses.py | 181 | if isinstance(owner, ast.Name) and owner.id in aliases: | COMMENT |
| LOW | tests/test_web_service.py | 421 | assert paths == {"/", | COMMENT |
| LOW | tests/test_ui_drive.py | 241 | "done.html": DONE_HTML, | COMMENT |
| LOW | tests/test_ui_drive.py | 441 | os.environ, | COMMENT |
| LOW | tests/test_ui_drive.py | 461 | # already been paid. | COMMENT |
| LOW | tests/test_the_transcript_follows_the_agent.py | 201 | """ | COMMENT |
| LOW | tests/test_release_pages.py | 61 | #: last version that belongs to it: (name, after, through). | COMMENT |
| LOW | tests/test_release_pages.py | 81 | #: deletion is declared, like the boundary, and checked in the one direction the | COMMENT |
| LOW | tests/test_release_pages.py | 101 | COMMENT | |
| LOW | tests/test_the_session_column.py | 121 | assert a is b is c | COMMENT |
| LOW | tests/test_the_plugin_really_installs.py | 61 | COMMENT | |
| LOW | tests/test_key_isolation.py | 141 | """Known-bad is the current code: the same secret under OPENAI_API_KEY is | COMMENT |
| LOW | tests/test_version_is_not_taken.py | 61 | COMMENT | |
| LOW | tests/mcp_server/test_open_first.py | 501 | assert rows[1]["urls"] == [] | COMMENT |
| LOW | tests/mcp_server/test_stdio_e2e.py | 41 | sdk = _installed("mcp") | COMMENT |
| LOW | tests/mcp_server/test_server_list.py | 21 | # Added in 0.15.0 with the rest of the multi-browser work: a session | COMMENT |
| LOW | tests/mcp_server/test_server_list.py | 41 | # navigate` opens the first page by itself. | COMMENT |
| LOW | scripts/check_content.py | 61 | import tomllib | COMMENT |
| LOW | scripts/check_content.py | 101 | PNG_OK = {b"IHDR", b"PLTE", b"IDAT", b"IEND", b"tRNS", b"gAMA", b"cHRM", | COMMENT |
| LOW | scripts/check_content.py | 121 | #: was skipped, and the gate printed `clean`. The four mutations in `selftest` | COMMENT |
| LOW | scripts/check_content.py | 381 | #: corrected by hand, and the only reason anybody looked was that a new page | COMMENT |
| LOW | scripts/check_content.py | 401 | #: | COMMENT |
| LOW | .github/workflows/publish.yml | 1 | name: publish | COMMENT |
| LOW | .github/workflows/publish.yml | 201 | COMMENT | |
| LOW | .github/workflows/publish-wiki.yml | 1 | # ───────────────────────────────────────────────────────────────────────────── | COMMENT |
| LOW | .github/workflows/publish-wiki.yml | 21 | # and 0.65.0 moved. The wiki was showing the previous month's numbers to anybody | COMMENT |
| LOW | .github/workflows/ci.yml | 101 | run: | | COMMENT |
| LOW | .github/workflows/ci.yml | 201 | python scripts/pack_bundle.py --smithery --output /tmp/bundle | COMMENT |
| LOW | src/invisible_playwright_mcp/link.py | 61 | # | COMMENT |
| LOW | src/invisible_playwright_mcp/link.py | 261 | COMMENT | |
| LOW | src/invisible_playwright_mcp/sessions.py | 61 | # ATTRIBUTE TO OVERWRITE. It was `self._open_link = self._spawn_link`, | COMMENT |
| LOW | src/invisible_playwright_mcp/sessions.py | 81 | #: conversations at the same instant. That is rare - a page opening, a | COMMENT |
| LOW | src/invisible_playwright_mcp/env.py | 41 | #: "1" when the process is spawned by a host that opens and closes its browser | COMMENT |
| LOW | src/invisible_playwright_mcp/chat.py | 21 | #: package - with a comment saying the two had to agree and a test asserting | COMMENT |
| LOW | src/invisible_playwright_mcp/chat.py | 41 | #: real saved file, not by a test: the developer's own `default.json` held | COMMENT |
| LOW | src/invisible_playwright_mcp/chat.py | 261 | # asking somebody to name a conversation before having it is | COMMENT |
| LOW | src/invisible_playwright_mcp/chat.py | 281 | # `err` with the single word `stopped`, so a deliberate and | COMMENT |
| LOW | src/invisible_playwright_mcp/cli.py | 161 | serving = ctx.invoked_subcommand is None | COMMENT |
| LOW | src/invisible_playwright_mcp/cli.py | 181 | # that was the whole visible evidence. | COMMENT |
| LOW | src/invisible_playwright_mcp/cli.py | 241 | # IT. Both said the same thing - the flag beats the variable, an empty one | COMMENT |
| LOW | src/invisible_playwright_mcp/agent.py | 41 | "emoji: not as a status marker in front of a line, not as a bullet, not for " | COMMENT |
| LOW | src/invisible_playwright_mcp/agent.py | 281 | if self.tool_defs is None: | COMMENT |
| LOW | src/invisible_playwright_mcp/agent.py | 301 | # and unlike the ceiling it can also stop a run on turn three. | COMMENT |
| LOW | src/invisible_playwright_mcp/agent.py | 341 | # Recoverable: telling the model its arguments were unreadable | COMMENT |
| LOW | src/invisible_playwright_mcp/agent.py | 381 | # `Conversation`, kept because about twenty-five tests drove the loop through | COMMENT |
| LOW | src/invisible_playwright_mcp/routes.py | 201 | service = await which(request) | COMMENT |
| LOW | src/invisible_playwright_mcp/routes.py | 221 | # be two accounts of one rule, free to disagree. | COMMENT |
| LOW | src/invisible_playwright_mcp/routes.py | 241 | # carries "1" and "0": a page older than this server keeps | COMMENT |
| 48 more matches not shown… | ||||
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| HIGH | tests/test_engine.py | 141 | except Exception: | CODE |
| HIGH | tests/conftest.py | 270 | except OSError: | CODE |
| HIGH | tests/conftest.py | 261 | except OSError: | CODE |
| HIGH | tests/conftest.py | 266 | except OSError: | CODE |
| HIGH | tests/test_web_service.py | 770 | except (asyncio.TimeoutError, StopAsyncIteration): | CODE |
| HIGH | tests/test_ui_drive.py | 371 | except RuntimeError: | CODE |
| HIGH | tests/mcp_server/test_watch.py | 299 | except RuntimeError: | CODE |
| HIGH | src/invisible_playwright_mcp/cli.py | 299 | except KeyboardInterrupt: | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| MEDIUM | docs/model-context-protocol-vs-api-vs-rag.md | 75 | ## MCP vs RAG | COMMENT |
| MEDIUM | docs/agentic-browser-open-source.md | 8 | # Open-source agentic browsers | COMMENT |
| MEDIUM | docs/agentic-browser-open-source.md | 132 | - [Top 5 agentic browsers in 2026](https://seraphicsecurity.com/learn/ai-browser/top-5-agentic-browsers-in-2026-capabili | CODE |
| MEDIUM | docs/is-openai-operator-still-available.md | 150 | - [OpenAI help: ChatGPT agent](https://help.openai.com/en/articles/11752874-chatgpt-agent) and [OpenAI help: Evolving At | CODE |
| MEDIUM | docs/best-ai-browser-agent.md | 166 | - [Wikipedia: OpenAI Operator](https://en.wikipedia.org/wiki/OpenAI_Operator), retrieved 2026-09-03, and [OpenAI help: E | CODE |
| MEDIUM | docs/what-is-an-agentic-browser.md | 8 | # What is an agentic browser? | COMMENT |
| MEDIUM | docs/what-is-an-agentic-browser.md | 44 | ## The four things any agentic browser has to solve | COMMENT |
| MEDIUM | docs/what-is-an-agentic-browser.md | 139 | - [Top 5 agentic browsers in 2026: capabilities and security risks](https://seraphicsecurity.com/learn/ai-browser/top-5- | CODE |
| MEDIUM | docs/what-is-an-agentic-browser.md | 140 | - [What is an agentic browser](https://www.sigmabrowser.com/blog/what-is-an-agentic-browser-best-agentic-browsers-in-202 | CODE |
| MEDIUM | docs/openai-operator-alternatives.md | 177 | - [OpenAI help: ChatGPT agent](https://help.openai.com/en/articles/11752874-chatgpt-agent) and [OpenAI help: Evolving At | CODE |
| MEDIUM | src/invisible_playwright_mcp/agent.py | 267 | # size IS the current occupancy of the context window; adding them up | COMMENT |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | tests/test_engine.py | 141 | except Exception: | CODE |
| LOW | tests/test_version_is_not_taken.py | 165 | except Exception: | CODE |
| LOW | tests/mcp_server/test_click_diagnosis.py | 77 | except Exception: | CODE |
| LOW | tests/mcp_server/test_a_gone_browser_is_said.py | 92 | except Exception as raised: | CODE |
| LOW | tests/mcp_server/test_a_host_that_opens_the_browser.py | 39 | except Exception as exc: | CODE |
| LOW | tests/mcp_server/test_read_text_in_a_browser.py | 90 | except Exception as exc: # noqa: BLE001 - the test reads it | CODE |
| LOW | src/invisible_playwright_mcp/link.py | 258 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/engine.py | 170 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/chat.py | 288 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/quiet.py | 40 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/storage.py | 181 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/agent.py | 370 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/chats.py | 78 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/chats.py | 83 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/routes.py | 338 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/routes.py | 382 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/clean.py | 715 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/clean.py | 274 | except Exception: | STRING |
| LOW | src/invisible_playwright_mcp/mcp/clean.py | 359 | except Exception as exc: | STRING |
| LOW | src/invisible_playwright_mcp/mcp/session.py | 99 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/session.py | 231 | except Exception as refused: | CODE |
| MEDIUM | src/invisible_playwright_mcp/mcp/actions.py | 197 | def order_of(selector): | CODE |
| MEDIUM | src/invisible_playwright_mcp/mcp/actions.py | 933 | def choose(): | CODE |
| LOW | src/invisible_playwright_mcp/mcp/actions.py | 701 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/actions.py | 755 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/actions.py | 758 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/actions.py | 936 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/actions.py | 200 | except Exception: # noqa: BLE001 - the selector stays out, not the snapshot | STRING |
| LOW | src/invisible_playwright_mcp/mcp/work.py | 386 | except Exception as exc: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/work.py | 699 | except Exception: | CODE |
| LOW | src/invisible_playwright_mcp/mcp/work.py | 742 | except Exception: | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | tests/conftest.py | 66 | CODE | |
| LOW | tests/conftest.py | 239 | CODE | |
| LOW | …s/test_the_product_surface_is_what_the_product_uses.py | 81 | CODE | |
| LOW | …s/test_the_product_surface_is_what_the_product_uses.py | 97 | CODE | |
| LOW | …s/test_the_product_surface_is_what_the_product_uses.py | 145 | CODE | |
| LOW | …s/test_the_product_surface_is_what_the_product_uses.py | 201 | CODE | |
| LOW | tests/test_a_rule_is_written_where_it_is_applied.py | 76 | CODE | |
| LOW | tests/test_agent_loop.py | 751 | CODE | |
| LOW | tests/test_what_is_imported_is_declared.py | 83 | CODE | |
| LOW | tests/test_what_is_imported_is_declared.py | 115 | CODE | |
| LOW | tests/test_the_page_tells_the_truth.py | 796 | CODE | |
| LOW | tests/mcp_server/test_every_tool_says_what_it_does.py | 100 | CODE | |
| LOW | scripts/check_content.py | 155 | CODE | |
| LOW | scripts/check_content.py | 238 | CODE | |
| LOW | scripts/check_content.py | 265 | CODE | |
| LOW | scripts/check_content.py | 526 | CODE | |
| LOW | scripts/sync_article_pixels.py | 124 | CODE | |
| LOW | scripts/build_wiki.py | 38 | CODE | |
| LOW | src/invisible_playwright_mcp/mcp/clean.py | 395 | CODE | |
| LOW | src/invisible_playwright_mcp/mcp/actions.py | 1164 | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| MEDIUM | tests/test_the_answer_pane_draws_markdown.py | 405 | # as the script being unparseable rather than as the harness being unable to | COMMENT |
| MEDIUM | tests/_page.py | 19 | #: Everything from the opening tag on, which is what a harness executes. | COMMENT |
| MEDIUM | tests/_page.py | 30 | #: across from the retired key. First in the script by construction, so a harness | COMMENT |
| MEDIUM | tests/test_the_split_between_the_panes.py | 78 | # as the splitter being broken rather than the harness being short a line. | COMMENT |
| MEDIUM | tests/test_the_page_tells_the_truth.py | 1326 | #: how the harness first reported a defect that was its own. | STRING |
| MEDIUM | tests/test_a_reconnection_keeps_what_you_typed.py | 112 | # harness that defined the prefix itself would keep passing after the page | COMMENT |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | src/invisible_playwright_mcp/_version.py | 154 | __all__ = ["__version__", "__install_record_version__", "versions", | CODE |
| LOW | src/invisible_playwright_mcp/__init__.py | 7 | __all__ = ["__version__", "__install_record_version__"] | CODE |
| LOW | src/invisible_playwright_mcp/mcp/clean.py | 44 | __all__ = ["clean_page", "CleanError", "VISIBLE_HTML_JS"] | CODE |
| LOW | src/invisible_playwright_mcp/mcp/__init__.py | 46 | __all__ = ["__version__", "DEFAULT_BROWSER_ID", "SUPPORT_BROWSER_ID", "NOT_OPEN", "OPENED", "GONE"] | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW⚡ | …s/test_the_product_surface_is_what_the_product_uses.py | 369 | product = {"agent.py": "def run_task(mcp, task):\n return task\n"} | CODE |
| LOW | tests/_loop.py | 23 | async def run_task(mcp, task: str, *, client, model: str, | CODE |
| Severity | File | Line | Snippet | Context |
|---|---|---|---|---|
| LOW | src/invisible_playwright_mcp/mcp/clean.py | 206 | "role", "type", "href", "value", "placeholder", "title", "alt", "action", | STRING |