辅助功能面板里显示已授权,程序却收不到按键。macOS 15 上给命令行程序授权有坑。这篇记下试过的四条路、最后能用的两种办法,以及顺带发现的一个 bug。
做 talk2clip(macOS 语音输入,按住右⌘说话)时,最费时间的功能不是语音识别——是让键盘监听拿到权限。前后折腾了两天,其中一半时间花在验证“系统到底骗了我什么”。

先交代症状,你对照一下是不是同款:
全局快捷键监听(pynput)一启动就打印:
This process is not trusted! Input event monitoring will not be possible
until it is added to accessibility clients.
去系统设置 → 隐私与安全性 → 辅助功能,把程序加上,开关拨到蓝色,重启进程。警告还在。代码里 AXIsProcessTrusted() 返回 False,面板里却明明白白写着“已允许”:

两种权限先分清:输入监控 vs 辅助功能
这个 bug 容易误判,一大半原因在这里:
| 你做的事 | 需要的权限 | 面板位置 |
|---|---|---|
| 监听键盘(收按键) | 输入监控(ListenEvent) | 隐私与安全性 → 输入监控 |
| 模拟按键(发 Cmd+V 等) | 辅助功能(PostEvent) | 隐私与安全性 → 辅助功能 |
pynput 的警告文本框里写的是 “accessibility clients”,但它检查的是辅助功能,而监听行为实际受输入监控控制。我们后来在不同面板添了同样的二进制,行为完全不一样,所以两条别搞混。下面按时间线说排查过程。
我试过的四个方法,全部失败
1. 以为是符号链接
程序在 Python venv 里,python3 是个符号链接,指向真实二进制:
~/talk2clip/.venv/bin/python3 -> python
(真实解释器在 venv 之外的运行时目录,路径随工具链而异)
猜面板添加链接路径不认,于是删了条目,用 Cmd+Shift+G 粘贴真实路径换进去。重登、重启进程,还是 False。在符号链接上花了大约 40 分钟,方向不对。
2. 以为是代码签名
真实二进制是 adhoc, linker-signed,无正式签名。网上有说法 macOS 15 对无标识 CLI 二进制的授权有 bug,于是补了一个稳定签名:
codesign --force --sign - --identifier com.voice-hub.python <真实二进制路径>
添加、重启。False。
3. 触发系统弹窗授权
AXIsProcessTrustedWithOptions 会弹“想要控制这台电脑吗”,想着点上允许就完了。结果发现 macOS 的辅助功能弹窗根本没有“允许”按钮,只有“打开系统设置”,点了还是跳面板手动加。这招对 CLI 工具等于白搭。
4. 重置 TCC 数据库
sudo tccutil reset Accessibility
重置 → 重启电脑 → 重新添加。没用。网上这个方案传得很广,但它针对的是“授权状态脏了”;我们的症状是“数据库里可能根本没这行记录”,方向不同,救不了。
转折:同一个二进制,换地方启动,结果完全不同
一度想放弃,后来发现一个决定性的现象,同一个 python,从不同地方启动,AXIsProcessTrusted() 给出不同的答案:
| 怎么启动 | AXIsProcessTrusted() | 键盘监听 |
|---|---|---|
| 终端里直接跑 | True | 能收到按键 |
| launchd(开机自启)拉起来 | False | 收不到 |
| Claude 的沙箱 shell 里跑 | False | 收不到 |
这是全文最重要的一张表。权限跟着“谁来启动你”走,不是光看二进制是谁。
- 终端里跑:进程链
Terminal.app → zsh → python,链上有已授权的终端,一路通过 - launchd 拉起来:
launchd → python,链上只有 launchd,不信任
我们无法严格区分“授权没写进数据库”还是“进程链判定”(两者在 macOS 15 上都有已知报告,input-leap 的 issue 是同款),但结论两说都指向同一个实践:从已授权的宿主启动,而不是给 CLI 二进制单独授权。
那为什么之前手动加辅助功能没生效?大概率是 macOS 15 对 CLI 二进制的授权是“面板添加但 TCC 数据库没写入”,面板中的条目只是 UI 状态。这也能解释为什么 tccutil reset 没用:根本没有记录可重置。
最终两条能跑通的路
方案 A:从终端启动(日常用这个)
双击 .command 启动器,由它调起终端再跑 python。首次使用,把 Terminal(终端)加进两个面板:
- 输入监控,监听按键
- 辅助功能,负责自动粘贴(模拟 Cmd+V)
以后一劳永逸。自查命令(终端里跑一下,返回 True 表示授权链已通):
python3 -c "import ApplicationServices as AS; print(AS.AXIsProcessTrusted())"
方案 B:开机自启,LaunchAgent 包一层隐藏终端
launchd 直接启动 python 会断在权限链。改用 LaunchAgent 调 osascript 开隐藏终端,让终端跑:
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>-e</string>
<string>tell application "Terminal"
do script "cd ~/Desktop/talk2clip && ./run.sh"
set visible of front window to false
end tell</string>
</array>
进程链 launchd → osascript → Terminal → python 完整,权限一路继承,终端窗口自动隐藏。
这个坑还顺带埋了一个隐藏 bug
因为走终端启动,我判断“是否已有实例在跑”时用了:
pgrep -f "talk2clip.py"
问题来了:启动命令是 cd ~/Desktop/talk2clip && ./talk2clip.py,父 shell 的命令行里就含 “talk2clip.py”。pgrep -f 把父 shell 也匹配进去,新进程以为已有实例,直接自杀。结果自启从来没成功过;用户手动双击时又出现两个真实例,识别结果被粘贴两遍。
两个大症状,同一个根因。换成 pidfile 判断进程存活就好了:
def _already_running():
try:
with open(_PID_FILE) as f:
old_pid = int(f.read().strip())
if old_pid != os.getpid() and _pid_alive(old_pid):
return True
except Exception:
pass
return False
顺带一句:权限相关的改动会静默影响进程行为(谁启动的、几个实例、日志写哪),改完记得全链路回归一遍。我这次先跑通键盘监听,回头才发现自启早就不对了。
别人按你步骤做,最容易踩的两个点
- 在“输入监控”添加程序 vs 在“辅助功能”添加程序,监听失败先查输入监控,粘贴失败先查辅助功能,别一门心思在辅助功能里找
- 面板开关开了 ≠ 权限真生效,用上面那行
AXIsProcessTrusted()自查,True 才是真通
写监听这块的同时,还解决了国内网络下的 whisper 模型下载(huggingface 429、hf-mirror 抽风、modelscope 各形态报错,最后从 Azure CDN 下官方 .pt 再本地转换),下一篇:《国内下载 AI 模型太难了?一条实测可行的完整方案(HuggingFace 篇)》。
项目代码在 github.com/Charlielyo/talk2clip(MIT),做类似工具的欢迎参考。
© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。