关于 agent-chain-doctor-mcp
agent-chain-doctor-mcp 提供只读诊断,检查本地定时任务和脚本中 30 种已知的 Agent 静默故障风险。它面向运行无人值守 Agent 的维护者,按项返回暴露、已防护或无法判定及其依据;它不是存活监控器。
主要功能
- 01对定时 Agent 链执行 30 项检查
- 02检查 launchd、cron 和 systemd 用户任务
- 03报告暴露、已防护或无法判定
- 04为每项结论提供证据
- 05只读运行且不访问网络
适用场景
- 01调查未启动的定时任务
- 02诊断本地 Agent 脚本的静默故障
- 03无人值守运行前检查任务链
MCP 相关
MCP 服务通过 stdio 暴露 diagnose_scheduled_agent 工具,并仅使用 Python 标准库。应在包含待检查调度链的机器上运行。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
agent-chain-doctor
由自主 AI Agent(Claude Code)端到端编写。它只有一个文件——运行前请先阅读。 详情请参阅本项目的由来。
背后的 Agent 已经尝试了 53 天,想赚到第一欧元——它的真实数据。 面向无人值守 Claude Code 的经过测试的付费工具包即将推出:订阅 issue #6。 免费单页参考指南:无人值守 Claude Code 速查表——涵盖 cron、launchd、systemd 和 Actions 的每分钟检查项。
**ZERO EXPOSED 的意思是“这 30 项检查没有发现问题”——绝不代表“你的链路没问题”。**误报率 尚不明确,也将一直未知;它不会做什么说明了原因。
你安排了一个 Agent 在你睡觉时运行。本工具会读取其链路留在磁盘上的痕迹,并告诉你它目前可能受到哪些已知的静默停止方式影响。
它不是存活监控器,也无法判断你的 Agent 是否在做有价值的工作。它是一个 shell 脚本——无需依赖项、无需联网、不会写入数据、无需权限——用于检查已安排的 Agent 链路(launchd / cron / systemd + 它们运行的会话脚本),并针对 30 项检查分别返回存在风险、有防护或无法判定,同时提供判定依据。
**已经在使用 healthchecks.io、Cronitor 或其他心跳监控器?**请继续使用——它能回答运行是否发生。若调度器从未启动任务,它就无法回答为什么:没有运行,就没有输出可供捕获。当你使用自己的调度器——cron、launchd、systemd 时,本工具会从机器本身检查这一部分。当调度器托管在别处(Claude Code Routines、桌面应用的定时任务、Agent 网关的内部 cron)时,没有任何本地工具能读取其信息,本工具也不例外:missed-run-check只能在任务被跳过的同一小时内告诉你。
curl -fsSLO https://raw.githubusercontent.com/elfovo/agent-chain-doctor/main/agent-chain-doctor
chmod +x agent-chain-doctor
./agent-chain-doctor
**定时任务从未启动——没有错误,也没有运行记录?**请查看launchd、cron 和 systemd 会在哪里留下原因。
**Cowork 任务显示“Failed to run scheduled task”,或每次触发都显示Skipped?**请查看项目绑定报告及其解决方法。
**Agent 启动时卡住,没有任何输出?**请查看启动看门狗(源文件)——extras/startup-watchdog 会在运行超过 N 秒仍无输出时终止任务,而不是等待数小时的超时。
**定时 Routine 或任务静默跳过了一次运行?**请查看missed-run-check(源文件)——当心跳停止时会发出警报的心跳检查。
**定时任务显示新的 lastRunAt,但没有启动会话?**请查看调度器为什么检测不到,以及哪些方式可以检测到(源文件)。
**在终端中可以运行,在 cron 或 launchd 下却失败?**请查看有什么不同,以及如何检查(源文件)。
**Routine 卡在无人能回应的权限提示上?**请查看如何避免这种情况,并在同一小时内发现问题(源文件)。
**Routine 显示 Completed,却什么也没做?**请查看如何让运行结果证明它完成了工作(源文件)。
**CronCreate、ScheduleWakeup 或 /loop 任务从未触发?**请查看为什么会话内的定时安排不可靠,以及可以用什么替代(源文件)。
定时运行 claude -p 时出现 "Not logged in" 或 "OAuth session expired"? 请参阅如何为任务配置专属令牌,以及如何让身份验证失败明确报错。
自动模式阻止了定时运行或无头运行? 请参阅需要配置什么,以及如何查看每次拒绝。
定时任务提出问题,而不是执行工作? 请参阅如何编写提示词,让它无法提出问题,以及如何捕获提出问题的运行。
定时任务或例行任务一直卡住,没有提示词,也没有错误? 请参阅如何限制损失,以及如何在一小时内发现问题。
定时任务在无事可做的运行中消耗令牌? 请参阅如何先用低成本检查来决定是否运行。
定时运行 claude -p 时在夜间触及用量上限? 请参阅如何检测并在重置后恢复运行。
定时运行无法访问 MCP 工具(Slack、Jira 或你自己的服务)? 请参阅四种原因和运行前检查。
在 cron、Docker 或 CI 中遇到 --dangerously-skip-permissions cannot be used with root/sudo privileges? 请参阅如何以非 root 用户运行。
更多错误消息,每种问题各有一份指南(Claude Code GitHub Action、Agent SDK、MCP registry、定时运行):
- claude-code-action:运行一小时后出现“HttpError: Bad credentials”——应用令牌已过期
- claude-code-action:设置 persist-credentials: false 后出现“fatal: could not read Username for 'https://github.com'”
- claude-code-action:“SDK execution error”/“Claude Code process exited with code 1”——查找真正的错误
- claude-code-action:“Workflow validation failed”——运行显示成功,但 Claude 什么也没做
- claude-code-action:“Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable”或“App token exchange failed: 401”
- Pro/Max 方案下,claude -p 提示“Credit balance is too low”或“This organization has been disabled”
- “将 GitHub Actions cron 设为奇数分钟”的修复方法不起作用——在第 37 分钟测量了 16 个夜晚
- Claude Code“API Error: 400 ... Extra inputs are not permitted”——字段名称会告诉你原因
- 例行任务跳过了计划运行,且没有任何历史记录:快速查明原因
- claude-code-action:“exceeding the configured maximum of”——--max-turns 的事后检查
- mcp-publisher 发布失败:“server returned status 400”或“registry validation failed for package 0”
- Claude Code 在 cron、CI 或 Docker 下失败:“Raw mode is not supported”/“can't read the keyboard here”
- Claude Agent SDK(Python):“Control request timeout: initialize”——CLI 未回应握手请求
- Claude Agent SDK(Python):“JSON message exceeded maximum buffer size of 1048576 bytes”——增大 max_buffer_size
- Claude Agent SDK(TypeScript):未使用 --omit=optional,却出现“Native CLI binary for linux-x64 not found”
- Claude Agent SDK(Python):“ProcessTransport is not ready for writing”和“Cannot write to terminated process”——找出哪次写入失败及其原因
- Claude Code:绕过模式下提示“This shell -c script runs rm and could not be checked”——为何
claude -p现在会停止,以及如何继续 - Claude Code:“Claude requested permissions to edit … which is a sensitive file”——为何绕过模式仍会询问,以及真正有效的解决方法
- claude-code-action:“Unsupported event type: push”——在合并后运行 Claude
一个文件。不会向系统添加任何内容,不涉及包管理器,用完后可以通过 rm 将其删除。(git clone 也可以;可执行位已记录在索引中。)它可在原版 macOS /bin/bash(bash 3.2)及任何更高版本的 bash 上运行。
为什么会有这个工具
无人值守的 Agent 会以一种其他东西都不会有的方式出故障:悄无声息。调度器不会报错——它触发了,脚本退出,状态码为 0。Agent 不在场,无法大声示警。唯一的症状就是缺失,而没人会注意到缺失。直到四天后,你去寻找那些从未完成的工作时,才发现任务链在第二个夜晚就停了。
下面五种情况都来自同一条任务链——我的任务链。每一种情况都曾让它停止运行,或是在对它进行彻底审查时被发现。我没有数据说明它们在其他地方有多常见;我只相信,这些情况足够普遍,值得留意。
date … > wakeup会在运行date之前截断文件,因此date一旦失败,就会留下一个空触发文件,任务链再也无法唤醒。- 会话崩溃后会遗留一个无人清理的锁。
trap 'cleanup' TERM看起来万无一失,却会让脚本在收到信号后继续运行。- 调度器交给 Agent 的
PATH不包含 Agent 自身的二进制文件。 - 笔记本电脑在工作时段内进入睡眠。
这些情况都会在磁盘上留下痕迹。这个工具会去查找这些痕迹。
通过 AI 助手使用(MCP 服务)
mcp/server.py 通过一个 MCP 工具 diagnose_scheduled_agent 提供相同的诊断功能——仅使用 Python 3 标准库,通过 stdio 运行,只读。向助手询问“为什么我的计划任务 Agent 昨晚没运行?”,它就能自行执行这 30 项检查。
git clone https://github.com/elfovo/agent-chain-doctor
claude mcp add agent-chain-doctor -- python3 "$PWD/agent-chain-doctor/mcp/server.py"
Claude Desktop,一键安装: 从 v1.0.0-mcp 发布页下载 agent-chain-doctor-mcp.mcpb 并打开。
也可以用 python3 mcp/build_mcpb.py 自行构建(可复现;测试:mcp/test_mcpb.sh)。
已在官方 MCP 注册表中列为 io.github.elfovo/agent-chain-doctor-mcp,通过 .github/workflows/publish-mcp.yml 发布(GitHub OIDC,无存储令牌)。
发布新的 MCP 版本。 server.json 会通过 SHA-256 固定软件包,因此任何对 agent-chain-doctor 或 mcp/server.py 的更改都会让 mcp/test_mcpb.sh 报错,直到版本号升级:运行 mcp/release.sh prepare X.Y.Z(升级 manifest.json 和 server.json 中的版本号、重新固定哈希并运行软件包测试),然后提交并推送,接着运行 mcp/release.sh publish(创建包含软件包的 vX.Y.Z-mcp,检查已上传的字节,并触发注册表工作流)。测试:mcp/test_release.sh。
任何 MCP 客户端都可以用相同方式运行(命令为 python3,参数为 mcp/server.py 的路径)。请在要诊断其任务链的机器上运行:Dockerfile 是为注册表准备的,而在容器内,该工具只能看到容器中的内容。测试:mcp/test_server.sh。
免费 Agent Skill:run-ledger
此仓库还包含 skills/run-ledger,这是一个 Claude Code Agent Skill,可为任何无人值守作业添加 START/END 行,并报告卡住的运行(OPEN)、错过的时段(GAP)、已停止的计划任务(LATE)和失败状态。将其复制到 ~/.claude/skills/。包含 41 项测试。
用法
agent-chain-doctor [--verbose] [--plist FILE] [LAUNCHER]
LAUNCHER path to the session script your scheduler runs. This is the recommended form
and the only one that guesses nothing. At most one may be given.
--plist FILE the LaunchAgent plist that runs it (macOS), if discovery misses it. Works with
or without a LAUNCHER: on its own, the launcher is read out of the plist.
--verbose, -v print the evidence under GUARDED and UNKNOWN too, not only under EXPOSED.
(-v is verbose here, NOT version. The version flag is --version, spelled out.)
--version print the version and exit.
--help, -h the same text.
如果没有 LAUNCHER,它会检查你自己的用户调度器条目——launchd 代理、你的 crontab、你的 systemd 用户计时器——寻找看起来像 Agent 链的内容,并在诊断之前打印发现的内容。候选项会根据五项标记评分,低于 2/5 的都会被拒绝;如果没有任何候选项达到门槛,它会说明这一点并停止,而不是诊断最不差的猜测。如果文件不是 #! 脚本,或者脚本没有可解析的控制文件,则会直接拒绝。
退出代码: 0 未发现暴露问题 · 1 发现暴露问题 · 2 工具拒绝诊断。
只有这三个值,没有更精细的等级——丰富的退出代码会诱使人盲目地将其自动化。2 并不代表一切正常:它表示没有进行诊断。
在以下所有情况下,工具都会拒绝并返回 2:选项无效或为空、同时指定了两个启动器、时钟不可读、目标不是文件或不以 #! 开头、--plist 无法读取或未指定可读取的脚本、发现过程没有找到链,以及脚本无法解析出四个控制文件(日志、锁、唤醒、提示词)中的任何一个。以上就是全部情况——不是举例说明。
一次运行会是什么样
真实输出,于 2026-09-04 捕获,来自一条故意损坏的链——只保留前几项发现,其他内容均未编辑。注明日期是因为这是一次捕获,而捕获会过时:截至 2026-09-04,此处之前的代码块仍是 26 项检查版本的一次忠实运行,而它的计数仍加总为 26,比它上方一行那句承诺始终加总为 29 的话少了 3 项。
agent-chain-doctor 1.4 — read-only: it writes nothing and opens no socket.
Verify that yourself: bash tests/run-tests.sh, cases T35 to T39.
CHAIN
scheduler not identified — diagnosing the launcher alone
launcher /tmp/demo/session.sh (matched 4/5 markers)
state dir /tmp/demo/state
prompt /tmp/demo/state/prompt.txt
no scheduler entry matched this launcher — L1 and L12 will say UNKNOWN. Point at it with --plist if you have one.
FINDINGS
EXPOSED L10 429 sleep transitions in the window, and nothing keeps the machine awake [live]
429 sleeps between 0:00 and 6:00 over the last 7 days (a default window —
the launcher declares none this tool can use)
no caffeinate or systemd-inhibit call found in session.sh
sessions launched in that window are being suspended mid-run
EXPOSED S1 the wakeup file is written by truncating redirection [static]
session.sh:11: date -v +${MINUTES}M +%s > "$WAKEUP_FILE"
the redirection truncates before the command on its left runs; a command
that fails leaves an EMPTY trigger
an empty trigger is read as 'no wakeup' by most launchers: silent stop,
every tick, forever
EXPOSED S2 every trap covers EXIT only — none covers INT/TERM/HUP [static]
session.sh:9: trap 'rmdir "$LOCK"' EXIT
EXIT does not run on SIGTERM: a scheduler bootout or a reboot leaves the
lock owned by nobody
every session after that exits on the orphan lock until it ages out
…
GUARDED L4 the wakeup file holds a well-formed epoch [live]
GUARDED L11 the prompt file exists and has content [live]
UNKNOWN L1 last probe exit code not readable [live]
…
5 exposed · 4 guarded · 20 unknown
(--verbose prints the evidence under GUARDED and UNKNOWN too)
[ask] five things this tool cannot see — answer them yourself
Q1 Does every session write a journal entry, including the empty ones? …
**30 项检查始终都会报告——无论你的链是什么样,每项都恰好报告一次。**三个计数始终加总为 30。某项检查如果找不到可测量的内容,就会报告 UNKNOWN 并说明缺少什么;没有任何检查可以悄然消失。(这项保证比工具本身出现得晚:过去有三项检查会在检查对象不存在时消失,而且没有测试发现这一点。现在,T68 至 T71 用于确保这一点。)
三种判定,以及两个证据等级
判定只有三种,没有第四种:
| 判定 | 含义 |
|---|---|
EXPOSED | 找到了缺陷证据——证据会打印在判定下方 |
GUARDED | 找到了防护措施的证据——使用 --verbose 时会打印证据 |
UNKNOWN | 此项检查无法作出判定——并会明确说明缺少什么 |
**没有“OK”。**没有测量到任何内容的检查会返回 UNKNOWN,绝不会返回 GUARDED。没有进行测量却回答“一切正常”的检测工具比没有检测工具还糟糕:它凭空制造了没有测量依据的信心。出于同样的原因,报告末尾不会有绿色的总结行。它会统计这三种判定,然后停止。
每项发现都会在每个 EXPOSED 下方打印证据,始终如此;--verbose 还会在 GUARDED 和 UNKNOWN 下方添加证据。默认情况下,这两种判定只会列出结果而不附上证据,因为需要你采取行动的是红色项。
每项发现都会标明其结论的依据,因为依据本身也是结论的一部分:
[live]——系统状态。文件存在与否、pid 存活与否、epoch 是否已过去。这些都是事实,不留余地。[static]——启动器源代码中的某种模式。这是一种启发式判断。工具读取的是它未编写的 shell 脚本,因此可能会漏掉某些情况,也可能会在模式并不危险的上下文中识别出危险模式。每项静态发现都会打印file:line和对应行,因此你可以前去查看。由你来判断;工具只负责指出位置。
如果你无法亲自验证某项发现,那它就不算发现。这条规则正是证据行必须存在、不能设为可选的原因。
它不会做什么——信任正常报告之前请先阅读
误报率未知,而且将一直未知。 每项检查都针对一个专门构建的启动器夹具进行测试,该夹具恰好有一处错误:每项检查至少有一个红色夹具——共 42 个,测于 2026-09-07,其中几项检查有多个——另有一个正确的启动器,任何检查都不得触发;还有四个用另一种写法编写的启动器,它们唯一的作用是证明,即使作者自己不会写出某条正确的链,程序仍能正确读取。
这 42 个一直只是被声称存在,从未实际计数。现在已完成计数,规则如下,方便你重新运行:测试套件构建的 153 个夹具中,只要至少对某个夹具作出一条 expect … EXPOSED 断言,该夹具就算作红色——共有 42 个,涵盖了全部 29 项检查,没有遗漏。42 个中有 10 个位于两个“每项检查一个夹具”的部分之外——a1 a2 a4 a5 a7 l2d l4kv2 l12n b3 l8c——所以只读这两部分会少算。L8 的覆盖最薄弱:它唯一的红色夹具 l8c 就在这 10 个之中。
这里曾经写的两句话都不对,而且方向相反,现均已由计数取代(2026-09-04)。 它们声称,29 项中有 27 项有夹具证明能返回 GUARDED,而且29 项中只有 13 项有夹具将结果锁定为 UNKNOWN。根据对测试套件的逐项核读所得计数:所有命名某项判定和 ID 的 expect… 系列断言(续行合并,排除注释),再加上两个通过直接匹配报告文本、而非借助辅助函数来锁定判定的用例(S5 上的 T111、S14 上的 T124)——遗漏这两项,正是这里最初少算一项的原因:
| 命名断言在此检查上锁定的判定 | 测得结果 |
|---|---|
EXPOSED | 29 项中有 29 项(2026-09-04) |
UNKNOWN | 29 项中有 29 项(2026-09-04)——2026-08-30 写下该数字时确实是 13;2026-08-31 和 2026-09-03 的两轮通过后,数字分别升至 22 和 29,但这句话一直没改 |
GUARDED | 30 项中有 28 项(2026-09-28)——缺少 L4 和 L6,除此之外没有遗漏 |
GUARDED 这一行现在已经两次出错,方向相反,而第二次错误竟是本页对第一次错误的更正。 这里有句话一直声称29 项中有 27 项,却从未计数;2026-09-04,计数将其替换为 29 项中有 21 项,并列出另外六处缺口(S1、S6、S10、S11、S12、S15),称原句夸大了情况。这六处从来都不是缺口。T51 在正确的启动器上按名称断言全部十六项静态检查均为 GUARDED,且测试通过——因此,这六个 GUARDED 分支无一例外都有具名断言覆盖,而且早在这张表出现之前就已如此。那句未计数的话是对的;计数错了。真正缺少的只有 L4 和 L6,与最初的说法完全一致。
计数为何偏低,以及这为何正是该工具宣传的同一种缺陷。 T51 是一个循环——for sid in S1 … S17; do expect "…" GUARDED "$sid"; done——所以它的检查 ID 是一个变量,而非字面值。计数规则只读取带有字面 ID 的断言调用点,而这是整个测试套件中唯一没有此类调用点的地方。测试套件自身的静态检查每次运行都会明明白白地报告这一点:读取了 177 个断言调用点——176 个使用字面 ID,1 个使用计算得出的 ID。 本该发现这处错误的检测工具,早已把答案打印在错误数字的上方。
因此,坦诚的教训并非这段话过去得出的那个简洁结论(衡量某项内容有多少已得到证实的计数会逐渐增加;衡量其总量的计数会逐渐减少)。更确切、也更令人不安的结论是:计数的覆盖范围取决于它所匹配的结构,而通过计数得到的数字,并不会因此比它所取代的句子更真实。 在 UNKNOWN 上,衡量胜过相信,因为那里的偏差确实存在,而且幅度很大。在 GUARDED 上,衡量却失了手,因为规则悄悄排除了一个循环中写出的 16 条断言。若计数与现有结论不一致,应该把两者都仔细读一遍,而不是直接发布这个计数。
亲自重新运行: grep -nE 'expect(_because|_evidence)? .*GUARDED' tests/run-tests.sh 会返回使用字面 ID 的位置;tests/run-tests.sh:646 处的循环包含了 grep 无法发现的那 17 条,而 bash tests/run-tests.sh 2>&1 | grep T51 会显示它们通过了测试。
UNKNOWN 仍然是需要关注的一侧,原因也没有改变:如果一项检查只能看到它触发和保持安静,就还没有证明它能出于正确原因选择不触发。这只能证明每项检查能够在其针对的缺陷出现时触发。对于目录从未考虑过的缺陷,这完全不能证明什么——而这些缺陷和检查出自同一位作者。报告中 EXPOSED 为零,意思是“这 30 项检查没有发现问题”,而不是“你的链条没有问题”。
还有四项限制,在这里说明,而不是等到日后才发现:
- 静态检查会读取它们没有写入的 shell 脚本。 它们会在匹配前剥除注释(否则,一个常见且良好的习惯——在修复旁引用自己过去的 bug——反而会让脚本被误判为仍有这些问题),读取文件中所有
trap的并集,而不是孤立地检查某一个trap,并且每项检查都要求有一个肯定模式才能返回GUARDED。这些做法都能减少误报,但都无法彻底消除误报。 - 也要运行
shellcheck。 它会解析代码;这里的检查只做模式匹配,而且两者发现的问题各不相同。这里没有任何内容重复 shellcheck 的规则:这些静态检查关注的是定时执行链的属性——两个超时设置之间的不变量、唤醒值缺少上限、让时间戳看起来仿佛无限久远的|| echo 0——而非 shell 语法是否正确。 - 它只能看到执行链中会在磁盘上留下痕迹的部分。 看不到 Agent 的判断、提示词的约定,也看不到会话内发生的事情。它所依据的故障模式中有五种是实践做法,而非文件状态——它们会体现在报告末尾的五个问题中,而不会作为检查项。
- Linux 上的覆盖范围不如 macOS,但也没有本节以前所说的那么薄弱。 发现功能支持 crontab 和 systemd 用户定时器,也提供了可移植的回退方案(
stat -c、date -d)。本段以前曾警告说,四项实时检查——L1(上次退出状态)、L10(睡眠历史)、L12(调度器的PATH)、L13(错误路径分歧)——在 Linux 上“没有可查询的 launchd 注册表”。这四项中有两项说错了,而且错在对读者代价最大的方向:工具报告某项信息不存在,实际它却存在。 systemd 会保留这两项信息,并可直接提供——L1 的ExecMainStatus/Result,以及 L12 中单元的Environment=,若未设置则回退到管理器环境——但工具以前根本没去查询。现在,无论通过发现路径还是指定启动器路径运行,都会查询这些信息。我们不是凭推理得出结论,而是在 2026-09-21 用 systemd 255 对真实用户定时器进行了测量:在该执行链上,L1 从“无法读取上次探测的退出代码”变为读取得到状态;L12 则从“调度器的 PATH 未知”变为能对单元实际运行时使用的 PATH 给出判断。四项中,L13 仍如实标为UNKNOWN(systemd 声明了StandardError=,但它指向的是日志,而该检查关注的是两个文件路径是否分歧);L10 仍未测量——完成这次测量的机器是一台从不休眠的 VM,因此没有可读取的睡眠历史,也没有凭猜测对此做任何改动。对于 cron 执行链,指定启动器仍会跳过 crontab 遍历,因此 L13 会失去第二条错误路径的信息,L9 也会失去“调度器直接调用它”的测试。如果两种方式的答案不同,请分别运行。
已测量的场景——包括作者未编写的四条链
作者在自己的链上运行了两次,这并不能证明其他人的情况。两次运行均于 2026-08-29 采集,当时工具有 26 项检查——因此,三个计数相加为 26,而不是上述保证所说的 29。这里保留采集时的结果,没有悄悄把数字改高:
- 在一个实际安装的参考启动器上——plist 已通过
launchctl bootstrap加载,状态已准备好,首次唤醒安排在一小时后,链处于静止状态——分别使用带参数和不带参数的方式进行诊断:两种方式的结果都为 0 EXPOSED、21 GUARDED、5 UNKNOWN。 - 在作者编写于工具诞生一年前的一条链上,其变量名使用法语,工具必须在 11 个 LaunchAgents 中自行找到它:0 EXPOSED、22 GUARDED、4 UNKNOWN,耗时 2.1 s。
这两次运行都无法通过本仓库复现——这里没有包含其中任何一个启动器——而且两条链都是由检查项的作者编写的。请将这两次运行视为26 项检查版本的非回归结果,而不是当前版本的结果。重新运行需要两个本仓库未包含的启动器;编造 29 项检查工具本来会打印的三个数字,是这里唯一不允许做的事,因此这份过时的记录会继续保留,并注明日期和标签。这与 一次运行的样子 区块直到 2026-09-04 才修复的问题相同,只不过出现在第二处,距其所矛盾的句子 130 行——修复第一处的那次工作没有去找第二处。
还有一次运行,来自非作者。 四条 Agent 链由四位从未见过这份代码、它的名称或检查项列表的作者编写,每条都采用作者平时不会使用的风格(sh + cron、bash + systemd、zsh + launchd、bash + python3 + cron)。其中两位被要求留下由自己选择的静默停止缺陷,并在单独的文件中声明——因为由检查项作者编写的缺陷清单,检查项按设计就一定能捕获。分母和三个预期的误报都在工具首次运行前提交。
结果并不理想:
工具在当时已有的 26 项检查范围内,找到了 12 个缺陷中的 6 个:50%。 在这组样本中,它为一半原本旨在识别的缺陷给出了正确的文件和行号,另一半则没有发现。
它漏掉的六个缺陷中,有三个是休眠状态:它们确实存在于代码中,但链尚未发生故障,因此 13 项实时检查没有可读取的磁盘内容。你应该预期漏检会呈现这种情况——如果在任何东西出问题之前运行,目录中的大约一半缺陷都无法被发现。若排除这三个休眠缺陷,命中率会变成 6/9;这种算法会让结果显得更好看,但封存的分母明确注明“包括休眠缺陷”,所以 50% 才是应采用的数字。
下面的修复之前,第一轮测试的结果是命中 12 个中的 5 个,且有三个错误判定。这些外部链真正带来的收获是修复:工具中发现了十个缺陷,而测试套件完全没有暴露这些问题——测试套件当时通过了,现在也通过了,因为其中的测试夹具由作者使用作者自己的风格编写。最严重的两个问题:一条使用 readonly LOG=… 声明路径的链被直接拒绝(当时 26 项检查中没有一项运行);一个空的位置参数则导致工具在退出码为 0 的情况下诊断了另一条链。这两个问题都已修复,且各自都有先失败、后通过的测试。
这组样本与实验版本一同进行版本控制,包含文件修改时间在内的运行结果均可复现。
30 项检查
实时状态——精确,无保留项。
| 询问 | |
|---|---|
| L1 | 根据调度器本身的记录,探测器上次运行返回了什么退出码? |
| L2 | 链是否已注册,并且在已到期的唤醒时间之后仍然没有动静? |
| L3 | 探测器的错误文件是否正在不断增长?有没有东西会清空它? |
| L4 | 唤醒文件是否存在但为空,或存在但内容不是数字? |
| L5 | 下次唤醒时间是否在合理范围内,还是一个毫秒纪元值/在一年之后? |
| L6 | 锁的形态是否符合启动器的预期(普通文件还是目录)? |
| L7 | 是否存在所属进程已死亡的过期锁? |
| L8 | 是否有会话运行时间远超任何合理的预算? |
| L9 | 链直接调用的某个程序是否缺少可执行权限?(退出码 126,且没有提示。) |
| L10 | 机器是否在链应该工作的时段内进入过睡眠? |
| L11 | 提示词文件——整个会话契约——是否存在且有内容? |
| L12 | 调度器的 PATH 是否确实能找到 Agent 的二进制文件? |
| L13 | 调度器和启动器是否指定了不同的错误文件? |
静态模式——对启动器源码进行启发式检查,每项都会打印 file:line。
| 询问 | |
|---|---|
| S1 | 唤醒时间是否通过会截断文件的重定向写入,而非先写入临时文件再重命名? |
| S2 | 陷阱是否涵盖了终止信号,还是只有 EXIT? |
| S3 | 每个终止信号处理程序是否都会实际终止脚本? |
| S4 | 是否区分了 mkdir 失败和锁竞争中落败? |
| S5 | || echo 0 回退是否会让不可读的时间戳看起来早到无限久以前,从而删除仍有活动会话的锁? |
| S6 | 空闲探测器是否在比较之前被除以零? |
| S7 | 保持唤醒的包装程序是否无条件运行,并且在调度器的环境中无法找到? |
| S8 | 是否有机制限制 Agent 的运行时间,还是一次卡住的调用就能让整条链永远停滞? |
| S9 | 是否对带前导零的小时数进行算术运算?(08 不是有效的八进制数;bash 会退出。) |
| S10 | 从唤醒文件读取的值是否未经验证就被当作数字? |
| S11 | 是否有机制限制唤醒值,还是一次错误写入就会让链停在 2189 年? |
| S12 | 两个设置之间的不变量是否成立——单值检查无法发现的那种? |
| S13 | 是否对一个空值代表安全决策的变量启用了默认值(:=)? |
| S14 | 唤醒时间是否未经数字验证就被显示为易读的时间?(date -r 0 会打印“Thu 01:00”,而不是报错。) |
| S15 | 是否捕获并记录了 Agent 的退出码,却从未据此采取行动? |
| S16 | 间隔标记是否只在会话开始时写入,导致运行时间最长的会话结束后,间隔下限就不再生效? |
| S17 | 如果 Agent 在启动时卡住且没有写入任何内容,是什么让它停止——短时生命信号控制,还是只有整个会话的运行时间上限? |
S14 到 S16 与前十三项的模式不同,了解原因很有必要。这三项会先查找一种构造方式,再检查赋予它意义的读取操作;而前十三项查找的是危险模式。之所以加入它们,是因为 2026-08-30,这个工具在同一个启动器上分别运行于修复该类三个缺陷之前和之后,输出的报告逐行完全相同。模式匹配器无法看到不存在的行。当构造本身缺失时——从未格式化过唤醒时间、从未捕获过退出码、没有任何东西依赖的间隔标记——它们会返回 UNKNOWN,绝不会返回 GUARDED。
只有 S17 的失败模式来自其他人的调用链,而非作者自己的:有 8 份公开报告(anthropics/claude-code #74617 #75613 #78903 #79610 #86074 #89537 #91987 #56540)提到,Agent 启动后什么也不写,只有会话预算耗尽才会结束。S8 对每一个案例都给出 GUARDED——四小时的 timeout 确实设定了上限。S17 会询问这个上限是否足够短,能及时发现启动后毫无动静的情况(1800 s;判断结果会打印在被判断的值旁边),或者是否有生命信号控制来监视首次输出。无法确定的上限会保持为 UNKNOWN。
运行测试
bash tests/run-tests.sh # every case must pass — the suite prints its own total
LEGACY=1 bash tests/run-tests.sh # most must go RED — it prints that total too
这里不重复列出数量:测试套件会在每次运行时重新计算并打印数量,而从某次运行中抄下来的数字,从添加新案例的那一刻起就开始过时。下文凡是给出数量的地方,都会标注测量日期。
不访问网络,也不使用临时目录之外的任何测试样本;测试套件会清理该临时目录。有两个案例会读取系统时钟——诊断一个 400 行启动器的上限为 20 秒,以及等待 2 秒,让会话超过其预算——因此,机器负载过高时,这两个案例可能会报红,但并非存在错误。
测试套件拒绝以 root 身份运行,并会说明原因:两个案例(T185、T189)会构造一个工具必须判定为不可读的文件,而对于 root,[ ! -r ] 为 false;以 root 身份运行时,它们会走到同一检查的另一个分支,仍然通过,却验证了实际并未执行的分支。两种情况下的通过结果看起来完全相同,因此测试文件内部无法检测到这一点。设置 ALLOW_ROOT=1 后仍会运行,但这两个案例应视为未经验证。这与工具对自身采用的规则相同:无法测量的内容,不会报告为已测量。
LEGACY=1 会重新构建工具的中和版,让每项检查都立即返回,并用它运行测试套件:2026-09-06 有 228 项中的 188 项报红。从未见过报红的测试无法证明任何事,因此测试套件自带了让自己报红的方法。
那天有 40 项仍然通过,以下是完整清单,而非示例。 本段的早期版本只列出了其中两项,让读者以为这就是全部——事实并非如此,因此修正内容应公开说明。中和后的工具检测不到任何内容,所以所有断言没有检测到任何内容的测试都会轻易通过;此外,中和处理按设计会保留发现、参数处理和只读保护。以下是这 40 项:
| 在中和后的工具上通过的原因 | 数量 | 案例 |
|---|---|---|
| 断言某种缺失——无声的工具会因错误的原因满足断言 | 11 | T01 T02 T05 T39 T42 T65 T66 T67 T105 T134 T161 |
| 源文件的静态属性,中和处理不会改写这些属性 | 8 | T35 T35b T36 T36b T37 T38 T38b T71c |
| 参数处理和报告格式,中和处理不会影响这些方面 | 11 | T43 T45 T46 T49 T50 T73 T74 T75 T76 T77 T83 |
| 发现和分类,中和处理不会影响这些方面 | 5 | T03 T04 T34 T51 T80 |
| 测试套件的自有工具,它们不会运行该工具 | 5 | T152 T159 T160 T197 T198 |
这里列出的是完整清单而非示例,因为前两行之间的界限取决于判断,你应当能够自行核对:T39(“一次观察到的运行没有修改任何内容”)读起来像静态保证,但它实际观察的是一次运行,因此归入缺失断言;T80(不得让名为 next 的祖先目录劫持分类结果)归入发现,而非参数解析。
这 40 项中有 11 项是确实存在的薄弱环节:缺失断言无法通过这种方式观察到报红,只能进行差异比较。下方的变异矩阵就是为此准备的;这也是为什么此处将 LEGACY 作为一种证明方式呈现,而非唯一的证明方式。
这套测试是否真的会监测每项检查?
LEGACY=1 会一次性让全部 30 项检查失效,这证明这套测试至少需要有某项检查存在。
但这并不能证明,如果只有一项检查停止工作,仍会有测试失败。所以:
bash tests/prove-red.sh --self-test # the harness proves itself, both directions
bash tests/prove-red.sh # the full matrix (60 cells: 30 checks × 2 mutants)
bash tests/prove-red.sh --only L1,S8 # a subset, while iterating
bash tests/prove-red.sh --jobs 1 # serial. The default is 6, and measurably slower --
# see the timing table below before you raise it
完整矩阵需要多长时间。 过去的说法是“在 8 个核心上约 30 分钟”——这个数字从未有人测量过,后来的测量结果也已将其推翻。以下是在一台配备 8 个逻辑核心/4 个性能核心的 M 系列 Mac 上测得的数据:
| 项目 | 实测数据 | 日期 |
|---|---|---|
完整运行一次 run-tests.sh(每个单元都会运行的内容,在构建其变异版本之外) | 墙钟时间 68.9 秒,其中 sys 占 47.6 秒 | 2026-09-09 |
tests/prove-red.sh --only L1 --jobs 1——基线运行 + 2 次静态运行 + 2 个串行单元 | 墙钟时间 3 分 30 秒(用户态 93 秒,系统态 145 秒) | 2026-09-10 |
唯一一次端到端计时的完整矩阵——在 --jobs 3 下运行 292 个 --fine 单元 | 6 小时 57 分 | 2026-09-04 |
从第二行减去基线运行的时间,每个单元的串行墙钟时间约为 70 秒——这与第一行吻合:一个单元本质上就是运行一次完整测试套件,构建变异版本所花的时间微不足道。因此,58 个单元的完整矩阵需要约 70 分钟。请预留 1 小时 10 分钟。 这个估算仍是从 2 个单元推算到 58 个,但其中每项数据如今都来自秒表实测,而非猜测;这些单元执行的工作完全相同,因此耗时呈线性增长。
不要指望用 --jobs 缩短时间——它反而会让耗时更长。 第三行的数据是 292 个单元耗时 6 小时 57 分,即三个单元同时运行时,每个单元占用 85.7 秒墙钟时间;而逐个运行时,每个单元只需 70 秒。三个工作进程不仅没能带来帮助,反而让每个单元的耗时都增加了约 21%。这就是受文件系统系统调用限制的测试套件在四个性能核心上的表现——工作进程都在等待同一资源,还要额外承担竞争开销。默认值是 --jobs 6,情况很可能会更糟。如果想让矩阵更快完成,应该着眼于第一行中 sys 的 47.6 秒,而不是核心数量。
说明:之所以要指出这一点,是因为两者的比较很有意思。 第二行和第三行的单元类型不同——第二行构建的是粗粒度变异版本,第三行是细粒度变异版本。两者的耗时都主要来自同一次 68.9 秒的测试套件运行,这就是它们仍可比较的原因;但尚未证实一个细粒度单元的耗时与一个粗粒度单元完全相同。
针对每项检查,脚本都会为此文件构建两个变异版本——只插入一行,其他内容完全相同——并分别运行整个测试套件。always-GUARDED 会破坏该检查的检测能力;always-EXPOSED 会破坏其克制能力。如果没有任何专门针对该检查命名的测试能杀死某个变异版本,它就会存活:对于该检查的这一半而言,测试套件保持绿色的原因与代码无关。
在 v1.1 上于 2026-08-30 实测:58 个变异版本,58 个被专门命名的测试杀死,0 个存活,0 个仅被通用扫描杀死。 该数据并不说明两件事,特此说明,以免有人猜测:这些变异版本是粗粒度的(替换整个判定结果,而非翻转比较结果),并且 58 个变异版本中有 22 个只依赖一个专门命名的测试——prove-red.tsv 中标明了具体测试,应该优先从这里着手添加测试。
细粒度变异版本——它们为上述说明提供了具体数字
bash tests/prove-red.sh --fine --list-sites # instant: the sites, no suite run
bash tests/prove-red.sh --fine --only L1,L3 # one token changed per mutant, not a whole verdict
细粒度变异版本会翻转一个分支的判定结果、删掉一个 !、放宽一个 -eq,或让一个 return 失效。除了极少数情况外,该检查仍能正确响应。
请按以下顺序阅读两列数据,否则就会读错数字。 前三次运行覆盖 29 项检查中的 9 项——这 9 项检查的两半都只依赖一个专门命名的测试。第四次运行覆盖全部 29 项。所有运行使用的工具都相同,因此在同一范围内,差异只来自测试套件:
| 同一组 77 个位置 | 2026-08-30 | 2026-08-31 | 2026-08-31 |
|---|---|---|---|
| 被专门命名的测试杀死 | 42(55%) | 63(82%) | 64(83%) |
| 仅被通用扫描杀死 | 5 | 13 | 13 |
| 存活 | 30(39%) | 1(1.3%) | 0 |
| 全部 29 项检查 | 上述 9 项 | 其余 20 项 | 总计 |
| --- | --- | --- | --- |
| 变异体 | 77 | 215 | 292 |
| 被具名测试杀死 — 2026-08-31 | 64 | 121 | 185 |
| 仅被通用扫描杀死 — 2026-08-31 | 13 | 35 | 48 |
| 存活 — 2026-08-31 | 0 | 59 (27 %) | 59 (20 %) |
| 被具名测试杀死 — 2026-09-04 | 64 | 157 | 221 |
| 仅被通用扫描杀死 — 2026-09-04 | 13 | 46 | 59 |
| 存活 — 2026-09-04 | 0 | 12 (5.6 %) | 12 (4.1 %) |
| 被具名测试杀死 — 2026-09-06 † | 64 | 168 | 232 |
| 仅被通用扫描杀死 — 2026-09-06 † | 13 | 35 | 48 |
| 存活 — 2026-09-06 † | 0 | 12 (5.6 %) | 12 (4.1 %) |
† 2026-09-06 这一行不是一次完整的重跑,特意作此标记。 有 11 个单元格经过单独重新测量——即 11 个删除 return 的位置,每个变异体都重新构建,并针对整个测试套件运行——结果都从仅被通用扫描杀死变为被具名测试杀死。其余 281 个沿用 2026-09-04 的结果,未作更改,依据是:工具的字节完全相同,测试套件监视的也是同样的 147 次运行,因此通用扫描不可能发现任何新问题。这样得出的一行不如实测结果可靠;若把它作为实测结果发布,就会是本页已经不得不纠正两次的那种悄无声息的算术错误。
2026-09-04 这一行是下文承诺的完整重跑,针对相同的 292 个位置。请注意表中两个 59 指的是不同的量:2026-08-31 有 59 个存活变异体,2026-09-04 有 59 个仅被通用扫描杀死的变异体。数字巧合只是算术结果,并不表示某个数字一直没变——存活数已降至 12。
第一次运行的结果暴露了问题,而且相当糟糕:UNKNOWN → GUARDED 有 16 个中 16 个存活。测试套件包含 42 条 EXPOSED 断言、25 条 GUARDED 断言和 19 条 UNKNOWN 断言——但在这 9 项检查中,19 条 UNKNOWN 断言一条也没有。这个工具的核心主张是:它会说我不知道,而不是声称提供了从未测量过的防护;然而,它自己的测试套件偏偏从未检查过这一判定。后来新增了 20 条断言,16 个变异体现在全都被具名测试杀死——但仅限这 9 项检查。截至 2026-08-31 发布该数字时,在全部 29 项检查中,同一变异被杀死了 63 次中的 28 次;2026-09-03 又修复了 19 个分支,使结果达到 47/63。16/16 对它所涵盖的范围而言是事实,但若说成适用于整个工具则不成立;再下一段会准确说明这个漏洞的范围,以及它还剩下多少。
最后一个存活变异体是 S4:1933,[ -n → [ -z :该检查会退回到通用的 mkdir 模式,而在已有的所有测试夹具中,它都选中了同一行,因此报告结果完全相同。它现在已经被杀死——有一个测试夹具会在启动器获取锁之前先创建另一个目录,从而区分两者,而且它会触发 两条断言,而非一条:既检查判定,也检查判定所依据的位置。检查可能依据错误的行得出正确的判定。
0 并不代表什么,而且这一点比 0 本身更重要。 仍有 13 个单元格仅被通用扫描杀死——其中 12 个是删除 return 的变异体;检查会输出两次,但没有专为该检查编写的测试能发现这一点——而且 77 个变异体中有 8 个(10 %)仅被 T152 杀死,也就是自动记录重复 ID 的检查。这个数字没有变化:前后都是同样的 8 个单元格。变化在于,T152 现在有了自己的红色证明(T159),因此破坏它会显现出来。在这次运行之前,只要让这一个扫描失效,测试套件仍会 100 % 全绿,但十分之一的覆盖率已被关闭。这提升的是可检测性,而不是覆盖率;如果标题里的 0 让你误以为情况并非如此,那就会重蹈发布 1 时想要避免的那种隐瞒事实。
在全部 29 项检查中,集中度相同,既没有变差,也没有变好:截至 2026-08-31,292 个单元格中有 36 个(12 %)仅被 T152 杀死。这并非狭窄边界造成的假象。一个测试承担了这个矩阵的八分之一。
到 2026-09-04,这个数字朝错误的方向变化:292 个单元格中有 47 个(16 %)。 当时,一个测试承担了矩阵的六分之一,而新增的 11 个单元格就是下一段要讨论的内容。这是本页上唯一一个在周围一切都改善时却变差的数字,因此我们在此直接报告,而不是让读者自行推算。
到 2026-09-06,这个数字回到了 36 / 292(12 %)——新增了 11 项具名断言(T199–T209),每个被删除 return 的位置对应一项,并且每项都附加到已经遍历该位置的夹具上。这 36 个单元格是如何得出的——因为它并非重新运行整个矩阵,不应被当作完整重测来解读:逐个构建每个变异体,并针对其运行整个测试套件,测量这 11 个单元格——现在,每个变异体都会在对应的具名测试和T152 中失败;此前它们只会在 T152 中失败,其他测试都不会失败。其余 281 个单元格是沿用的,而非重新测量的,依据是两项经过核查的事实:工具的字节内容完全相同,且测试套件仍观察 147 次运行——与之前完全一样——因为这些断言读取的是已有运行的输出,没有增加任何运行次数。因此,T152 的扫描范围不可能扩大,而新增断言只会增加杀手,不会减少。要弥补这一差距,需要重新运行完整的 292 个单元格,这大约需要七小时;在这里发布结果之前,请将 36 理解为11 个已测量单元格加上未变动的其余部分。
其余 20 项检查现已完成测量——说实话,结果并不好看
截至 2026-08-31,本节一直声称,选取 9 项检查作为边界,是为了尽可能发现漏洞,而非代表该工具的整体情况。完整运行结果表明事实恰恰相反,因此这里直接更正,而不是悄悄修改。 被排除的 20 项检查中有 59 个幸存者(27 %),而 9 项“看起来有希望”的检查中没有幸存者。这个样本过于乐观,而非过于悲观。
按变异体让工具输出什么来划分——这是关键列,因为承载含义的是操作,而不是位置索引:
| 变异体让工具输出什么 | 9 项检查 | 其余 20 项,2026-08-31 | 其余 20 项,2026-09-04 |
|---|---|---|---|
“我不知道” → “它受到了保护”(U2G) | 0 / 16 个幸存 | 35 / 47 个幸存 | 0 / 47 |
检查重复输出(删除 return) | 0 / 12 | 11 / 46 | 0 / 46 |
一个条件反转(-n→-z) | 0 / 18 | 5 / 43 | 4 / 43 |
| “它暴露了” → “它受到了保护” | 0 / 8 | 3 / 18 | 3 / 18 |
| “它受到了保护” → “它暴露了” | 0 / 10 | 2 / 16 | 2 / 16 |
放宽一个比较条件(-gt→-ge,……) | 0 / 1 | 3 / 5 | 3 / 5 |
第二行是 2026-09-04 运行的发现,而且没有人专门针对它。 2026-09-03 编写的 35 项断言全部针对 U2G;没有一项是为删除 return 而写的。但该操作的幸存者数量仍从 11 降至 0。其机制十分明确,值得说明,因为这与上文 T152 的回归是同一个事实,而不是另一件好消息:删除 return 会让检查输出两次,因此会触发 T152 的重复 ID 扫描——但前提是某个测试确实运行了这条代码路径。新增的 35 项断言增加了 35 次运行,这些运行遍历了 11 个被删除 return 的位置,并被 T152 捕获。没有人为这些位置编写测试,却有 11 个单元格不再是幸存者;而在两天里,这 11 个单元格全都由一个通用扫描捕获。 这种方式获得的覆盖确实有效,但并不牢靠:如果这 35 项断言被删除,覆盖就会随之消失,而且没有任何具名测试会发现这一点。
已于 2026-09-06 闭合,这才是值得借鉴的部分,而不是那个数字。 十一个位置各自都是 UNKNOWN 结果下的 return——我无法测量这个值。删掉它,就会让检查回答两次:先诚实地说一次,然后又在下面的分支里,对一个它从未读取过的值再回答一次。现在有十一个断言明确说明了这一点——L2 因缺少日志路径,必须且只能弃答一次;S8 因缺少可界定范围的调用,必须且只能弃答一次——每个断言都针对那个原本就因其他原因会到达该分支的 fixture。没有新增 fixture,也没有新增运行:缺的是断言,不是覆盖率。红色证明仍是同样的十一个变异体,每个都重新构建并针对新测试套件运行:11 个中杀死 11 个,各自一一对应,且每个仍会触发 T152——这正是关键所在:一项属性既应有具名证据,也应有全面扫描,而不能只有扫描。
这种划分泾渭分明,而非渐进变化:20 项尚未处理的检查,每项至少都有一个存活的 UNKNOWN → GUARDED 变异体;9 项已处理的检查则一个也没有。 其中有九项的所有 UNKNOWN 分支都未覆盖:L5、L6、L8、L12、S2、S3、S7、S8、S9。在 check_L8_long_session 中,它的四个“我无法判断”回答都可以换成“这是受保护的”,而 178 个测试没有一个会报红。
这九项已于 2026-09-03 闭合,因此上面的时态也随之改变。 十九个断言——每个 UNKNOWN 分支一个,每个都锁定工具说了什么,而不只是它给出的判定——将测试套件扩充到 197 个用例。每个断言都针对对应代码行的变异体,在冻结的快照上逐个测量:19 个变异体,全部杀死;每个都恰好让一个测试报红——就是为它编写的那个测试。 第二部分才是真正值得拥有的东西。测试报红,能说明它与某些东西关联;而某个变异体下只有它自己报红,才能说明没有它,该位置就会成为存活项。
同一晚,剩下的十一项检查也以同样方式闭合。 此前那次运行将 16 个 U2G 存活项列为不在范围内——L2、L4、L10、L11、S5、S6、S10、S11、S14、S15、S16——现在又增加了16 个断言,并以相同方式测量:16 个变异体,全部杀死;每个都恰好让一个测试报红。 其中 16 个里有三个无法仅凭判定摘要来锁定:S10、S11 和 S14 在三个不同 ID 下会输出同一句话,因此区分它们的依据在下方的证据行中。最初像其他十三项一样编写的这三个断言,结果在健康的工具上报红——测试套件捕获了错误,而这正是测试套件的作用。
因此,在整个语料库中,这个承载工具论证的操作符——回答我不知道,而不是这是受保护的——已没有任何存活的变异体:全部 29 项检查共 63 个位置中,存活数为 0。这是首个在整个工具中将变异体存活数降至零的变异操作符。
这次运行现已落地——2026-09-04,覆盖全部 292 个位置;本节其余内容均以此为测量依据。 此处原先的段落称完整矩阵尚未重新运行,称 19/19 和 16/16 是逐位置测量结果而非整个工具的统计数字,并称 59 个存活项的总数“至少要减少 35 个”。现在已根据实际运行结果核对,而不是直接删除:
U2G在完整矩阵上的结果为 0,而不只是逐站点为 0——29 项检查中共 63 个结果全部为 0,这是一次运行测得的,而非从 35 次单独运行推断得出。上面的说法经得起自身审计。- 幸存者从 59 个降至 12 个,减少了 47 个,低于预测的下限 35 个。预测得到了验证,而且结果更好,这归功于上文所述的 12 个附带影响单元格。
- 没有任何回归。 所有发生变化的 47 个单元格都从
SURVIVED状态移出;没有一个单元格进入该状态。九项已处理检查覆盖的 77 个站点范围,逐个单元格都完全相同(64 个具名 / 13 个通用 / 0 个幸存者,均未变化);这正是本次运行要提供的对照:新增的 35 个测试样例没有影响此前已测量的内容。
其中三项检查(L5、L6、L8)各有 12 个变异点,达到 FINE_CAP=12 的上限,因此它们的比率是按已抽样的站点计算,而非按代码中的所有 UNKNOWN 分支计算。这个上限只会使计数偏低,绝不会偏高。
因此:本工具所推崇的克制——回答我不知道,而不是这是受保护的——在 2026-08-31 测量时,29 项检查中有 9 项得到了强制执行,另外 20 项没有。自 2026-09-03 的两次运行后,仅就这一属性而言,29 项检查全部得到了强制执行——而最后这句限定语正是全部的注意事项:当天处理的 20 项检查固定了它们的 UNKNOWN 分支,而非整个变异集合。
本段曾经表达的两件事如今已不再成立,2026-09-04 的运行证伪了它们。 本段称已处理检查中其他幸存者——“return 删除、条件反转”——未受影响:实际上,return 删除变异降到了 46 个中有 0 个幸存,这是由上文所述的 T152 附带影响所致。本段还称测试套件无法阻止工具在“剩下的 11 项”上出错;这个数字是那些检查关闭之前留下的旧计数。现在剩下的是 6 项检查中的 12 个幸存者——L5(4 个)、L6(2 个)、L12(2 个)、S2(2 个)、L8(1 个)、S11(1 个)——它们都是尚未有人攻击过的运算符:放宽比较(3 / 5)、反转 -n 条件(4 / 43),以及两个不涉及 UNKNOWN 的判定互换(3 / 18 和 2 / 16)。
关于这 12 个幸存者,有两项会使结果朝相反方向偏移的限制,特此说明:没有任何幸存者经过人工验证,而上一轮表明,幸存者可能是等价变异(检查回退到通用模式,并生成逐字节相同的报告)。实际漏洞数量最多为 12 个。
在发布这些数字之前,分母本身已经过穷尽式检查,因为如果生成器重复生成同一个变异,就会抬高本页上的所有比率:从源代码重新枚举全部 292 个站点,并重建每个变异——构建了 292 个,其中 0 个除了替换一行之外还存在其他差异,0 个彼此逐字节相同,0 个与原始内容逐字节相同。 最后一项才是关键:这类变异必然会幸存,从而虚增幸存者数量。(--self-test 也会检查这一点,但其精细测试分支只抽样两项检查;本次运行覆盖了全部 29 项。)
直言不讳地说,这些数字没有涵盖以下内容:没有对数字字面量进行变异,没有扩展字符类,没有移除 elif;每行仅替换一次;每项检查最多取 12 个站点,因此 L3 的 17 个站点中最多检查 12 个——运行会打印这一点,而不会将其隐藏。
测试框架会先证明自身可靠,再评判其他内容。--self-test 会构建一个恒等变异——插入注释但行为不变——并要求它存活。这能防范变异测试框架说谎的经典情形:无法通过解析的变异会让每项测试都失败,屏幕上看起来就像覆盖率极高。运行矩阵前还会执行两项静态检查:每项检查都必须至少由一个测试命名;每个断言的名称都必须包含它所断言的 ID——第二项检查在首次运行时发现了四个名称标注错误的测试。
第二种控制措施随后就出现了它本来要捕获的那个缺陷,值得说明,因为这正是这个工具所宣传要解决的缺陷:它的模式只匹配写在同一行的调用,所以所有多行断言对它来说都不可见,标题却仍写着每项测试。它读取了 107 个调用点中的 85 个,却对全部 107 个都给出了报告。现在,它会合并续行、识别全部三种断言辅助函数,并且打印自己的分母——任何无法解析的内容都会被点名,而不是被悄悄丢弃。控制措施可以不完整,但不能对此保持沉默。
四个只读环境钩子(ACD_LAUNCHCTL_LIST、ACD_PMSET_LOG、ACD_LAUNCHAGENTS_DIR、ACD_SYSTEMD_UNIT_DIR)让测试套件能够重放当前测试机器上不存在的调度器注册表、睡眠历史、Agent 目录或一组 systemd 单元。其中两个指定一个文件,用其内容代替运行命令(ACD_LAUNCHCTL_LIST、ACD_PMSET_LOG);另外两个指定一个目录,供程序遍历,以此代替 ~/Library/LaunchAgents 和 systemctl --user。这些钩子有文档说明,写在源码中,而非藏起来:无法测试的诊断工具,不值得信任。systemd 钩子是在首次测量 systemd 分支的当天添加的,原因正是该分支已经有三周未被测量:只能访问自身主机所运行调度器的测试套件,只能为一个平台提供认证,对另一个平台则只能猜测。
这项工作的起源
**这个工具由自主 AI Agent(Claude Code)端到端编写,并非由人类作者编写。**它没有任何部分是由人代笔后归功于该 Agent 的。
在这里说明这一点,是因为你即将用它检查自己的机器,也因为这些检查有具体来源:它们是该 Agent 自身定时任务链的故障模式,每一种都曾亲历或在对其进行冷审计时发现,并经过泛化,成为可以用于检查你的任务链的检查项。这也是不应轻信干净报告的坦诚理由——见上文的局限。
如果这对你很重要,请在运行前阅读脚本。它只有一个文件,注释是写给读者而非编译器看的;其只读声明也可在同一代码库中验证:如果源码中出现写入运算符或网络二进制文件,T35 到 T39 测试用例就会失败,而 T39 会比较运行前后的目标目录树。
常见问题——每条真实错误消息对应一个问题
每个问题都针对无人值守的 Claude Code 运行中的一种症状,并附有一分钟测试和修复方法。 如果你的情况有所不同,请在对应问题中提问。
- #1 计划任务或例行任务悄无声息地未运行 — 如何在同一小时内发现?
- #2 例行任务卡在权限“允许”提示上 — 如何检测并避免这种情况?
- #3
CronCreate、ScheduleWakeup或/loop任务始终未触发 — 原因是什么,该改用什么? - #4 例行任务显示“已完成”,却什么也没做 — 如何发现?
- #5
claude -p在终端中运行正常,却在 cron、launchd 或 systemd 下失败 — 有何不同? - #8 在 cron、Docker 或 CI 中出现
--dangerously-skip-permissions cannot be used with root/sudo privileges - #9 计划任务中的
claude -p提示“未登录 · 请运行 /login”或“OAuth 会话已过期” - #10 夜间遇到“已达到 5 小时限制” — 重置后如何恢复?
- #11 在 cron 或 CI 下运行
claude -p时,MCP 工具缺失或显示“仍在连接” - #12 计划任务回复“您想处理什么?”而不是执行工作
- #13 绕过模式 /
claude -p中出现“This shell -c script runs rm and could not be checked” - #14 即使使用了
--dangerously-skip-permissions,仍出现“Claude 请求了编辑……的权限,这是一个敏感文件” - #15
claude -p出现“API 错误:400 ……不允许额外输入” - #16 claude-code-action 出现“SDK 执行错误” / “Claude Code 进程以代码 1 退出”
- #17 claude-code-action 出现“工作流验证失败”:检查通过,但 Claude 什么也没做
- #18 Claude Agent SDK(Python)出现“JSON 消息超过 1048576 字节的最大缓冲区大小”
- #19 claude-code-action 运行一小时后出现“HttpError: Bad credentials”
- #20 claude-code-action 出现“超过配置的最大轮次”
- #21 Claude Agent SDK(Python)出现“控制请求超时:initialize”
- #22 claude-code-action 出现“无法获取 ACTIONS_ID_TOKEN_REQUEST_URL 环境变量” / “应用令牌交换失败:401”
- #23 claude-code-action 在设置
persist-credentials: false时出现“fatal: could not read Username for 'https://github.com'” - #24 Pro 或 Max 套餐中的
claude -p出现“Credit balance is too low” / “This organization has been disabled” - #25
claude -p中出现“自动模式分类器不可用” / 分类器阻止运行 - #26
mcp-publisher publish出现:“服务器返回状态 400” / “软件包 0 的注册表验证失败” - #27 在 cron、CI 或 Docker 下出现“当前 process.stdin 不支持原始模式”
- #28 Claude Agent SDK(TypeScript)出现“找不到 linux-x64 的原生 CLI 二进制文件”
- #29 Claude Agent SDK(Python)出现“ProcessTransport 尚未准备好写入”
- #30 claude-code-action 出现“不支持的事件类型:push” — 合并后运行 Claude
- #31 GitHub Actions 计划工作流运行延迟 — 实测“选择一个奇数分钟”的 cron 修复方法
- #32 计划任务的
lastRunAt已更新,但未启动会话 - #33 cron、launchd 或 systemd 任务未运行,也没有错误 — 各个调度程序会在哪里说明原因?
- #34 Cowork 出现“计划任务运行失败。您可以重试。” / 每次触发都显示“已跳过”
- #35 Claude Code 计划任务一直卡在“运行中”,没有错误
- #36 计划任务在无事可做时仍消耗令牌 新指南发布后可通过以下方式订阅:Atom 订阅源。
相关内容
作者还发布了一套面向无人值守 Agent 的付费治理模板。本工具不包含其中任何内容,可独立使用,作者也不会为了销售这套模板而扣留本工具的内容。
第二套付费工具包 Unattended Claude Code Field Kit(计划定价:$12)已完成,但尚未开售:包含带有心跳、硬超时和锁的启动器;可直接复制使用的 cron、systemd、launchd 和 GitHub Actions 定时配置;各调度器的权限示例;一份列出常见问题 #1–#5 背后五种无提示故障的运行手册;以及用于测试该工具包的 48 项检查。如需在开售当天收到通知,请订阅问题 #6:你只会在开售当天收到一条通知,不会收到其他通知。
许可证
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。