Codex desktop in-app browser screenshots are DPI-scaled twice and tiled on Windows
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 reportwindow.innerWidth === 1920andwindow.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.4924276 / 2865 ≈ 1.4921 / 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗