cmux-next: agents can screenshot the app again; browser pages are blacked out
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 screenshotruns 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
WKWebViewnot 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
cmuxandcmux-browserskills 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).
- an app's
- 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,ScreenshotPrivacyMaskTestson scratch refs (the red tests, then the fix, each with #23935's build-script patch for the baseContextSection.cssred). 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 onCEFFileDragRelay.swift, which #23790 added) and theChecks job structurestep ofcmux-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