在 macOS 15 上无法在 Zoom 中隐藏窗口
在现代macOS系统中,无法可靠地将窗口从屏幕截图中隐藏——无论是从其他应用程序,还是从自己的应用程序中都无法实现。这是CGSSetWindowCaptureExcluded被移除、SLSSetWindowSharingState在跨进程环境中被静默忽略,以及NSWindow.sharingType = .none 对Zoom和QuickTime而言只是一个软性建议而非强制保证的工程背景故事。
一篇简短的工程复盘,关于开发一个“私人窗口”应用,但最终未能成功。
想法
和很多人一样,我在会议期间会保持Claude桌面窗口打开。和很多人一样,我有时会在Zoom上共享屏幕,而希望那个窗口不会被会议中的所有人看到。并非因为涉及敏感信息——只是因为那是一个私人的工作状态。
我知道像1Password和Hand Mirror这样的应用可以隐藏自己的窗口以避免被屏幕录制。我设想可以开发一个简单的菜单栏工具,让我选择 任意 应用——比如Claude、Notes之类的——并在屏幕录制时将其窗口隐藏,同时保持它在我的屏幕上完全可见且可交互。
一个周末的项目,最多两小时。
实际花费的时间更长。而结论是: 在现代macOS上,你无法实现这一点。不仅无法对其他应用实现,而且——事实证明——甚至无法可靠地对自身应用实现。
这是背后工程原因的说明。
尝试1:隐藏其他应用的窗口
在macOS上让一个窗口对屏幕录制不可见的经典方法是 NSWindow.sharingType = .none。但这只是一个针对单个窗口的标志,只有 拥有 进程才能设置其自身的窗口。AppKit不允许你访问其他应用的窗口列表。
广为人知的绕过方法,多年来被Hand Mirror和各种屏幕遮蔽工具使用,是私有SkyLight函数:
OSStatus CGSSetWindowCaptureExcluded(CGSConnectionID cid, CGWindowID wid, bool excluded);
你通过 CGWindowListCopyWindowInfo枚举窗口,筛选出目标应用的PID,并对每个窗口ID调用该函数。由于该调用是通过WindowServer(而非拥有进程)进行的,因此可以排除任何窗口被录制。
我通过 dlsym 在 /System/Library/PrivateFrameworks/SkyLight.framework/...实现了这一功能,构建了它,并在macOS 15.3.1上运行,结果是:
[InvisibleApp] symbol not found: CGSSetWindowCaptureExcluded
[InvisibleApp] symbol not found: SLSSetWindowCaptureExcluded
该函数 已不存在。苹果已经移除了它。我查找了重命名版本(SLSSetWindowExcludedFromCapture, CGSSetWindowSharingState,所有显而易见的变体)。大多数都不存在,但我发现了两个可用的:
SLSSetWindowSharingState(CGSConnectionID, CGWindowID, int sharingState)——这是内部使用的底层调用。NSWindow.sharingType = .none——提供窗口拥有进程的连接ID。0是NSWindowSharingNone.SLSGetWindowOwner因此,我重建了桥梁,调用
在目标窗口上,尝试了我的主连接以及窗口的拥有连接。 SLSSetWindowSharingState 构建,运行,切换开启,日志显示返回
,截图——Claude仍然出现在截图中。尝试Zoom——Claude仍然出现在共享画面中。 noErr在跨进程调用时成功,但WindowServer静默忽略该调用。
SLSSetWindowSharingState 在macOS 15上,只有拥有进程才能更改其自身窗口的共享状态。 我也确认了显而易见的逃生通道已被关闭:
对Claude进程的注入被阻止,因为Claude(像大多数现代应用)启用了加固运行时,且没有 DYLD_INSERT_LIBRARIES 权限。 disable-library-validation 因此,结论是:
在macOS 15上,没有第三方应用可以将另一个应用的窗口隐藏于屏幕录制之外。 结论与苹果所表达的立场一致:窗口级别的录制状态是拥有应用的专属权利,绝对不可更改。 尝试2:隐藏我们自己的窗口
备选方案:调整产品方向。不再开发一个隐藏Claude的工具,而是构建一个属于自己的小型聊天窗口——同样的功能,不同的形态。我的窗口,我的
。这是每个录制隐藏工具都采用的经典机制,只需一行代码: sharingType = .none将一个流式聊天连接到Anthropic Messages API(或本地
window.sharingType = .none
CLI用于订阅认证),设置 claude 在聊天窗口上,发布该应用。这是一个明确、受支持且公开的API。 sharingType = .none 构建了它。在运行时记录了实际的共享类型值,以确保macOS接受它:
截图——聊天窗口正确地被隐藏。
[InvisibleApp] window 151526 sharingType=0
截取了屏幕截图——聊天窗口确实缺失了。✅
使用QuickTime录制屏幕——聊天窗口出现在录制中。❌
在Zoom上共享屏幕——对方可以看到聊天窗口。❌
目标是让这些流程中不再出现该窗口。因此,我在应用中构建了一个自检功能,使用ScreenCaptureKit(现代公开的录制API),这是QuickTime底层使用的框架。我尝试了单次截图( 观点 )和连续录制( NSWindowSharingNone )——两种方式都生成了正确的PNG图像,其中聊天窗口被正确排除。SCScreenshotManager.captureImage因此:SCStream录制方式
是否隐藏我们的窗口?
| Cmd-Shift-3/4/5(系统截图) | 我们应用的SCK单次截图 |
|---|---|
| 我们应用的SCK连续SCStream | ✅ |
| QuickTime“新建屏幕录制” | ✅ |
| Zoom“共享屏幕 → 桌面” | ✅ |
| 同一操作系统、同一台机器、同一窗口、同一 | ❌ |
| 。唯一变化的是谁在执行录制。 | ❌ |
特权录制路径 sharingType = .none这是大多数工程师没有意识到的部分:
在macOS 15上,屏幕录制并非一个API,而是至少有两个——一个公开的,一个带有额外权限的私有API。
我dump了QuickTime Player的权限: 这些
键只能由苹果授予其自有的一线应用。你和我无法请求这些权限。未经签名的权限将被拒绝。App Store当然也会拒绝。
codesign -d --entitlements - /System/Applications/QuickTime\ Player.app
com.apple.private.screencapturekit.noprompt = true
com.apple.private.tcc.allow = [
kTCCServiceMicrophone,
kTCCServiceCamera,
kTCCServiceScreenCapture,
]
它们授予的内容,除了跳过录制权限提示外,还包括在WindowServer应用共享类型过滤之前捕获显示图像 com.apple.private.* 。该图像直接来自帧缓冲区,苹果工具看到的是全部内容。
Zoom使用了类似的特权路径。它可能是一个内核扩展、一个系统扩展,或通过长期授权的会议类应用获得的私有API——我尚未深入探究具体是哪一种。但行为是相同的:标准的公开排除机制不适用于它。 这意味着如果你是第三方开发者,目标是“这个窗口在任何Mac上的屏幕录制中都必须完全不可见”,
你无法在macOS 15上保证这一点。
你的窗口在你能够编写的所有录制路径中都被隐藏。它被Google Meet(浏览器 →
→ SCK)、Microsoft Teams(SCK)、OBS(SCK)以及所有符合标准的第三方录制工具所隐藏。但苹果自己的QuickTime和苹果认证的会议合作伙伴仍可以看见它。 1Password / Hand Mirror的“屏幕录制保护”功能也存在同样的限制。人们通常不会用QuickTime录制1Password保险箱,因此泄漏未被察觉。 我诚恳地向苹果提出的总结是: getDisplayMedia 发布一个公开权限,等同于
,或明确声明
是防止 所有 com.apple.private.screencapturekit.noprompt录制路径的硬性保证。 NSWindow.sharingType = .none 截至macOS 15.3.1,它既不是这样的硬性保证,而是对 大多数 路径的保证,以及对特权工具的软性建议,可以绕过该限制。现状导致用户误以为某个窗口在录制期间是不可见的,而实际上并非如此,这比完全没有保护更糟糕。 绕开方案,按对用户体验的伤害程度排序 针对我的使用场景——在Zoom共享期间隐藏聊天窗口——这些方案都有效,但都不理想: 在Zoom中仅共享单个窗口,而非整个桌面。
共享屏幕对话框有一个“窗口”选项卡。Zoom仅捕获该窗口的像素,不捕获其他内容,因此我的聊天窗口以及屏幕上的其他内容自动消失。这是最可靠的解决方案,无需任何代码。缺点是观众无法看到屏幕整体上下文。
改用Meet或Teams代替Zoom。
- 两者都支持 。如果控制会议工具,这是个好方案;如果无法控制,则非常糟糕。
- 将聊天窗口置于另一个空间。 Mission Control提供多个桌面。共享空间1,将聊天窗口留在空间2。通过滑动切换交互。虽然缓慢,但确实有效。
sharingType = .none使用第二台设备。 - 一台手机或iPad放在笔记本旁边,从定义上来说,任何录制工具都无法捕获它。 我选择了方案1加上我已构建的聊天应用。聊天窗口在约95%的录制路径中不可见,对于Zoom共享桌面的情况,我干脆不使用该功能。
- 我接下来希望实现的 一个公开权限
允许经过验证的应用声明“我这个窗口在录制中被排除,包括被特权调用者所捕获”。目前这不可行。
在文档中保持诚实。
- 文档中暗示这是一个硬性保证,但实际上并非如此——至少文档应说明,特权苹果工具和拥有私有权限的应用可以绕过该限制。目前文档中并未提及这一点。 提供一种方式来检查 大多数 系统中哪些应用拥有特权录制访问权限——类似于“文件和文件夹”在隐私与安全设置中的功能。目前用户没有任何界面可以查看谁可以绕过其隐私设置。
- 在上述任何功能落地之前,实际建议是:假设你屏幕上任何窗口都可能被操作系统和主流会议应用捕获,无论你设置了什么。排除API是真实的——它们在大多数情况下有效——但它们并不能作为你可信赖的边界,去抵御那些真正重要的部分。
NSWindow.sharingType❌ 文档似乎暗示这是一个绝对的保证,但实际上并非如此——至少文档应说明,拥有特权的苹果工具和具有私有权限的应用程序可以无视限制进行截取。而目前它们并没有这样做。 - 一种检查方式 系统中哪些应用程序具有特权截取权限——类似于“文件和文件夹”在隐私与安全设置中的功能。目前用户无法查看哪些应用程序可以绕过他们的隐私设置。
在这些功能实现之前,实际建议是:假设你屏幕上的任何窗口都可以被操作系统和主流会议应用程序截取,无论你设置了什么。 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.
