Vous ne pouvez pas cacher une fenêtre à Zoom sur macOS 15
Sur macOS moderne, il est impossible de cacher de manière fiable une fenêtre aux captures d'écran — ni depuis une autre application, ni même depuis votre propre application. C'est l'histoire technique de la suppression de CGSSetWindowCaptureExcluded, de l'ignorance silencieuse de SLSSetWindowSharingState entre processus, et de la raison pour laquelle NSWindow.sharingType = .none est une suggestion douce pour Zoom et QuickTime, et non une garantie rigoureuse.
A short engineering post-mortem on building a “private window” app that doesn’t quite work.
The idea
Like a lot of people, I work with a Claude desktop window open during meetings. And like a lot of people, I sometimes share my screen on Zoom and would prefer that window not be visible to everyone in the call. Not because of anything sensitive — just because it’s private working state.
I knew apps like 1Password and Hand Mirror can hide their own windows from screen recording. I figured I could build a small menu-bar utility that lets me pick any app — Claude, Notes, whatever — and toggle its windows invisible to capture while keeping them fully visible and interactive on my own screen.
A weekend project. Two hours, tops.
It took longer. And the punchline is: on modern macOS, you cannot do this. Not for another app, and — as it turns out — not reliably for your own.
This is the engineering story of why.
Attempt 1: hide another app’s window
The classic way to make a macOS window invisible to screen capture is NSWindow.sharingType = .none. But this is a per-window flag that only the owning process can set on its own windows. AppKit doesn’t let you reach into another app’s window list.
The well-known workaround, used for years by tools like Hand Mirror and various screen-blanking utilities, is the private SkyLight function:
OSStatus CGSSetWindowCaptureExcluded(CGSConnectionID cid, CGWindowID wid, bool excluded);
You enumerate windows via CGWindowListCopyWindowInfo, filter to the target app’s PID, and call this on each window ID. Because the call goes through the WindowServer (not the owning process), it can except any window from capture.
I wired this up via dlsym sur /System/Library/PrivateFrameworks/SkyLight.framework/..., built it, ran it on macOS 15.3.1, and got:
[InvisibleApp] symbol not found: CGSSetWindowCaptureExcluded
[InvisibleApp] symbol not found: SLSSetWindowCaptureExcluded
The function is gone. Apple removed it. I poked around for a renamed version (SLSSetWindowExcludedFromCapture, CGSSetWindowSharingState, all the obvious variants). Most don’t exist either, but I found two that do:
SLSSetWindowSharingState(CGSConnectionID, CGWindowID, int sharingState)— the underlying call thatNSWindow.sharingType = .noneuses internally. Sharing state0estNSWindowSharingNone.SLSGetWindowOwner— gives you the connection ID of the window’s owning process.
So I rebuilt the bridge to call SLSSetWindowSharingState on the target window, trying both my main connection and the window’s owning connection.
Build, run, toggle on, log says it returned noErr, take a screenshot — Claude is still in the screenshot. Try Zoom — Claude is still in the share.
SLSSetWindowSharingState succeeds when called cross-process but the WindowServer silently ignores it. Only the owning process can change its own window’s sharing state on macOS 15.
I also confirmed the obvious escape hatch is closed: DYLD_INSERT_LIBRARIES injection into Claude’s process is blocked because Claude (like most modern apps) ships with hardened runtime enabled and without the disable-library-validation entitlement.
So that’s settled: no third-party app can make another app’s window invisible to capture on macOS 15. Verdict aligns with what Apple says they want: window-level capture state is the owning app’s prerogative, full stop.
Attempt 2: hide our own window
Plan B: pivot the product. Instead of a tool that hides Claude, build a tiny chat window of my own — same job, different shape. My window, my sharingType = .none. This is the well-trodden mechanism every capture-hiding utility uses, and it’s a one-liner:
window.sharingType = .none
Wire up a streaming chat to the Anthropic Messages API (or to the local claude CLI for subscription auth), set sharingType = .none on the chat window, ship it. This is unambiguous, supported, public API.
Built it. Logged the actual sharing-type value at runtime to make sure macOS accepted it:
[InvisibleApp] window 151526 sharingType=0
Took a screenshot — the chat window was correctly missing. ✅
Recorded the screen with QuickTime — the chat window appeared in the recording. ❌
Shared the screen on Zoom — chat window visible to the other side. ❌
This was unexpected. The whole indiquer of NSWindowSharingNone is to get it out of these flows. So I built a self-test inside the app that performs a screen capture using ScreenCaptureKit (the modern, public capture API), the same framework QuickTime uses under the hood. I tried both single-shot capture (SCScreenshotManager.captureImage) and continuous capture (SCStream) — both produced a PNG with the chat window correctly excluded.
So:
| Capture method | Hides our window? |
|---|---|
| Cmd-Shift-3/4/5 (system Screenshot) | ✅ |
| Our app’s SCK single-shot capture | ✅ |
| Our app’s SCK continuous SCStream | ✅ |
| QuickTime “New Screen Recording” | ❌ |
| Zoom “Share Screen → Desktop” | ❌ |
Same OS, same machine, same window, same sharingType = .none. The only thing that varies is who’s doing the capturing.
Privileged capture paths
This is the part most engineers don’t realize: on macOS 15, screen capture isn’t one API; it’s at least two — a public one, and a private one with extra entitlements.
I dumped QuickTime Player’s entitlements:
codesign -d --entitlements - /System/Applications/QuickTime\ Player.app
com.apple.private.screencapturekit.noprompt = true
com.apple.private.tcc.allow = [
kTCCServiceMicrophone,
kTCCServiceCamera,
kTCCServiceScreenCapture,
]
Those com.apple.private.* keys can only be granted by Apple to its own first-party binaries. You and I can’t request them. Notarization would reject them. The App Store certainly would.
And what they grant, in addition to skipping the capture-permission prompt, is the ability to capture a display image before the WindowServer applies sharing-type filtering. The window is in the framebuffer that comes out — Apple’s tools just see all of it.
Zoom uses an analogous privileged path. It might be a kernel extension, a system extension, or a private API obtained by being a long-blessed conferencing app — I haven’t dug far enough to say which. But the behavior is the same: the standard public exclusion mechanism doesn’t apply to it.
What this means
If you’re a third-party developer and your goal is “this window must never appear in a screen recording on any Mac,” you cannot guarantee that on macOS 15. Your window is hidden from every capture path you can write yourself. It’s hidden from Google Meet (browser → getDisplayMedia → SCK), Microsoft Teams (SCK), OBS (SCK), every conformant third-party capture tool. But Apple’s own QuickTime and Apple’s blessed conferencing partners can still see it.
The 1Password / Hand Mirror “screen capture protection” feature has the same limitation. People just don’t usually screen-record their 1Password vault with QuickTime, so the leak goes unnoticed.
The honest summary I’d give Apple on this: publish a public entitlement equivalent to com.apple.private.screencapturekit.noprompt, or commit that NSWindow.sharingType = .none is a hard guarantee against all capture paths. As of macOS 15.3.1 it’s neither — it’s a hard guarantee against most paths and a soft suggestion that privileged tools can bypass. The status quo creates a security posture where users believe a window is invisible during capture when it isn’t, which is worse than no protection at all.
The workarounds, ordered by how much they hurt
For my use case — keeping a chat window hidden during a Zoom share — these all work and none are great:
- Share a single window in Zoom, not the desktop. The Share Screen dialog has a “Window” tab. Zoom captures only that window’s pixels and nothing else, so my chat — and everything else on screen — is automatically absent. This is the most reliable fix and requires zero code. Downside: viewers don’t see screen-wide context.
- Use Meet or Teams instead of Zoom. Both honor
sharingType = .none. This is great if you control the meeting tool, terrible if you don’t. - Put the chat on a separate Space. Mission Control gives you multiple desktops. Share Space 1, keep the chat on Space 2. Swipe to interact. Slow, breaks flow, but it works.
- Use a second device. A phone or iPad next to your laptop is undefeatable by any capture tool, by definition.
I went with option 1 plus the chat app I already built. The chat is invisible to ~95% of capture paths, and for the Zoom-share-desktop case I just don’t use Zoom-share-desktop.
What I’d want next
- A public entitlement that lets a notarized app declare “this window of mine is excluded from all capture, including by privileged callers.” Today, that’s not possible.
- Honesty in
NSWindow.sharingTypedocumentation. The docs make it sound like a hard guarantee. It’s not — at minimum the docs should mention that privileged Apple tools and apps with private entitlements can capture regardless. Today they don’t. - A way to inspect which apps on a system have privileged capture access — analogous to “Files and Folders” in Privacy & Security. Today there’s no surface for users to see who can bypass their privacy.
Until any of that lands, the practical advice is: assume any window on your screen is capturable by the OS and by major conferencing apps, regardless of what sharingType you set. The exclusion APIs are real — they work against most of the world — but they’re not a security boundary you can trust against the parts of the world that matter most.
Code
The full source for the menu-bar chat app — including the SkyLight bridge, the stream-based SCK self-test, and the conclusion-confirming experiments — is available here (this directory). It’s about 700 lines of Swift, single-binary, no dependencies. Useful as a reference for anyone reaching for NSWindow.sharingType = .none and wondering what they’re actually getting.
The TL;DR for that audience:
NSWindow.sharingType = .noneis the right API. It’s just not as strong as you think it is. Test against the specific tool your users are sharing through, not against screenshots and your own SCK code, before you promise anything.
Installez nos extensions
Ajoutez des outils IO à votre navigateur préféré pour un accès instantané et une recherche plus rapide
恵 Le Tableau de Bord Est Arrivé !
Tableau de Bord est une façon amusante de suivre vos jeux, toutes les données sont stockées dans votre navigateur. D'autres fonctionnalités arrivent bientôt !
Outils essentiels
Tout voir Nouveautés
Tout voirMise à jour: Notre dernier outil a été ajouté le 26 avr. 2026
