# 代理工具临时文件：查找并清理秘密残留

代理工具的临时文件需要像源代码仓库一样接受检查。编程代理可能先创建请求正文，运行命令，开启详细日志后重试，把输出复制到缓存，再让原始工作目录看起来干干净净。敏感材料仍然留在机器上，而且常常位于没人纳入审查的位置。

很多人误以为，只有代理把秘密打印到聊天记录里才算泄露。更常见的情况是，某个辅助程序为了发送请求，把带有凭据的请求复制到临时文件；或者有人上周打开了调试模式，失败命令因此被写入日志。清理很重要，但预防更重要。如果另一个进程可以代为执行操作，就不要把原始凭据交给代理。

## 代理工具会在工作树之外创建副本

即使项目目录里看不到痕迹，一次代理运行也可能在多个层面留下数据。工作树只是一个写入位置，开发者经常检查它，只是因为熟悉。代理、运行时、Shell、包管理器、编辑器、终端和操作系统都有各自的写入位置。

先把材料分成三类。载荷文件包含代理准备发送的内容，例如 JSON 请求正文、SQL 批次、提示词导出、补丁文件或 SSH 配置。输出文件包含远程系统返回的内容，例如 API 响应、命令输出、数据库导出和错误页面。诊断材料包含周边证据，例如命令参数、环境信息、堆栈跟踪、重试记录和追踪信息。

这三类材料都可能包含敏感数据。脚本可能在发送前注入令牌，因此载荷里会有令牌。输出可能包含客户记录或部署秘密。诊断日志可能把两者写在同一行，这也是调试日志有时比失败命令本身造成更大损害的原因。

实用的清单应包括：

- macOS 的进程临时目录和 `/private/tmp`。
- `~/Library` 下的应用支持目录、缓存目录和日志目录。
- Shell 历史记录、终端回滚导出和命令包装器。
- 项目本地的 `.cache`、`tmp`、`logs`、`.agent` 和测试 fixture 目录。
- 容器可写层、绑定挂载、CI 工作区和上传的构建构件。

不要假设代理只会使用一个固定目录。不同版本、插件、语言运行时和错误路径可能作出不同选择。工具可能使用 `TMPDIR` 指定的目录，工具调用的辅助程序可能使用 `/tmp`，库也可能把缓存放在当前项目下。最可靠的答案来自观察你自己的运行过程。

命令输出也是如此。像 `command > result.txt` 这样的 Shell 重定向很明显，终端复用器日志、包管理器调试归档、HTTP 客户端追踪文件和代理修改文档后写出的编辑器恢复文件就不那么明显。如果代理可以调用多个工具，在测试之前应假设每个工具都有独立的保留习惯。

## 清理前先绘制真实的写入位置

猜错路径，就无法清理它。使用无害且独特的标记运行代理，然后在运行可能写入的位置搜索标记。使用看起来足以通过相同代码路径的假数据，但绝不要在这项测试中使用生产令牌。

在 macOS 上，先记录启动代理的 Shell 所继承的临时目录：

```sh
echo "TMPDIR=$TMPDIR"
ls -ld "$TMPDIR" /private/tmp /tmp
```

macOS 通常会为每个用户提供 `/var/folders` 下的目录，而 `/tmp` 会指向 `/private/tmp`。生成的具体路径不是安全边界，也可能变化。记录实际值，不要在清理脚本中写死路径。

创建一个容易搜索的标记，再让代理执行一项具有代表性的操作，使标记经过请求正文、命令参数和命令输出。下面的例子刻意使用非秘密字符串：

```sh
export AGENT_TRACE_MARKER='TEMP-PAYLOAD-CANARY-9f2a7c'
printf '%s\n' "$AGENT_TRACE_MARKER" > /tmp/agent-canary.txt
```

运行结束后，搜索可能属于当前用户的位置。`grep` 可能遇到二进制文件和权限错误，因此应把它当作发现工具，而不是证明不存在任何文件的依据。

```sh
grep -RIl --exclude='*.sqlite*' \\
  'TEMP-PAYLOAD-CANARY-9f2a7c' \\
  "$TMPDIR" /private/tmp \\
  "$HOME/Library/Caches" \\
  "$HOME/Library/Logs" \\
  "$HOME/Library/Application Support" 2>/dev/null
```

输出应是一列文件路径。对每个路径回答四个问题：哪个进程写入了它，它包含哪类内容，谁可以读取它，以及它什么时候消失。如果无法把文件与某个进程联系起来，检查修改时间，再用新的标记重复测试。不要先删除无法解释的文件，否则可能抹掉能说明哪个组件需要重新配置的线索。

如果文件只短暂出现，可以使用 `fs_usage`。代理运行时，它能显示进程产生的文件系统活动：

```sh
sudo fs_usage -w -f filesystem | grep -iE 'agent|tmp|cache|log'
```

这条命令会产生大量输出。只在短时间测试中运行它，把观察到的路径保存到共享项目目录之外，完成后停止命令。一个进程可能在一秒内写入并删除文件，之后的目录列表自然看不到它，但内容可能已经进入备份、监视器或其他日志收集器。

## 凭据通常在构建请求时泄露

最危险的临时文件往往在网络调用之前创建。许多脚本会把请求构建在文件中，因为在 Shell 中引用 JSON 很麻烦。文件开始时可能没有问题，后来有人为了让请求成功加入 `Authorization` 字段、Cookie 或完整连接字符串。它就这样意外变成了持久的秘密容器。

避免这种模式：

```sh
cat > /tmp/request.json <<'EOF'
{"endpoint":"https://api.example.invalid/export","token":"$PRODUCTION_TOKEN"}
EOF
```

上面的单引号 heredoc 不会展开变量，看起来似乎安全。后续编辑却可能更改分隔符，或采用另一种构造方式。更重要的是，这种设计仍然让人习惯于把凭据放进请求构件。完整请求的调试转储会把它暴露出来。

在协议允许时，把凭据放入传输层，并确保传输层不会记录标头。HTTP 授权标头比把令牌放进查询字符串更好，但它并非天然安全。详细日志客户端、代理设置、异常处理器和自定义重试代码仍可能记录它们。

HTTP Semantics 规范 RFC 9110 建议用户代理不要在 Referer 标头中发送包含敏感信息的 URI。这一提醒说明了一个更广泛的事实：URL 的传播范围比人们想象的更远。它们可能进入访问日志、浏览器历史记录、复制的终端命令、支持工单和分析系统。除非协议没有其他选择且凭据有效期极短，否则不要把承载令牌、权限范围很大的签名 URL、密码或数据库连接字符串放进 URL。

Shell 参数同样需要谨慎。在类 Unix 系统中，根据权限和平台设置，其他本地进程可能观察到参数。参数还可能进入 Shell 历史记录、任务运行器日志和代理工具记录。环境变量可以减少部分路径，却会带来继承给子进程和诊断报告等新路径。两者都不是安全保险库。

更好的边界很简单：代理只通过指定目标和非敏感输入来请求操作。独立的凭据持有者在调用前一刻加入身份验证。代理收到的是响应或经过脱敏的错误，而不是用于获得响应的凭据。

Sallyport 在 HTTP 和 SSH 操作中遵循这一边界：凭据留在加密保险库中，代理请求本地应用执行操作。这样可以将原始秘密移出代理上下文，但不会让响应正文或代理创建的调试文件自动变得无害。你仍然需要控制操作返回什么，以及代理把内容写到哪里。

## 调试模式会把普通失败变成秘密记录

排查损坏的集成时，调试日志很有用。它的设计目标也是保存普通日志会省略的证据，通常包括标头、完整请求和响应正文、命令行、从环境读取的设置、重试状态和堆栈跟踪。

常见错误是在问题发生一次后忘记关闭宽泛的调试开关。几周后，它可能作用于无关任务，而那次任务使用了真实权限执行导出或部署。生成的文件可能位于缓存目录下，没人知道该目录属于这个工具。

把诊断信息视为一种生命周期很短且有明确期限的数据类别。启用追踪前，先决定它必须回答什么问题。如果要确认 DNS 是否解析，只收集解析器输出。如果要确认服务器是否拒绝某个 JSON 字段，记录状态码和经过脱敏的响应片段。完整的网络记录应当是例外，因为它会保存解决问题并不需要的材料。

在写入端实现脱敏，不要事后再处理。只搜索 `Authorization:` 的清理任务会漏掉自定义标头、JSON 字段、URL 参数、多行值、Base64 数据块以及包含秘密的响应内容。日志收集器或备份服务一旦复制了未脱敏文件，之后再清理原文件作用有限。

安全的诊断包装器应有一个允许写入的字段清单。例如，记录 HTTP 方法、不含查询参数的主机和路径、状态码、耗时、响应字节数及请求标识符。不要记录所有标头，然后声称会把危险字段遮住。凭据格式变化的速度比清理脚本更新得快。

有些工具会在执行前显示命令。显示命令不等于记录命令的实际环境，也不等于数据包追踪。要确认工具保存的是哪一种表示形式。工具可能会对控制台显示进行脱敏，却让详细日志保持原样。

要主动检查失败路径。中途取消请求，发送格式错误的 JSON，使用合成凭据触发身份验证失败，让请求超时以触发重试。这些路径可能创建成功调用不会创建的临时请求文件和异常记录，也可能是攻击者诱使系统泄露更多信息的路径。

## macOS 上的删除需要隔离，而不是神奇的粉碎

在现代 macOS 存储设备上，安全删除并不等于反复覆盖文件名。SSD 磨损均衡、写时复制、快照、云同步和备份意味着软件无法可靠保证覆盖触及了每个物理残留。运行粉碎工具的旧习惯可能让人以为工作完成，却没有处理真正重要的副本。

使用删除来移除仍然存在且可访问的文件。使用隔离来限制敏感材料最初可以出现的位置。如果怀疑已经暴露，则使用加密和凭据轮换。

对于代理专用的临时目录，在运行前用严格权限创建，运行后删除：

```sh
run_dir="$(mktemp -d "${TMPDIR%/}/agent-run.XXXXXX")" || exit 1
chmod 700 "$run_dir"
trap 'rm -rf "$run_dir"' EXIT HUP INT TERM
export TMPDIR="$run_dir"

# launch the agent from this same shell
# agent-command
```

这样可以为本次运行提供已知的临时区域，并确保普通退出路径会删除它。但它不能强迫每个依赖都遵守 `TMPDIR`，也不能删除已经复制到其他目录的内容。因此必须先做发现测试。

避免使用 `rm -rf /tmp/*` 之类的宽泛清理命令。它们可能破坏其他进程、删除调查所需的材料，并让人错误地以为 `/tmp` 是唯一需要检查的位置。只删除启动器创建的目录，以及清理代码能够证明归它所有的名称。

如果秘密可能已经逃出原目录，应在开始漫长的清理工作前轮换或撤销它。复制到未知日志中的有效 API 令牌就是当前的访问路径。与它授予的权限相比，文件名没那么紧急。接着检查备份工具、共享磁盘、CI 构件、日志聚合系统和终端搜索系统中的下游副本。只删除原件而不处理副本，暴露仍然存在。

加密本地存储可以降低设备丢失带来的风险，但不能防止同一解锁用户账户下运行的其他进程访问数据。文件权限仍然重要。目录模式 `700` 表示其他本地账户不应浏览它，但无法阻止已经以你的身份运行的代理进程，也无法阻止你授权读取主目录的同步客户端。

## 凭据边界可以移除最危险的载荷

清理能力有上限。如果代理在提示词、环境变量、配置文件或命令输出中获得生产令牌，它就可以把令牌放入自己能够写入的任何文件。你可以减轻后果，却无法靠细致的日常清理让这种架构变得安全。

将秘密的所有权留在代理进程之外。代理应表达这样的意图：「将这个部署请求发送到这个获准的终端」或「使用这个命名连接运行 SSH 命令」。本地凭据持有者负责决定是否授权、注入凭据、执行操作并记录结果。代理不需要占位令牌，因为它根本不需要令牌本身。

这也让临时文件审查不再模糊。你可以检查代理临时目录中的用户输入、生成代码和返回数据，而不必假设每个文件都可能包含代理能够使用的所有生产秘密。

固定的批准模型比大量自定义规则更有操作优势。事件发生时，人们可以解释它。Sallyport 会保持保险库锁定，直到本地身份验证将其打开；新的代理进程第一次执行操作时需要授权；对选定凭据的每次使用也可以要求批准。这是对权限的明确控制，而不是试图猜测某条生成命令是否可疑。

不要把操作授权和数据最小化混为一谈。获准的调用仍可能返回敏感响应正文，代理可能把响应写入项目文件、缓存或记录中。应根据任务所需的最小结果设计响应。如果任务只需要部署标识符和状态，就不要返回完整配置文档。如果 API 支持服务端筛选，就使用它。

SSH 尤其需要注意，因为远程命令输出没有固定边界。读取配置文件的命令、出错后打印环境变量的命令，或运行详细部署工具，都可能把秘密发送回代理。把 SSH 输出视为需要指定目的地、保留期限和审查规则的数据。凭据受到保护，并不意味着返回的输出也安全。

## 为每次运行指定负责人、目录和期限

当清理策略能把文件关联到一次具体运行时，它才真正有效。通用的夜间脚本无法判断临时目录属于仍在运行的进程、值得调查的失败任务，还是无关应用。代理启动器可以做到。

在临时目录名称中加入运行标识符，记录开始时间，并在临时目录之外保存一份只包含非敏感元数据的小清单。清单应说明启动器创建了哪个目录、哪个进程拥有它、何时到期以及运行是否完成。不要把命令参数、请求正文或环境值放入清单。

实用的生命周期包括四个动作：

1. 在代理启动前创建私有临时目录。
2. 为代理和你控制的辅助程序设置临时路径环境变量。
3. 正常退出时删除目录，并将清单标记为完成。
4. 让计划任务标记超过规定期限的遗留目录，交由人工检查。

计划任务应标记未完成运行的目录，而不是自动删除它们。进程可能仍在写入，失败的部署也可能需要保留证据。人工确认目录已被遗弃且事件调查不再需要后，再依据同一所有权规则删除它。

不要把临时目录放在代码仓库中。项目搜索、版本控制状态命令、IDE 索引、文件监视器和备份客户端都会关注仓库。用户控制的临时位置下的同级目录通常更容易排除在开发工具之外，也更容易作为一个整体销毁。

谨慎对待由代理自身启动的自动清理。能够选择任意清理路径的代理可能删除源文件或证据。启动器应自行构建路径并保存状态，只在路径规范化后删除符合其生成命名模式的路径。即使没有加入自主文本生成，Shell 插值也已经造成足够多的事故。

目标不是完全不保留任何内容。你需要足够的运行证据来回答哪个运行发起了请求以及是否成功。将这类记录与原始载荷和完整输出分开，并让字段保持有意的简单。

## 日志和备份需要明确的保留决定

临时文件一旦被另一个系统复制，就变成了保留记录。备份软件、云同步文件夹、终端安全工具、崩溃报告、CI 构件上传和集中式日志都可能延长它的生命周期。原始路径可能已经消失，但有用的副本仍在别处。

列出所有能够读取代理运行目录的进程。在开发机器上，通常包括备份客户端、编辑器索引器、源代码管理界面、终端记录器和恶意软件扫描器。有些副本是有价值的，关键是主动选择它们并设置保留期限，而不是等令牌出现在恢复归档后才发现它们。

不要把原始诊断输出放在会自动同步的目录中，包括桌面文件夹、共享项目文件夹和 CI 客户端会打包为构件的工作区。如果工具必须生成大型响应供审查，请将其存放在权限受限的私有目录，指定到期时间，并在工具支持排除规则时将其排除在常规同步之外。

哈希链审计记录与调试转储不同。审计记录应回答谁请求了操作、何时运行以及发生了哪类结果，而不重复秘密。Sallyport 从加密审计日志生成 Sessions 和 Activity 日志，`sp audit verify` 可以在离线状态下验证哈希链，无需保险库密钥。这样可以保留代理活动证据，而不必把每个原始载荷都当作审计要求。

保留策略必须覆盖例外情况。发生事件时，不要让常规清理计时器销毁理解经过所需的证据。限制访问，有意保存相关文件并记录位置。然后轮换受影响的凭据，在事件流程不再需要这些材料后删除它们。事件保留不是把所有普通代理输出永久存储的借口。

修改策略后也要检查备份。新排除的临时目录只影响未来的备份运行。现有快照可能仍包含旧材料，直到备份提供商的保留期限结束。应在暴露响应中记录这一事实，不要假装一个 `rm` 命令可以改写历史。

## 像好奇的本地攻击者一样测试清理

从未接受过搜索测试的清理策略只是承诺，还不是控制措施。使用类似于你担心泄露的字符串的虚假金丝雀标记测试，然后像另一个本地进程试图寻找它那样检查机器。

先在正常条件下测试。将金丝雀标记放入输入文件、请求字段和模拟命令输出。让代理运行完成，等待清理钩子执行，再搜索临时目录、缓存、日志、项目目录、Shell 历史记录和可能的支持目录。记录每次命中并解释原因。

接着测试不顺利的情况。强制进程崩溃，在网络请求期间取消运行，打开工具支持的最详细日志，从 IDE 而不是终端启动，使用带有主机绑定挂载的容器。每种变化都可能把输出导向不同位置。

按标记搜索，不要按文件名搜索。复制的载荷可能获得随机文件名，进入 SQLite 数据库，或被压缩进归档。如果二进制文件或数据库包含金丝雀标记，应找出写入者，并决定需要配置、排除、脱敏还是改用不同的执行边界。

保留一份简短的测试记录，包含代理版本、操作系统版本、启用的插件、执行过的命令、发现的路径和清理结果。加入能够执行命令或发送请求的新工具时更新记录。这项工作很普通，却能捕捉源代码安全审查容易漏掉的回归问题。

今天就用无害标记做第一次测试。如果它出现在意料之外的位置，先修复写入者，再编写更复杂的删除脚本。最干净的临时文件，是从未接收凭据或敏感响应的文件。
