≡articles //snippets ./categories $uses ~about /search

macOS 15 辅助功能授权添加了却不生效?一次排查记录

辅助功能面板里显示已授权,程序却收不到按键。macOS 15 上给命令行程序授权有坑。这篇记下试过的四条路、最后能用的两种办法,以及顺带发现的一个 bug。

做 talk2clip(macOS 语音输入,按住右⌘说话)时,最费时间的功能不是语音识别——是让键盘监听拿到权限。前后折腾了两天,其中一半时间花在验证“系统到底骗了我什么”。

talk2clip 演示:按住右⌘说话,识别文字自动粘贴到光标处

先交代症状,你对照一下是不是同款:

全局快捷键监听(pynput)一启动就打印:

This process is not trusted! Input event monitoring will not be possible
until it is added to accessibility clients.

去系统设置 → 隐私与安全性 → 辅助功能,把程序加上,开关拨到蓝色,重启进程。警告还在。代码里 AXIsProcessTrusted() 返回 False,面板里却明明白白写着“已允许”:

macOS 15 辅助功能面板里 python3 显示已授权,监听仍收不到按键

两种权限先分清:输入监控 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 &amp;&amp; ./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

顺带一句:权限相关的改动会静默影响进程行为(谁启动的、几个实例、日志写哪),改完记得全链路回归一遍。我这次先跑通键盘监听,回头才发现自启早就不对了。

别人按你步骤做,最容易踩的两个点

  1. 在“输入监控”添加程序 vs 在“辅助功能”添加程序,监听失败先查输入监控,粘贴失败先查辅助功能,别一门心思在辅助功能里找
  2. 面板开关开了 ≠ 权限真生效,用上面那行 AXIsProcessTrusted() 自查,True 才是真通

写监听这块的同时,还解决了国内网络下的 whisper 模型下载(huggingface 429、hf-mirror 抽风、modelscope 各形态报错,最后从 Azure CDN 下官方 .pt 再本地转换),下一篇:《国内下载 AI 模型太难了?一条实测可行的完整方案(HuggingFace 篇)》。

项目代码在 github.com/Charlielyo/talk2clip(MIT),做类似工具的欢迎参考。

© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。

独立开发者,在 WordPress、Nginx 和各种 API 之间切换。不写没用的东西。

相关文章

用 PyObjC 画一个 40fps 的胶囊波形动画:NSTimer 里的三个坑5 min做减法:我把 macOS 语音助手从功能堆叠砍到只剩核心4 min把 1000 行代码变成开源项目:git 历史清理、单实例保护和“多少行值得开源”5 min
$ echo "less bullshit, more debugging" | send-to-inbox
新文章直接发到邮箱,一个月 2-4 封。
订阅