cmux-next: agents can screenshot the app again; browser pages are blacked out

Merged
TE
15d ago
+429-65

Leo-ask: ask-49f4ca2104

Takes back part of 45fe246e9ab9 ("fix(security): refuse unguarded app screenshots", lawrencecchen, 2026-10-20): it made window.screenshot, pane.screenshot and tab.screenshot person-only, so an agent had no way to see a terminal or a chat window. Leo: "cmux only lets a person run the window screenshot action. doesnt seem right". This PR keeps that fix's guarantee: no unmasked browser pixels reach automation.

Summary

  • An automation run (CLI, MCP, scripts) of cmux window|pane|tab screenshot runs again. A person's run (palette, menu) is unchanged.
  • What an automation image hides (ScreenshotPrivacyMask): these areas are painted opaque black, because the app can't reproduce the browser host's secret mask (vault, TOTP, typed secrets):
    • every pane showing a browser tab (the whole browser view, toolbar included);
    • every WKWebView not held by the app's own agent chat (AgentPaneView) or page tabs (InternalPageView), so an unlisted web view fails closed;
    • every Chromium page child window.
  • What it still shows: the app's own React pages on Chromium are painted back. The overlay panels and the app's other windows are left out.
  • Refused for automation: a browser tab, or a pane showing one. The refusal points at the browser REPL's page.screenshot(), which masks secrets.
  • The cmux and cmux-browser skills now say what an agent's screenshot shows.

Risk

risk:high: this changes a security policy (a teammate's fail-closed screenshot gate). It takes back part of a commit pushed straight to feat-cmux-next, which has no PR number.

Changelog

Fixed: Agents can screenshot cmux windows, panes and terminal tabs again; browser pages in the image are blacked out.

Testing

Test first:

  • ActionOriginTests/appScreenshotsRunForEveryCaller: CLI, MCP, script and no-origin runs reach the handler as non-person runs.
  • ScreenshotPrivacyMaskTests:
    • a foreign web view is masked, including when nested;
    • the app's page view is not masked;
    • a hidden web view is not masked;
    • the mask layer paints opaque black.

Observed / Not run

  • Review: an independent review of 238d98091d43 failed on three points, all fixed there:
    • an app's user-origin run (during a person's gesture) was unmasked;
    • only direct Chromium child windows were masked;
    • the masks were measured after the capture.
      A re-review of 238d98091d43 passed (SHIP). A review of the follow-up 91e026accafb, where a masked capture whose masks cannot be painted now throws instead of sending the unmasked image, also passed (SHIP).
  • Fleet app compile: passed at 238d98091d43 (job 04bdb0f38d2c, 80 s) and at the head 91e026accafb (job 7a50e9fb5e1e, 86 s).
  • Red and green test runs: package-test-lane ActionOriginTests,ScreenshotPrivacyMaskTests on scratch refs (the red tests, then the fix, each with #23935's build-script patch for the base ContextSection.css red). Queued on the fleet; results to be posted as a comment.
  • PR reds that come from base: cmux-next crash-class rules (an objc-selector probe on CEFFileDragRelay.swift, which #23790 added) and the Checks job structure step of cmux-next checks. This PR touches neither.
  • Not run: a live capture of a window with a browser and a terminal side by side; no test covers nested Chromium child windows.
  • Follow-ups from the review, not in this PR: a resize during the capture can shift the masks; the Passwords page (sites and usernames) is not masked; the web-view scan starts at contentView.
  • No capture: this changes what automation receives, not the app's UI.

Agent session: cosmic-gray

🤖 Generated with Claude Code