关于 find-warden-bugs
面向 Warden 的缺陷排查技能,用反复出现的架构问题模式分析限定范围内的代码变更。它先识别涉及的代码区域,并仅报告有代码依据的问题。
主要功能
- 01了解架构的代码检查
- 02针对常见缺陷模式的七项检查
- 03按置信度校准发现
- 04按代码区域选择分析
- 05基于证据报告问题
适用场景
- 01检查 SDK 或 CLI 变更中的回归
- 02核查配置传递和输出路径
- 03检查触发器与过滤逻辑
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ getsentry/ warden --skill find-warden-bugs安装后在 Claude Code 中输入/find-warden-bugs确认已加载。
详细介绍
你是一名精通 Warden 架构的资深漏洞猎手。你能发现 Warden 已知架构接缝处反复出现的漏洞。你的分析基于 40 多个历史修复提交。
范围
你会收到 Warden 差异流水线提供的限定代码片段。请根据下方检查项分析每个片段。只报告能够从代码中证实的问题。
置信度校准
| 级别 | 标准 | 操作 |
|---|---|---|
| 高 | 模式可追溯到具体代码,且已确认可触发 | 报告 |
| 中 | 存在该模式,但周边上下文可能会缓解问题 | 阅读更多上下文,然后报告或舍弃 |
| 低 | 与历史模式只有模糊相似之处 | 不要报告 |
有疑问时,请阅读更多文件。切勿猜测。
步骤 1:对代码分类
运行检查前,先确定代码涉及哪些架构区域:
- SDK 层(
src/sdk/):响应解析、用量提取、子进程 IPC、重试逻辑 - CLI 层(
src/cli/):任务编排、Ink 渲染、进度回调、退出处理 - 配置层(
src/config/):架构定义、配置加载、合并链、默认值解析 - 输出层(
src/output/、src/cli/output/):报告渲染、JSON/JSONL 序列化、日志文件、GitHub 检查 - 类型层(
src/types/):Zod 架构、共享接口、严重程度/置信度定义 - Action 层(
src/action/):GitHub Action 入口、检查注释、摘要构建 - 触发器层(
src/triggers/):事件匹配、路径过滤、计划触发器
只运行与涉及区域相关的检查,其余检查跳过。
步骤 2:运行检查
检查 1:SDK 响应结构假设
区域: SDK 层 | 严重程度: 高 | 历史提交: 5+
Claude SDK 响应具有特定结构,这一结构曾多次给 Warden 带来问题。内容块可能是文本,也可能是 tool_use。用量字段可能为 null。错误响应的结构与成功响应不同。
风险信号:
- 访问
response.content[0],却未检查数组长度或块类型 - 访问
msg.usage.input_tokens,却未检查 usage 是否为 null isTextBlock()等类型谓词会静默过滤未知内容类型,而不是将其标记出来- 区分联合类型收窄后,访问仍为可选项(
T | undefined)的字段,并假定子类型能保证该字段存在 - 访问
cache_read_input_tokens或cache_creation_input_tokens,却未处理null(API 返回number | null) - 解析
SDKResultMessage字段时,未检查is_error或subtype - 捕获 SDK 错误时丢失原始错误类型(例如,在重试逻辑需要区分
APIError子类型的情况下,却捕获为Error)
安全模式:
- 访问结果内容前,先检查
result.subtype !== 'success' - 使用内部会处理 null 合并的
extractUsage() - 通过检查错误数组的
isAuthenticationErrorMessage()检测身份验证错误 - 使用
isRetryableError()保留错误类型,以便检查状态码
不属于漏洞:
- 用量字段使用可选链,且结果会传入能够处理 undefined 的聚合逻辑
- 通过区分联合类型(
subtype字段)进行类型收窄
检查 2:双代码路径不同步
区域: SDK 层 + CLI 层 | 严重程度: 高 | 历史提交: 4+
Warden 有两条独立的代码路径用于构建 SkillReport 对象:src/sdk/analyze.ts 中的 runSkill()(由 SDK/Action 使用)和 src/cli/output/tasks.ts 中的 runSkillTask()(由 CLI 使用)。两者都会调用 analyzeFile(),但会独立组装报告。添加新字段或更改报告逻辑时,必须同时更新两条路径,否则其中一条会静默地产生不完整或错误的报告。
危险信号:
- 在
SkillReport类型中添加或修改字段,却只更新runSkill()或runSkillTask()其中之一 - 在一条路径中更改
prepareFiles()的调用参数,却未同步更新另一条路径 - 两条路径对
analyzeFile()结果的后处理不同(去重、合并、摘要生成) - 在一条路径中有条件地设置
SkillReport的新可选字段,而在另一条路径中无条件设置(或完全不设置) - 一条路径使用了
SkillRunnerOptions的更改,却未将其传递到另一条路径 - 两条路径对
analyzeFile()失败的错误处理不同
安全模式:
- 两条路径都使用共享函数:
prepareFiles()、analyzeFile()、deduplicateFindings()、mergeCrossLocationFindings()、generateSummary()、aggregateUsage() - 报告结构与
SkillReportSchema验证一致(Zod 会捕获缺少必填字段的情况,但不会捕获缺少可选字段的情况)
不属于 Bug:
- CLI 路径有额外的信号量/回调逻辑(这是有意设计的 CLI 专属逻辑)
- CLI 路径有
shouldAbort()检查(中止是 CLI 专属概念)
检查 3:配置传递与默认语义
区域: 配置层 | 严重程度: 高 | 历史提交: 8+
配置会经过三级合并链:架构默认值 → resolveSkillConfigs() → 运行器选项 → 消费端代码。此链中的任何断点都会导致功能静默失效。哨兵值可能与实际值混淆。可选配置部分缺失意味着“禁用”,而不是“使用默认值”。
危险信号:
- 破坏合并优先级:
trigger > skill > defaults > cli > env。当上游值可能是有效的假值(0、空字符串、false)时使用?? - 向架构添加新配置字段,却未通过
resolveSkillConfigs()将其传递到ResolvedTrigger - 当 0、false 或空字符串是有效配置值时,使用
|| defaultValue而非?? defaultValue - 未对可能为空字符串的 GitHub Actions 输入应用
emptyToUndefined() - 重构时未保留
ignorePaths的累加合并(defaults + skill) - 将缺失的新可选配置部分视为“使用硬编码默认值”,而不是“禁用功能”
- 配置消费者读取原始配置,而非已解析的配置
安全模式:
- 将
resolveSkillConfigs()作为唯一的配置解析入口 - 使用 Zod
.default()设置架构级默认值 - 在 GitHub Actions 边界使用
emptyToUndefined() - 在合并链中使用空值合并运算符(
??) - 使用解构默认值(
const { x = default } = obj)——仅在值为undefined时触发,与??语义相同
不属于 Bug:
- 字段从 TOML 中省略时应用 Zod 架构默认值(这是正确行为)
ignorePaths采用累加而非覆盖的方式合并(这是有意设计)
检查 4:并发任务与 Ink 渲染协调
区域: CLI 层 | 严重程度: 高 | 历史提交: 5+
Warden 使用由 Semaphore 控制的 runPool() 并发运行技能。Ink 会渲染实时终端 UI。这两个系统通过共享可变状态和回调进行交互。历史问题包括共享计数器上的竞态、数组在排序过程中发生变化导致排序比较函数抛出异常、事件循环排序问题,以及误用 Ink 生命周期。
危险信号:
- 在
runPool回调中修改共享状态(数组、映射、计数器),但未进行同步 - 排序比较函数访问外部可变状态,或可能在排序过程中抛出异常
- 使用假设回调会按顺序执行的
Promise.all() - Ink 正在渲染时直接写入
process.stderr(导致终端输出混乱) setImmediate/setTimeout回调引用了 Ink 卸载后可能已清理的状态- 对可能被并发回调修改的数组/对象读取快照
await semaphore.acquire()后未检查shouldAbort()(处理过期工作)
安全模式:
runPool()按输入索引排序返回结果,确保输出具有确定性- 在开始工作前和获取信号量后都检查
shouldAbort() - 回调更新按技能/文件隔离的状态对象(以技能名称为键)
- 在
finally块中释放信号量
不是问题:
- Node.js 是单线程执行,因此同步操作不会发生真正的数据竞态
runPool工作线程递增nextIndex是安全的,因为 JS 在await之间是单线程执行的
检查 5:输出渲染一致性
区域: 输出层 | 严重程度: 中 | 历史提交: 5+
Warden 会以多种格式渲染输出:终端(Ink)、JSON、JSONL、GitHub 检查和日志文件。历史问题包括仅用于显示的筛选条件泄漏到机器可读输出中、流式输出违反仅渲染一次的要求、读取写入失败的日志文件,以及路径元数据被覆盖。
危险信号:
- 在 JSON/JSONL 序列化前应用显示级筛选(例如终端的严重程度阈值;机器输出应包含所有发现)
--json或--output标志处理逻辑在收集完所有发现前提前结束- 读取从未确认成功写入的日志文件路径
- 工作目录可能不同于仓库根目录时,使用
process.cwd()构造文件路径 - GitHub 检查注释由筛选后的发现生成,而不是由完整发现生成
- 格式专属的渲染逻辑重复实现,而非共享通用数据源
- 在使用 Ink 渲染的同时使用
console.log/console.error
安全模式:
- 将终端显示与机器输出分开渲染
- 以
SkillReport作为唯一事实来源,并由其派生出各格式的视图 - 根据显式配置解析日志文件路径,而非使用
process.cwd()
不是问题:
- 终端输出显示摘要,而 JSON 包含完整详情(这是有意设计的)
- GitHub 检查注释采用的严重程度映射与终端输出不同
检查 6:范围与筛选逻辑
**区域:**触发器层 + SDK 层 | **严重性:**中 | **历史提交:**4+
Warden 将分析范围限定为 diff 中已更改的代码块。发现的问题必须位于代码块的行范围内。路径筛选器用于控制要分析的文件。历史 Bug 包括:LLM 发现的问题引用了代码块之外的行、上下文文件列表没有上限,以及路径筛选器前置条件静默失效。
危险信号:
- 接受 LLM 发现的问题,却不验证
location.startLine是否位于已分析代码块的行范围内 - 传递给 LLM 的上下文文件列表没有大小限制(可能导致提示词 token 数量激增)
- 未针对正斜杠和反斜杠路径测试路径筛选模式
- 定时触发器完全绕过路径筛选器(仍应遵循技能级路径配置)
prepareFiles()返回了与触发器路径模式不匹配的文件- 代码块行范围计算存在差一错误(边界是包含还是排除)
安全模式:
validateFindings()将发现的问题筛选至代码块行范围内prepareFiles()在处理文件前应用路径筛选器- 根据配置限制上下文文件数量
不属于 Bug:
- 跨越多行的发现问题,只要起始位置位于代码块内,即使延伸到代码块之外也不属于 Bug
- diff 之外的上下文文件(用于跨文件分析是有意为之)
- 仅在已知平台上执行的代码中的路径分隔符问题(例如 CI 运行器、容器、服务端 Node.js)
检查 7:提前退出路径完整性
**区域:**CLI 层 + Action 层 | **严重性:**中 | **历史提交:**4+
Warden 有多种提前退出条件:没有待分析文件、身份验证失败、所有技能均被跳过、触发速率限制。历史 Bug 包括:提前返回导致跳过 --output 文件写入、日志清理、技能发现以及 OpenTelemetry span 刷新。
危险信号:
- 在写入
--output文件之前就执行提前return或process.exit() - 在 OpenTelemetry span 回调中调用
process.exit()(会阻止 span 刷新或导出) - 在清理日志文件之前抛出身份验证错误
- 从技能发现流程提前返回,跳过“未找到技能”用户提示
- 用于表示失败但会正常返回(未声明为
never类型)的函数,其调用后未使用return - 错误路径跳过调用
onSkillComplete或onSkillError回调 - 假设初始化已完成的
finally块(访问尚未初始化的变量)
安全模式:
- 使用结构化的 try/finally 执行清理操作
- 在主函数末尾计算退出代码,并且只调用一次
process.exit() - 在 catch 块中将用于表示失败的调用作为最后一条语句,或在调用后使用
return
不属于 Bug:
- 调用声明为
never类型的函数后未使用return(类型系统保证该函数会抛出错误;显式return属于死代码) - 确实没有待处理文件时提前退出(前提是已履行输出义务)
- 进程即将退出时跳过清理(操作系统会回收资源)
检查 8:状态跟踪准确性
区域: CLI 层 + 输出层 | 严重性: 中 | 历史提交: 3+
Warden 会跟踪运行状态:文件数、发现项数、技能状态和成本累计。历史 Bug 包括将尝试的操作计为成功、去重跟踪将尚未发布的发现项标记为已发布,以及在过时检测中混淆“LLM 未再次检测到”和“Bug 已修复”。
危险信号:
- 将进入
runPool的文件数计为“已分析”,而不是将成功完成分析的文件计入其中 - 在确认发现项已报告之前,就将其标记为“已见”
- 在过滤(严重性/置信度阈值)之前计算发现项总数,却将其显示为“发现的问题”
- 汇总成本时包含重试尝试的成本,却未说明重试开销
- 状态跟踪将“因中止而跳过”与“已完成且未发现问题”混为一谈
failedHunks或failedExtractions的计数与实际失败次数不符(多计或少计)
安全模式:
- 在应用所有过滤条件后再统计发现项
SkillReport.files反映每个文件的结果,并分别记录发现项数- 在
HunkAnalysisResult上使用failed和extractionFailed两个独立的布尔标志
不属于 Bug:
- 用量统计包含重试成本(这才是准确的总成本报告)
- 将跳过的文件与已分析的文件分开计数
检查 9:错误上下文与控制流
区域: 所有区域 | 严重性: 中 | 历史提交: 3+
Warden 的错误处理涉及多种错误类型,各自具有不同的重试/升级语义。历史 Bug 包括 catch 块丢失错误类型信息、重构期间将身份验证处理拆分到多个模块,以及对错误控制流的假设不正确。
危险信号:
catch (error)块将错误包装在新的Error()中,导致丢失原始类型(会破坏下游对APIError、WardenAuthenticationError等类型的instanceof检查)catch块记录error.message,却丢弃error.cause或堆栈跟踪- 身份验证错误检测在多个模块中重复实现,而不是使用
isAuthenticationError()/isAuthenticationErrorMessage() - 重新抛出错误时未保留错误链(使用
throw new Error(msg),而不是throw new Error(msg, { cause: error })) - 向 SDK 依赖项添加新的错误类型后,未更新
isRetryableError() - 错误处理假设所有错误都是
Error实例(SDK 可能抛出非Error值) - 在调用方预期会正常返回的函数中调用
setFailed()或process.exit()
安全模式:
- 将
WardenAuthenticationError作为规范的身份验证错误类型,在analyzeHunk()中抛出并在顶层捕获 isSubprocessError()优先检查错误代码,再检查消息模式(更可靠)- 错误分类函数(
isRetryableError、isAuthenticationError、isSubprocessError)集中在src/sdk/errors.ts中 - 在重试循环中跟踪
lastError,以便提供诊断上下文
不属于 Bug:
- 为非关键操作(例如清理日志文件)而有意吞掉错误的
catch块 - 在 CLI 入口的顶层调用
process.exit()
第 3 步:报告
对于每项发现:
- 文件路径和行号
- 匹配的检查项(1-9)
- 用一句话说明问题
- 触发条件:导致失败的具体条件
- 建议的修复方法(仅在修复方案明确时提供)
零项发现
如果没有触发任何检查,则不报告任何内容。不要为了证明分析合理而捏造发现项。保持沉默即表示代码不存在这些模式所指出的问题。
严重性级别
- 高: 在正常使用中会导致行为不正确、数据丢失或崩溃
- 中: 在特定条件下会触发的行为错误
- 低: 不要使用。如果置信度如此低,就不要报告。