Codex desktop in-app browser screenshots are DPI-scaled twice and tiled on Windows

Open 💬 1 comment Opened Jul 9, 2026 by guajun

What version of the Codex App are you using (From “About Codex” dialog)?

26.623.19656.0

Additional local browser plugin/cache version observed:

C:\Users\gyli\.codex\plugins\cache\openai-bundled\browser\26.623.141536

What subscription do you have?

Not sure / not relevant to this screenshot backend issue.

What platform is your computer?

Microsoft Windows 11 Pro 64-bit

Version: 10.0.26200

Build: 26200

What issue are you seeing?

Codex desktop in-app browser screenshots are incorrectly scaled and can be returned as a repeated 2x2 tiled image on Windows after using the documented viewport capability.

This affects the in-app browser screenshot API enough that screenshots cannot be trusted for visual evidence or coordinate-based automation. DOM inspection and page navigation still work; the failure appears to be in the viewport/screenshot coordinate and bitmap capture path.

Possibly related to #19429, but that issue is closed and described a wrong-region capture on macOS. This report is a current Windows repro where the screenshot output becomes oversized and visibly tiled.

What steps can reproduce the bug?

Use the Codex desktop in-app browser with a non-sensitive test page. No login or third-party site is required.

if (globalThis.agent?.browsers == null) {
  const { setupBrowserRuntime } = await import(
    "C:/Users/gyli/.codex/plugins/cache/openai-bundled/browser/26.623.141536/scripts/browser-client.mjs"
  );
  await setupBrowserRuntime({ globals: globalThis });
}

globalThis.iab = await agent.browsers.get("iab");
globalThis.tab = await iab.tabs.new();

const html = `<!doctype html>
<html>
<head>
  <style>
    html,body{margin:0;width:100vw;height:100vh;overflow:hidden;background:white}
    .q{position:fixed;width:50vw;height:50vh}
    .tl{left:0;top:0;background:#e53935}
    .tr{right:0;top:0;background:#00a878}
    .bl{left:0;bottom:0;background:#2563eb}
    .br{right:0;bottom:0;background:#ffd166}
    .v{position:fixed;left:50vw;top:0;width:4px;height:100vh;background:#000}
    .h{position:fixed;left:0;top:50vh;width:100vw;height:4px;background:#000}
  </style>
</head>
<body>
  <div class="q tl"></div><div class="q tr"></div>
  <div class="q bl"></div><div class="q br"></div>
  <div class="v"></div><div class="h"></div>
</body>
</html>`;

await tab.goto("data:text/html;charset=utf-8," + encodeURIComponent(html));
await (await iab.capabilities.get("viewport")).set({ width: 1920, height: 1080 });

const metrics = await tab.playwright.evaluate(() => ({
  innerWidth: window.innerWidth,
  innerHeight: window.innerHeight,
  devicePixelRatio: window.devicePixelRatio,
  visualViewport: window.visualViewport && {
    width: window.visualViewport.width,
    height: window.visualViewport.height,
    scale: window.visualViewport.scale,
  },
  clientWidth: document.documentElement.clientWidth,
  clientHeight: document.documentElement.clientHeight,
  scrollWidth: document.documentElement.scrollWidth,
  scrollHeight: document.documentElement.scrollHeight,
}));

const bytes = await tab.screenshot({});

I also reproduced it with ordinary authenticated pages, but the data URL above is sufficient and avoids leaking private user data.

Observed diagnostic data

On this machine:

| viewport action | DOM innerWidth x innerHeight | devicePixelRatio | screenshot size | notes |
| --- | ---: | ---: | ---: | --- |
| reset() | 511 x 1041 | 0.8375 | 763 x 1519 | not a normal desktop viewport |
| set({ width:1280, height:720 }) | 1910 x 1074 | 0.6700 | 2851 x 1603 | visibly tiled |
| set({ width:1600, height:900 }) | 2388 x 1343 | 0.6700 | 3564 x 2004 | visibly tiled |
| set({ width:1920, height:1080 }) | 2865 x 1611 | 0.6700 | 4276 x 2404 | visibly tiled 2x2 |

Additional checks after set({ width:1920, height:1080 }):

| screenshot call | returned size | result |
| --- | ---: | --- |
| tab.screenshot({}) | 4276 x 2404 | oversized 2x2 tiled capture |
| tab.screenshot({ fullPage: true }) | 4276 x 2404 | same oversized/tiled result |
| tab.screenshot({ clip: { x:0, y:0, width:1920, height:1080 } }) | 2866 x 1612 | still in the wrong scaled/tiled coordinate system |
| tab.screenshot({ clip: { x:0, y:0, width:960, height:540 } }) | 1433 x 806 | also scaled unexpectedly |
| tab.screenshot({ clip: { x:0, y:0, width:2138, height:1202 } }) | 3191 x 1794 | also scaled unexpectedly |

The returned image bytes were JPEG even when saved to a .png filename by the caller. The file extension is not the root cause; the bitmap content itself is already wrong.

What is the expected behavior?

The in-app browser viewport capability should have consistent, documented semantics:

  • If viewport.set({ width:1920, height:1080 }) is in CSS pixels, the page should report window.innerWidth === 1920 and window.innerHeight === 1080.
  • If the capability is in physical pixels or DIP, the documentation should say so and the screenshot API should still return one correctly rendered page image.
  • tab.screenshot({}) should capture the visible viewport once, not as a repeated/tiled bitmap.
  • tab.screenshot({ fullPage:true }) should not return the same broken viewport tile.
  • tab.screenshot({ clip }) should crop in a clear coordinate system and return the requested region, not a differently scaled/tiled coordinate space.

Additional information

The ratios suggest inverse DPI scaling may be applied twice:

  • 2865 / 1920 ≈ 1.492
  • 4276 / 2865 ≈ 1.492
  • 1 / 0.6700 ≈ 1.492

So the observed behavior is approximately:

DOM viewport width ≈ requested width / DPR
screenshot width ≈ DOM viewport width / DPR

This is consistent with a Windows DPI / device-independent-pixel conversion bug in the in-app browser viewport or screenshot path.

agent.browsers.list() on the affected session reports only the Codex in-app browser backend:

[
  {
    "name": "Codex In-app Browser",
    "type": "iab",
    "capabilities": {
      "browser": ["visibility", "viewport"],
      "tab": ["pageAssets"]
    }
  }
]

For workflows that need reliable visual screenshots, this currently forces users to avoid the in-app browser and use a real Chrome/Edge/Chromium browser capture engine instead.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗