`RiskClassifier` 里 readOnlyTools 是无条件早退的(注释写明理由:Read 一个讲 `rm -rf` 的 README 不该触发审批)。但「读密钥文件」恰恰是要拦的那件事—— 共用密钥交给 AI 的契约是 `set -a; source`,值只进子进程环境;改用 Read 之后文件内容就进了 ~/.claude/projects/*.jsonl(0644、永久、每轮重发给 API)。 判断**只放在 readOnlyTools 那个分支里,不放在 classify 开头**。放开头的话, 约定用法里那条 source 命令每次都会弹审批,正常流程当场堵死。Bash 走下面的 模式匹配,`\.env\b` 已经把它定成 dangerous(记录不拦截),不动。 写清楚它挡不住什么:python -c 读、软链、base64 转一道都绕得过, 而且 hubctl 超时是 fail-open——app 没开着的时候这道闸是关的。 它是一次提醒,不是安全边界,README 和注释都按这个写。 路径常量抄在 HubIPC 里(那个模块刻意不依赖 HubProjects,hubctl 要小而快)。 抄就会漂,漂了的症状是静默的——改个文件名,闸就再也命不中而界面毫无变化。 所以加了 SensitivePathsAgreementTests,在同时看得见两个模块的测试目录里 把两边的真实路径对一遍。 625 个测试全过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
33 lines
1.5 KiB
Swift
33 lines
1.5 KiB
Swift
import Foundation
|
||
|
||
/// Hub 自己存密钥的那几个位置。
|
||
///
|
||
/// ## 为什么常量抄在这里而不是从 HubProjects 引
|
||
///
|
||
/// `HubIPC` 刻意不依赖 `HubProjects`(见 `Package.swift` 的注释):
|
||
/// `hubctl` 每次工具调用都要跑一遍,必须小而快,多链一个库就多一份启动开销。
|
||
///
|
||
/// 抄常量的代价是会漂。所以 `HubUITests/SensitivePathsAgreementTests`
|
||
/// 把两边的真实路径拿来对了一遍 —— 那个测试目录同时看得见两个模块。
|
||
/// 改了 `EnvMaterializer.defaultRoot` 或者哪个 `.dat` 的名字,那条测试会红。
|
||
public enum SensitivePaths {
|
||
|
||
/// 相对家目录的前缀。
|
||
static let prefixes = [
|
||
// 共用密钥物化出来的 .env(对应 EnvMaterializer.defaultRoot)
|
||
".vibe-foreman/env/",
|
||
// 两个库的密文本体(对应 SharedSecretStore / CredentialStore 的 defaultURL)
|
||
"Library/Application Support/claude-hub/shared-secrets.dat",
|
||
"Library/Application Support/claude-hub/credentials.dat",
|
||
]
|
||
|
||
/// 这段文本里有没有指向密钥文件的路径。
|
||
///
|
||
/// 用后缀匹配而不是拼上真实家目录再比:`~` 有没有展开、有没有走软链、
|
||
/// 是不是 `/Users/xxx` 还是 `/private/...`,取决于调用方怎么写的,
|
||
/// 一个都不能假设。而这几个前缀本身已经足够独特,误伤概率可以忽略。
|
||
public static func matches(_ text: String) -> Bool {
|
||
prefixes.contains { text.contains($0) }
|
||
}
|
||
}
|