Nikita Orlov

How apps hide windows from screen capture — and what broke in macOS 15

by



If you screenshot Netflix or Spotify's playback window, you get a black rectangle where the video should be. Same thing when sharing your screen in Zoom, recording in OBS, even in Snipping Tool. Audio plays fine, content is gone.

This isn't codec protection or some OpenGL surface trick. It's one flag in one API that tells the window system: "this window should not appear in captured frames." The flag is public, documented, has been in Windows 10 since 2020, and is used by any app that needs to hide content from screenshots — password managers, banking clients, 2FA tokens.

On macOS there used to be a symmetric equivalent, but in macOS 15 Sequoia Apple broke it against ScreenCaptureKit, and now the situation there is considerably messier. On Linux it depends on the display server. In browsers it works through a chain of platform APIs.

We worked all this out while building a desktop app for online interviews that needs this mechanism for a concrete reason: the hints window shouldn't appear in the interviewer's screen share. One paragraph about the product at the end. Everything else in this article is about what's under the hood.

Who captures the screen when you hit Share Screen

When Zoom asks for screen sharing access, it's not taking photos of your monitor. It subscribes to a frame stream from the operating system. Which API it uses matters, because they have different architectures and different behavior with protected windows.

On Windows there are three main paths.


GDI BitBlt from desktop DC. The oldest method, works since Windows 2000. Calling BitBlt from GetDC(NULL) copies pixels from the DWM (Desktop Window Manager) surface into an arbitrary HDC. Slow, no hardware acceleration, but works everywhere. Used by old applications and some monitoring utilities.

Desktop Duplication API (DXGI). Arrived in Windows 8. Works via IDXGIOutputDuplication::AcquireNextFrame — returns a GPU texture with the current desktop frame already composited from all windows. Fast, hardware-accelerated, but captures only a whole monitor at a time. Used in classic Zoom, TeamViewer, AnyDesk, and old Teams.

IDXGIOutputDuplication* duplication = nullptr;
output1->DuplicateOutput(d3dDevice, &duplication);

DXGI_OUTDUPL_FRAME_INFO frameInfo;
IDXGIResource* desktopResource = nullptr;
duplication->AcquireNextFrame(500, &frameInfo, &desktopResource);
// desktopResource contains a texture with the current desktop frame

Windows.Graphics.Capture (WGC). Arrived in Windows 10 1803 (2018). This is what Microsoft officially recommends now. Can capture either an entire monitor or a specific window by HWND. Used in modern OBS, Microsoft Teams, Chromium (and therefore all browser-based calls), updated Zoom, and the system Snipping Tool.

auto item = GraphicsCaptureItem::CreateFromHwnd(targetHwnd);
auto device = CreateDirect3DDevice(dxgiDevice);
auto framePool = Direct3D11CaptureFramePool::Create(
    device, DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, size);
auto session = framePool.CreateCaptureSession(item);
session.StartCapture();
// frames arrive via FrameArrived


All three methods ultimately read pixels from DWM — the only place in Windows where "what's currently on screen" actually lives. That's where the protection mechanism comes from.


SetWindowDisplayAffinity: three modes and how they work


DWM has a display affinity attribute for each window. It tells the compositor how to include that window in capture streams. Set via SetWindowDisplayAffinity:

BOOL SetWindowDisplayAffinity(HWND hwnd, DWORD affinity);


Three values:

Constant

Value

Behavior

WDA_NONE

0x00

Default. Window visible in all captures

WDA_MONITOR

0x01

Window shows on monitor, appears as black rectangle in captures

WDA_EXCLUDEFROMCAPTURE

0x11

Window completely absent from capture, as if it doesn't exist

WDA_MONITOR works since Windows Vista. This is what Netflix and Spotify use — hence the black square on Netflix screenshots. The observer can see that something is there, but doesn't get the content.

WDA_EXCLUDEFROMCAPTURE is the cleaner option, arrived in Windows 10 version 2004 (May 2020 Update, build 19041). The difference is fundamental: with WDA_MONITOR a black hole in the shape of the window remains in the captured frame. With WDA_EXCLUDEFROMCAPTURE the window isn't in the stream at all, and whatever is behind it shows through.

Minimal working example in C++:

#include <windows.h>
#include <cstdio>

int main() {
    HWND hwnd = GetConsoleWindow();
    if (!SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE)) {
        printf("SetWindowDisplayAffinity failed: %lu\n", GetLastError());
        return 1;
    }
    printf("Window is now invisible to capture. Try taking a screenshot.\n");
    Sleep(60000);
    return 0;
}


Compile it, run it, open Snipping Tool — the console won't be in the screenshot.


From C# / WPF the same thing via P/Invoke:

[DllImport("user32.dll")]
static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);

const uint WDA_EXCLUDEFROMCAPTURE = 0x11;
var hwnd = new WindowInteropHelper(this).Handle;
SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);


In Electron there's a high-level wrapper:

mainWindow.setContentProtection(true);


Under the hood it calls SetWindowDisplayAffinity with WDA_EXCLUDEFROMCAPTURE on Windows and changes sharingType on macOS. In Tauri the same via with_content_protection(true).

The underlying mechanics: DWM maintains separate composition pipelines for each capture client. When a window is marked WDA_EXCLUDEFROMCAPTURE, its layer goes into the pipeline for the physical monitor and not into the pipelines for capture clients. The separation happens at the composition level — not as a filter on top of the finished frame — which is why bypassing it at the pixel-reading level is impossible.

What applications actually see with the flag set


Tested on Windows 11 23H2 across all capture paths with a window that has WDA_EXCLUDEFROMCAPTURE set:

  • GDI BitBlt from desktop DC — window absent.

  • Desktop Duplication (DXGI) — window absent. In Windows 11 24H2 there's a quirk where AcquireNextFrame now wakes on updates from the hidden window too, but the content still doesn't come through.

  • Windows.Graphics.Capture (WGC) — window absent.

  • PrintWindow — the flag is ignored, the window ends up in the bitmap. PrintWindow bypasses DWM and asks the window to render directly into the specified DC. Protection doesn't work. Zoom and other screen-sharing tools don't use PrintWindow, but it's worth knowing during audits.

Bottom line for typical scenarios: Zoom Screen Share, Google Meet (via Chrome), Microsoft Teams, Discord, Slack Huddle, OBS, Snipping Tool, Lightshot, Greenshot — the window with WDA_EXCLUDEFROMCAPTURE is absent in all of them. Not a black rectangle. Absence of the object.

One limitation: works on Windows 10 20H1 and newer. On Windows 10 1809 LTSC and earlier the flag is not supported — SetWindowDisplayAffinity with WDA_EXCLUDEFROMCAPTURE returns FALSE and GetLastError() == ERROR_INVALID_PARAMETER. Fall back to WDA_MONITOR, and then someone else's screen share shows a black rectangle instead of true invisibility.

Separate topic: the WCA_EXCLUDED_FROM_DDA attribute via the undocumented SetWindowCompositionAttribute. Works since Windows 10 1709, excludes the window only from Desktop Duplication while leaving it visible in other APIs. In practice unnecessary, since WDA_EXCLUDEFROMCAPTURE handles the same task more completely — but you'll sometimes run into it in legacy codebases.

macOS before 15: sharingType as the standard solution


On macOS the historical standard was the sharingType property of NSWindow:

window.sharingType = .none


Values:

  • .readWrite — window available for reading and modification from other processes (rarely used)

  • .readOnly — default, window is capturable

  • .none — window excluded from capture

Under the hood it worked through WindowServer. macOS has no DWM — composition is done by WindowServer itself via Core Graphics. The NSWindowSharingNone flag told WindowServer: "don't hand this window's content to capture APIs."

Through macOS 14 Sonoma this worked against everything:

  • CGWindowListCreateImage — legacy capture API, since macOS 10.5. Used in old screenshot utilities and some apps that haven't been updated.

  • ScreenCaptureKit — new framework, introduced in macOS 12.3 in 2022. All modern apps are migrating to it since Apple marks the other APIs as deprecated.

In Electron the same setContentProtection(true) line — under the hood it set sharingType = .none.

macOS 15 Sequoia: sharingType no longer works with ScreenCaptureKit


macOS 15 shipped in fall 2024. Apple changed how WindowServer handles composition: it now first assembles all visible windows into a unified framebuffer, and ScreenCaptureKit captures that framebuffer. The sharingType = .none flag stopped excluding the window from SCStream.

There's an Apple Developer Forums thread where a developer asks directly: does kCGWindowSharingStateSharingNone work against ScreenCaptureKit on 15.4+? The response from an official Apple representative: "At this time there are no public APIs for preventing screen capture."

Important nuance: this didn't break everything. sharingType = .none still works against legacy APIs (CGWindowListCreateImage and whatever is built on top of it). Just not against ScreenCaptureKit.

This matters in practice because different apps are migrating to ScreenCaptureKit at different speeds:

Application

What it uses on macOS 15

Sees window with sharingType = .none

Zoom (current)

ScreenCaptureKit

Yes

Microsoft Teams (new)

ScreenCaptureKit

Yes

QuickTime Screen Recording

ScreenCaptureKit

Yes

System Screenshot (Cmd+Shift+3/4/5)

ScreenCaptureKit

Yes

Google Chrome (getDisplayMedia)

Legacy CoreGraphics

No

Google Meet in Chrome

Via Chrome → CoreGraphics

No

OBS (old versions)

CoreGraphics

No

OBS (new versions with SCK)

ScreenCaptureKit

Yes

So on macOS 15 a window with sharingType = .none is still hidden from a Google Meet call in Chrome, but is already visible when sharing through desktop Zoom. This isn't an implementation bug — it's an architectural decision by Apple, and there's no public workaround.

This broke in both Electron and Tauri — both teams documented it as an upstream blocker that can't be resolved without private APIs. Private workarounds exist, but any app that uses them won't pass App Store review and risks breaking with the next macOS update.

For anyone building products with the requirement "not appear in screen sharing on macOS" — this regression is serious. Either cap support at macOS 14 and earlier, or accept that on macOS 15+ protection from ScreenCaptureKit is impossible and switch to behavioral approaches: automatically close the window when sharing starts, detect SCStream via system signals.

Linux: you can't hide in X11, Wayland is a different story


On Linux the answer depends on the display server.


X11. Any client with display access can call XGetImage or XCompositeNameWindowPixmap and get pixels from any window, including other apps'. The X server doesn't distinguish between "own" and "foreign" windows for a client. This is the protocol's architecture, not a bug. Hiding a window from capture in X11 in the general case isn't possible — compositor extensions for specific window managers (Picom, KWin, Mutter) may offer something, but there's no cross-WM solution.

Wayland. Different story. In Wayland a client fundamentally cannot read other surfaces. Screen capture is only possible via xdg-desktop-portal + PipeWire:

  1. The app calls org.freedesktop.portal.ScreenCast.

  2. The portal shows the user a system dialog for selecting the source (monitor, window, application).

  3. The compositor passes a PipeWire stream only for the selected source.

Marking a window as "don't include in capture" is not standardized in the Wayland protocol. At the compositor level it could theoretically be implemented via private extensions, but there's no public API. In practice: if the user chose to capture the entire monitor through the portal, every window on it ends up in the stream.

One approach that works on Wayland: design the UX so the user always selects a specific window in the portal (the browser window, or a specific call app) rather than the whole screen. Then exclusion isn't needed — the assistant window was never part of the selection.


Browsers and getDisplayMedia


Google Meet and all other web calls go through navigator.mediaDevices.getDisplayMedia(). This is a WebRTC method, and implementation depends on the browser and OS:

  • Chromium on Windows — newer versions use Windows.Graphics.Capture. WDA_EXCLUDEFROMCAPTURE works.

  • Chromium on macOS — as of early 2026 still uses legacy CoreGraphics. sharingType = .none continues to work, even on macOS 15. Chrome's migration to ScreenCaptureKit is being discussed in the Chromium tracker, but hasn't happened yet.

  • Firefox on WindowsWindows.Graphics.Capture + fallback to DXGI. Works.

  • Firefox on macOS — mixed implementation, generally the legacy path, sharingType works.

  • Safari — only ScreenCaptureKit, so on macOS 15+ it will see the protected window.

Practical takeaway: on macOS 15 calls via Chrome and Firefox (i.e., Google Meet) still don't see the protected window, while desktop Zoom and Safari do. Fragile equilibrium — Chrome will migrate to ScreenCaptureKit at some point.

What bypasses protection regardless


These flags are not absolute protection. Here's what gets around them.


Hardware capture. An HDMI capture card (Elgato, AVerMedia) receives the signal from the monitor after the GPU has already sent it to the port. DWM and WindowServer have no involvement at that point. Protection doesn't work.

Phone camera. Obvious, but people forget. No API flags help against physical recording.

Kernel-level drivers. Signed kernel-mode drivers can read the GPU framebuffer directly. This is how some anti-cheats and corporate monitoring systems work. Rare on personal machines, possible in corporate environments with MDM.


Accessibility APIs. On Windows there's UI Automation, on macOS — Accessibility. This isn't pixel capture — it's structured access to the interface. Affinity and sharingType flags don't affect UIA. If an app exposes coherent UIA providers, it's still accessible through this channel. Usually you'd also want to make the window invisible to accessibility as well.


GPU debuggers. RenderDoc, NVIDIA NSight, PIX can intercept a frame before composition. In practice a manual development tool, not something applied automatically.


For ordinary scenarios — screen sharing in a call, recording in OBS, a screenshot in a system utility — the flags are enough. For protection against someone actively trying to record: no.


Where this was used and what came of it


We're building JobPath — a desktop assistant app for online interviews, with a feature where the hints window should not appear in the interviewer's screen share. We worked through it in the same order this article is written: Windows first as the primary platform, then macOS, then thought about Linux and set it aside.


The final stack: on Windows — SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) with a fallback to WDA_MONITOR for machines older than 20H1. On macOS 14 and below — NSWindow.sharingType = .none. On macOS 15+ we had to accept that against current Zoom there's no protection, and added a behavioral fallback: we detect the system capture indicator in the menu bar and automatically close the assistant window when it appears. Not pretty, but it's what's possible within public APIs right now.

What we didn't realize at first: WDA_EXCLUDEFROMCAPTURE needs to be set before the window becomes visible. If you set it on an already-shown window, DWM will for some time keep delivering its content to already-open capture sessions until they reconfigure on the next frame. The simplest approach: set the flag right after CreateWindowEx, before ShowWindow. Same on macOS: sharingType = .none before makeKeyAndOrderFront.

One more thing: on Windows in an RDP session (when the user is on a remote machine rather than a physical one) the flag behaves unpredictably, because RDP forms its own capture path through a separate component. For JobPath this doesn't matter — nobody does job interviews over RDP — but for enterprise scenarios it's worth knowing.

In summary


Excluding a window from screen capture is not a system bypass and not a gray-area trick. It's a public API built for DRM, actively used by password managers, banking clients, and video streaming services. On Windows it's one line and works reliably. On macOS 14 and below — same story. On macOS 15 Apple quietly broke it for ScreenCaptureKit with no public fix available. In browsers it still works on macOS (Chrome uses legacy CoreGraphics), but that's a temporary situation.

If you're building an app with the requirement "don't show up in screen sharing" — plan from the start that macOS 15+ will need a behavioral fallback. setContentProtection(true) in Electron/Tauri on that macOS version doesn't do what it promises.

15 views

Add a comment

Replies

Be the first to comment