从终端控制序列识别 Claude 审批等待
文章拆解 osFoundry 如何识别 Claude Code 的等待审批状态:终端通过光标移动绘制界面,原始字节可能既无词间空格,也无换行,直接匹配文本容易失效。作者使用真实捕获数据验证规则,并通过模拟代理比较文案匹配、静默计时和进程状态三种方法。
AI 深度解读
Claude Code 的终端还在刷新,不代表它正在干活;终端安静了,也不代表它在等你批准。osFoundry 要解决的,是在多个终端会话中找出「正在等待用户授权」的那个,并避免把普通输入误送进权限确认框。
作者给出的结论很具体:先还原终端当前屏幕,再匹配提示规则,同时结合 hooks、协议事件和前台进程信息。文章用真实终端捕获验证了屏幕识别,但动态对比使用的是模拟代理。理解这个边界,才能判断哪些结果可以借鉴。
为什么删掉颜色代码,还是读不出授权提示
终端输出不是排好版的文本,而是一串绘制指令。Claude Code 2.1.303 的真实捕获中,授权提示长这样,下面只为阅读添加了换行:
\r\x1b[1C\x1b[1B\x1b[39mDo\x1b[5Gyou\x1b[9Gwant\x1b[14Gto\x1b[17Gproceed?
\r\x1b[1C\x1b[1B\x1b[38;2;177;185;249m❯\x1b[4G\x1b[38;2;153;153;153m1. \x1b[38;2;177;185;249mYes
\r\x1b[3C\x1b[1B\x1b[38;2;153;153;153m2. \x1b[39mYes,\x1b[12Gand\x1b[16Galways\x1b[23Gallow ...
要看的是 \x1b[5G:它把光标移到第 5 列,Do 和 you 之间并没有空格。直接删除转义序列,得到的是 Doyouwanttoproceed?。换行也类似,程序用 \r 和光标下移指令绘制下一行,并不输出 \n。
作者检查的 11 份 Claude Code 捕获,每份 2,099~7,672 字节,换行字节全部为 0。把整份输出当成一行,再保留最后一个 \r 后面的内容,只会留下末尾碎片。
对同一批样本,截取最后 8 KB、清理转义序列的方法只判对 5/11,而且全是默认返回的 idle;把字节送入 32×120 的屏幕模拟器,再读取屏幕文本,则判对 11/11。
授权提示属于当前屏幕状态,不能靠历史输出的末尾片段可靠判断。
osFoundry 为每个会话维护 vt100::Parser,通过 screen_text() 获取当前屏幕。保留的 200 行历史记录不参与判断,避免已经回答、滚出屏幕的提示继续触发告警。作者的简化模拟器约 100 行,只处理字符、\r \n \b、光标移动 A B C D E F G H f d 和清除指令 J K,便足以通过这批样本;它对全角字符宽度的处理仍有限制。
为什么规则顺序,比多写几个正则更重要
恢复屏幕后,还得区分三种状态:正在工作 working、本轮结束等待新任务 idle、需要用户确认 blocked。后两者都可能在等输入,但处理方式不同。
Claude Code 的 TOML 规则直接表达了判断顺序:
[[rules]]
state = "blocked"
any = [
'Do you want to (proceed|continue|make this edit|create)',
'❯\s*1\.\s*Yes',
'Esc to cancel · Tab to amend',
'Enter to select · ↑/↓ to navigate',
'Exit plan mode\?',
]
[[rules]]
state = "working"
any = ['esc to interrupt']
[[rules]]
state = "idle"
any = ['\? for shortcuts', '(?:⏸|⏵⏵) [a-z ]+ mode on']
这里要看的是 blocked 排在最前面。CompiledManifest::evaluate 使用首次匹配结果,而授权框出现时,底部仍可能残留 esc to interrupt。如果先匹配 working,授权请求就会被遮住。所有规则都没命中时,返回 idle,不会猜成 blocked。
规则也刻意避开宽泛问句。作者提到,Would you like to 是 Codex 的提示用语,而 Claude 普通回答也常出现 Would you like me to…。识别问号很容易,识别需要接管的交互才是目标。
osFoundry 每 700 ms 扫描一次全部 pane,由 SWEEP_INTERVAL 控制。转圈动画每秒重绘多次,没有必要每次输出都重新匹配。规则作为数据文件更新,不必跟随应用发版,但规则文件不能改变要执行的程序。
安静计时和进程状态,为什么都容易猜错
作者没有在这台 Mac 上运行真实 Claude Code,因为机器没有 claude 二进制。动态实验使用 fake_agent.py,模拟 10 Hz 转圈动画、每帧 60 字节输出、持续 0.5~6 秒的静默工具调用,以及授权框绘制和清除。
PTY 监督程序每 50 ms 记录判断,另有独立状态文件供评分。用户机器人在提示出现后 3~8 秒回答,在一轮结束后 3~10 秒提交新任务。固定 seed 7 的一次运行包含 30 个任务,历时 10.6 分钟,出现 25 次权限提示,累计等待 144 秒。
静默计时器没有找到合适阈值:低于 5 秒,容易把静默工具调用当成授权等待;不超过 3 秒,30 次正常结束都会触发;达到 5 秒后,又开始漏掉已经及时回答的提示。8 秒阈值漏掉全部 25 次,直接原因是机器人没有等待超过 8 秒,不能把它当成真实用户的漏检率。
检查问号也会误报普通回答。截取最后 8 KB 在模拟实验中能识别提示,是因为替身把 ❯ 1. Yes 输出成了真实空格,但回答后平均还会误判 77 秒。清屏指令只改变屏幕,旧字节仍留在缓冲区;即使动画每秒输出 600 字节,也需要时间挤掉它们。
700 ms 扫描的结果符合预期:延迟中位数 0.26 秒,最大 0.70 秒,回答后的状态残留也最多约一个周期。
进程状态同样缺少区分度。作者每秒两次执行 ps -o stat=,time=,得到:
| 实际状态 | 样本数 | S 状态 | R 状态 | 单核 CPU 占用 |
|---|---|---|---|---|
| working | 603 | 99.5% | 0.5% | 0.04% |
| idle | 357 | 100.0% | 0.0% | 0.02% |
| blocked | 280 | 100.0% | 0.0% | 0.02% |
等终端输入和等下一帧动画都可能显示为 S,此次测量的 wchan 也都是 -。作者进一步推测,Node CLI 经常等待事件循环,仅看状态字母更难判断业务状态;这部分没有真实 Claude Code 测量支撑。
进程信息仍有用:MasterPty::process_group_leader 对应的 tcgetpgrp 能确定谁持有终端前台,以及代理退出后 shell 是否重新接管。
能直接收到事件,为什么还要看屏幕
osFoundry 自己启动 Claude Code 时,会通过 --settings 为本次运行追加 hooks,原有用户 hooks 继续执行。
UserPromptSubmit 和匹配器为 * 的 PreToolUse 调用 osf-pane report working;Notification 调用 osf-pane notify;Stop 调用 osf-pane report done。命令均附带 >/dev/null 2>&1 || true。
Notification 既可能来自授权提示,也可能来自持续 1 分钟的输入等待,因此还要读取 payload:属于 permission 才记为 blocked,其他情况记为 idle。如果通过 claude-agent-acp 适配器使用 ACP,session/request_permission 请求本身就是明确的权限事件。
但 signals::combine 仍让屏幕识别出的 blocked 优先,其后依次是最新代理报告、shell 回到前台、其他屏幕规则。原因是报告可能过期,hook 也可能静默失败。
通知按会话和状态设置 30 秒冷却,减少重绘造成的反复提醒。真正发送输入前,程序还会重新执行 classify_session:blocked pane 只接受经过用户确认的 runtime_answer,普通 runtime_send 会被拦住。
我的判断
这篇最有价值的,是把终端绘制状态、进程状态和业务状态分开处理。屏幕模拟解决文字在哪里、是否已被清除;明确事件补充业务含义;前台进程组识别会话归属和退出。几种信息各有用途。
证据也有边界。11/11 只能证明规则覆盖了这 11 份捕获;动态数据来自一次固定种子的模拟运行,不能外推成真实 Claude Code 的准确率或线上稳定性。原文没有给出跨版本、不同终端尺寸和其他语言界面的对照结果。
屏幕 blocked 永远优先也有前提:规则必须足够专一。文章没有证明普通回答引用授权文案时一定不会误报。相比单靠 hooks,这套方案能覆盖手动启动和事件缺失,但维护屏幕规则的成本依然存在。
能拿来做什么
- 给 Node CLI 或 AI 编程工具做状态面板时,先区分「等待新任务」与「等待授权」,避免把所有安静会话统一标成待处理。
- 消费 PTY 输出时,使用终端解析器维护当前屏幕,并保存真实字节捕获做回归样本,覆盖无换行、光标定位和提示清除。
- 能控制代理启动方式时,接入 hooks 或协议权限事件,同时检查前台进程归属,补上手动启动和退出后的状态变化。
- 做自动输入或多会话调度时,展示状态可以按数百毫秒刷新,但发送前应重新分类,并为授权回答保留单独入口。