# 剪贴板凭据暴露：别再把秘密交给代理

复制凭据时，整个动作不到一秒，很容易让人觉得它只是暂时的。但事实并非如此。剪贴板历史、终端回滚内容、代理会话记录、聊天同步和 Shell 历史，可能把这一个动作变成多份独立副本，而每份副本都有不同的访问规则和保留期限。

开发者常常只关注密码管理器是否对静态秘密加密。这当然重要，但真正危险的时刻发生在复制之后。明文一旦离开密码管理器，就可能进入那些会记忆、索引、同步或重放你粘贴内容的工具。自主代理会让这种错误更容易重复，因为它鼓励人们编写很长、很详细的提示词和命令输出，而人们往往把这些内容当成用完即弃的东西。

## 剪贴板历史会创建第二套存储系统

如果剪贴板历史在产生文本的应用忘记内容之后仍然保留文本，它就会变成第二个凭据存储。操作系统剪贴板本身就是共享状态。历史功能会延长这种状态的生命周期，而且通常还会让旧条目可以被搜索。

这和应用在你粘贴的瞬间读取剪贴板，是两种不同的风险。开发者可能会在粘贴前后留意复制的值，之后再清空剪贴板。历史数据库却可能在清空操作之前就已经保留了同一个值。如果你先后复制了令牌、秘密标头和完整命令，它还可能保留多个版本。

在 macOS 上，`pbcopy` 会将标准输入写入 pasteboard，`pbpaste` 则会将其读回。正因为方便，脚本和调试习惯很容易把秘密带进剪贴板。可以在一次性终端中运行下面这个无害测试：

```sh
printf '%s' 'CLIPBOARD-TEST-7f3c' | pbcopy
pbpaste
```

预期输出是：

```text
CLIPBOARD-TEST-7f3c
```

现在打开你使用的所有剪贴板历史功能，搜索 `CLIPBOARD-TEST-7f3c`。在任何共享剪贴板的设备上也进行同样的操作。这个测试不能证明某个工具会保存所有类型的剪贴板内容，但它可以显示：在原始操作结束后，你平时的文本路径是否还会留下条目。

Apple 将 Universal Clipboard 记录为一项连续互通功能。它允许用户在一台 Apple 设备上复制，再粘贴到另一台已登录同一 Apple Account 且满足连续互通条件的设备上。这份文档描述的是一种有用的传输机制，不是秘密处理边界。如果凭据移动到了另一台设备，那么那台设备上的本地应用、备份和会话状态都需要纳入暴露范围。

人们通常会说：“我只是在本地复制了它。”本地不是保留策略。本地剪贴板管理器可能持续运行，为搜索保留数据库，将条目纳入备份，或者把它们交给同步提供商。本地机器上还可能存在其他用户会话、远程管理软件、屏幕录制、支持工具和开发实用程序。不要因此产生对所有本地软件的模糊恐惧。应当明确哪些程序可以读取剪贴板、会保留多久，以及是否会把记录发送到其他地方。

复制的秘密并不一定已经泄露，但它已经越过了一个你无法像描述保险库那样有把握描述的边界。这应该改变你的应对方式。

## 代理提示词是凭据分发渠道

把令牌粘贴到代理提示词中，会把它分发到比请求本身所需更多的地方。代理可以读取它，但存储会话历史的客户端、准备后续轮次上下文的构建器、客户端周围的日志，以及任何能够查看最终会话记录的人，也可能读取它。

提示词还会诱发一种尤其糟糕的模式：因为觉得高效，就复制完整的工作请求。开发者粘贴 bearer 令牌、URL、客户标识符和 `curl` 命令，然后要求代理进行调整。代理可能在回答中引用这条命令，开发者又可能把回答复制回终端。这样一来，一个秘密就可能出现在原始剪贴板记录、提示词、响应、终端回滚内容，以及 Shell 历史文件中。

不要在粘贴之后才试图用一条脱敏指令解决问题。代理无法忘记已经接收的上下文，指令也不会删除本地或远程记录。请求结构，而不是凭据。

即使不暴露任何敏感信息，下面这样的提示词也足以给代理提供明确方向：

```text
Call the staging inventory API endpoint GET /v1/items.
Use the credential named staging-inventory.
Return the status code and the count of items.
Do not print request headers or authentication material.
```

这样的提示词把操作请求和授权材料分开了。它还告诉代理应该返回什么结果，从而避免代理为了让人放心而倾倒完整请求或响应的常见习惯。

这个区别经常被混淆：秘密引用不是秘密值。`staging-inventory`、`PAYMENTS_TOKEN` 或“使用我的生产部署凭据”只有在代理无法将它们解析成明文时，才可能是安全引用。如果本地配置文件展开了引用，再把值反馈给代理，那只是用间接方式替代了复制。

应把提示词文本视为可能被保留、搜索、审查、导出，或意外放进错误报告的内容。同样的标准也适用于代理工具输出。返回请求标头的工具、回显 URL 令牌的授权失败信息，或者详细调试输出，都可能在无人刻意粘贴的情况下，把凭据带进下一条提示词。

## 终端便利功能会留下多份副本

带有明文秘密的 Shell 命令，可能通过比剪贴板历史更多的路径泄露。Shell 可能把它保存到历史记录中，终端可能保留回滚内容，终端复用器可能把它写进窗格日志，录制器可能捕获它。在某些系统上，只要拥有足够权限，其他本地进程还可能看到命令参数。

因此，下面这条熟悉的命令不应该成为默认做法：

```sh
curl -H 'Authorization: Bearer eyJ...' https://api.example.test/v1/items
```

输入或粘贴时，令牌会直接可见，还可能进入剪贴板，并持续存在于 Shell 历史中。用环境变量替换令牌可以把它从命令行中移除，但并不会让它从进程环境中消失：

```sh
curl -H "Authorization: Bearer $INVENTORY_TOKEN" https://api.example.test/v1/items
```

只有在你能控制 `INVENTORY_TOKEN` 如何进入环境、哪些子进程会继承它，以及诊断信息是否会打印它时，这才算改进。不要把 `export INVENTORY_TOKEN=...` 粘贴到交互式 Shell 后就以为问题解决了。你可能只是把明文值提前一个命令放进了历史记录。

手动操作时，交互式提示通常更安全，因为输入不会成为命令本身的一部分。小型脚本可以在不回显的情况下读取令牌：

```sh
#!/bin/sh
printf 'Inventory token: ' \u003e\u00262
stty -echo
IFS= read -r token
stty echo
printf '\\n' \u003e\u00262
curl -sS -H "Authorization: Bearer $token" https://api.example.test/v1/items
unset token
```

这样可以防止秘密出现在输入的命令和终端的正常显示中。但它不会把 Shell 脚本变成保险库。进程仍会在内存中持有该值，`curl` 会接收标头，详细模式或代理日志仍可能泄露它。把这种方式用于一次短暂的手动恢复任务，不要把它当作永久集成方案。

更好的设计是让凭据留在命令路径之外。让具备凭据能力的组件发出请求，只返回开发者或代理真正需要的数据。如果任务是“告诉我部署 X 是否完成”，结果应该是状态和时间戳，而不是完整的已认证 HTTP 交换内容。

## 共享剪贴板工具会悄悄扩大受众

共享剪贴板工具不适合保存凭据，因为共享会把本地副本变成传递机制。暴露范围可能包括同事的桌面客户端、浏览器扩展、聊天集成、远程工作区，或者一台你忘记仍处于登录状态的设备。

开发者往往根据工具的用途来判断风险。共享剪贴板本来是为了帮助团队快速传递片段，所以它看起来像一个工作渠道。凭据并不在意这个渠道看起来是否专业。如果每个参与者之后都能取回某条记录，你就等于把秘密的访问权交给了每个参与者。

临时事故频道尤其尴尬。有人需要 API 令牌来诊断生产故障，同事说：“把它放进共享剪贴板，我用完就删除。”不要这样做。接收者可能把它粘贴进自己的 Shell 历史，服务可能在删除之前就记录了条目，本地同步客户端还可能把它下载到多台设备。你无法从自己的机器验证所有副本都已经删除。

发送引用，并建立一条经过批准的凭据使用路径。如果人必须接收秘密，请使用组织指定的、带访问控制和过期时间的秘密共享方式。如果没有这样的方式，那么创建一个权限范围严格受限的新凭据，并在事故后撤销它，通常也比把协作工具当作秘密渠道更稳妥。

剪贴板共享还会造成一种更隐蔽的失败：开发者先在本地复制秘密，之后才启用同步、安装历史工具或登录第二台设备。旧记录可能因此变得可以被新的设备访问。在处理敏感工作前检查保留和同步设置，但也要假设过去的复制操作需要单独调查。

## 清空剪贴板不会抹去痕迹

清空当前剪贴板，只会替换当前剪贴板的内容。它不能保证从历史数据库、同步记录、终端、代理会话或目标应用自己的日志中移除内容。

意外复制后仍然应该清空当前剪贴板，因为这能减少进一步的随手暴露。在 macOS 上，下面的命令会用空字符串替换明文剪贴板内容：

```sh
printf '' | pbcopy
```

不要把这个操作报告为补救措施。它属于遏制。同样，剪贴板管理器中可见的“删除”按钮也不等于彻底删除。它可能只是从用户界面移除了记录，而备份、同步副本、索引搜索数据或另一台端点设备仍然保留着它。

把意外复制当作一次小型安全事件处理。正确响应取决于凭据的权限范围，但顺序很重要：

1. 停止使用暴露的凭据，并在签发方支持时撤销或轮换它。
2. 清空当前剪贴板，并在你控制的每台设备上删除已知的历史条目。
3. 搜索可能的目的地：代理聊天、终端历史、终端日志、Shell 脚本、笔记、问题评论和代码仓库文件。
4. 检查凭据所属服务的活动记录，寻找你无法识别的操作。
5. 记录发生了什么，并修复那个让粘贴看起来必不可少的工作流。

人们有时会因为无法证明第三方读取过条目而拒绝轮换凭据。在故障期间，这种想法可以理解，但证明标准并不正确。你知道秘密已经进入了预期控制范围之外的存储位置或渠道。轮换成本应该根据凭据的权限和有效期来衡量，而不是根据你能否证明它已被盗来决定。

不要盲目轮换，然后把未撤销的旧令牌留在命令历史里。确认旧凭据已经无法使用。如果提供商无法撤销单个值，就通过更改上级秘密或访问策略来缩短暴露窗口，并记录这一限制，供下一次事故参考。

## 密码管理器能减少复制，但无法消除风险

密码管理器在存储问题上做得很好：它们可以加密保存秘密，并控制检索权限。但它们无法控制应用把值粘贴到提示词、终端、表单或剪贴板历史之后会发生什么。

许多密码管理器提供剪贴板清除超时。请使用它。它可以限制活动剪贴板携带明文的时间，但无法可靠清除另一个程序的历史条目、同步记录，或已经粘贴到其他应用中的文本。这个功能有助于避免之后误粘贴，但不等于允许把复制作为广泛使用的工作流。

更安全的做法是，只对真正需要明文且能够保护明文的目标使用密码管理器集成。把秘密存储在本地工作区文件中的 API 客户端，往往比看起来更糟。浏览器表单可能因自动填充错误而泄露。终端命令通常是最差的地方，因为出问题时，开发者经常会把同一条命令粘贴到工单和聊天中。

对于人必须输入登录页面的密码，和用于 API 或 SSH 调用的机器凭据，应采用不同的判断方式。人类密码可能没有比受控输入更好的替代方案。机器凭据通常应该置于操作边界之后，这样代理和人都不必把它作为文本来回传递。

这个区别可以帮助团队避免“永远不要复制秘密”这种没有实际帮助的规则。有时人确实必须复制恢复代码。更有用的规则是：不要把秘密复制到会记录、同步、解释或重新分发文本的系统中，除非该系统明确获准保存这个秘密。

## 凭据注入胜过提示词级授权

凭据注入比提示词级授权更安全，因为代理请求的是操作，而不是接收授权它执行操作的材料。代理可以说“使用凭据 X 执行这个 HTTPS 请求”，由独立的本地组件提供授权标头并返回经过筛选的结果。

这种架构限制了代理工作流中最危险的失败模式：代理只要把凭据回显到文件、响应、提交消息或后续提示词中，就可能泄露它。如果代理从未接收到这个值，它就无法意外打印出来。当然，代理仍可能滥用授予它的权限，因此操作本身仍需要审批和审计控制。

SSH 也需要同样的处理方式。把私钥复制到代理上下文中是不可接受的。把私钥复制进终端 heredoc，只是稍微没那么糟。正确的 SSH 路径会把私钥留在受保护的存储中，在本地完成签名或连接设置，并向调用方返回命令输出，而不是密钥材料。

Sallyport 对 HTTP 和 SSH 都采用这种模式：它的保险库存放 API 和 SSH 凭据，代理通过捆绑的 MCP shim 请求操作，而不是接收明文凭据。它的保险库锁、会话批准以及可选的逐次凭据使用批准，会直接控制操作，而不是要求开发者编写策略规则。

不要把这和网络代理或通用规则引擎混为一谈。操作网关无法修复一个本来就不该允许已授权代理发出的请求。它可以让授权过程清晰可见，在预期边界要求人工决定，并让秘密远离提示词和剪贴板路径。

## 审批应该描述操作，而不是显示秘密

审批界面应该说明谁请求了操作、想使用哪项凭据权限，以及将要执行什么操作。它不应该要求人查看或比对秘密本身。

许多自制包装器正是在这里失败的。它们把令牌放在配置文件中，然后打印完全展开的 `curl` 命令供审批。开发者避免了把令牌复制到代理提示词中，却在审批对话框及其日志里暴露了令牌。安全的审批记录可以显示 `credential: staging-inventory`、`method: GET`、`host: api.example.test` 和 `path: /v1/items`，没有理由打印授权标头。

如果每个无害的读取请求都产生一个含糊的对话框，审批疲劳就是设计失败。信息无法帮助人做决定时，人们就会直接点击模糊的对话框。有效的决定请求应该显示请求进程的代码签名身份，区分新进程和已经批准的进程，并说明当前操作是否会使用标记为每次都需批准的凭据。

操作结果也应保持狭窄。例如，部署状态调用可以返回：

```json
{"deployment":"api-472","state":"completed","finished_at":"2025-04-17T11:26:00Z"}
```

它不应该返回请求标头、包含无关客户数据的完整响应体，或会促使代理在后续上下文中重复秘密的调试转储。筛选输出不是装饰性的工作，它限制了下一步会被复制的内容。

## 审计轨迹应该回答代理是否执行过操作

审计轨迹需要区分一次代理运行和一次具体的凭据调用。会话记录告诉你哪个进程获得了权限，并让你能够撤销该次运行。调用记录告诉你授权后实际做了什么。一条记录无法清晰回答这两个问题。

日志也不能变成另一个秘密保险库。为了方便取证，完整记录请求很有诱惑力，尤其是在开发阶段。不要记录授权标头、原始 Cookie、私钥或携带凭据的请求正文。记录凭据引用、目标、方法、路径、结果、时间、进程身份和审批决定。如果目标服务提供请求标识符，也应记录下来。

当代理可以无人值守地执行操作时，防篡改证据很重要。如果进程可以事后改写日志，日志就无法解决“究竟发生了什么”的争议。哈希链记录可以让审计人员验证条目是否被删除或修改，而无需暴露底层凭据材料。

Sallyport 会从加密的哈希链审计日志中分别生成会话日志和调用日志，`sp audit verify` 可以离线检查链条，无需保险库密钥。这比终端记录更适合作为事故证据，因为它记录了已授权的操作，却不会把每个复制过的字符串都当作值得永久保存的证据。

发生复制秘密的事故时，可以用审计轨迹回答具体问题：哪个代理进程运行过？它访问了哪些目标？是否尝试过写入？在下一次调用之前，会话是否已经被撤销？这些答案有助于确定事件范围。但它们无法证明没有剪贴板读取程序看到原始值，因此轮换凭据仍然属于响应措施。

## 本周就移除这个工作流

把明文凭据从团队当作普通文本使用的路径中移除。先从复制粘贴最自然的地方开始：代理提示词、终端命令、共享剪贴板工具、问题评论和团队聊天草稿。

使用无害标记，例如 `CLIPBOARD-TEST-7f3c`，进行一次简短的桌面演练。复制一次，然后搜索团队实际使用的历史工具、终端记录、代理会话和同步设备。与其争论笼统的安全建议，不如直接找出环境中真正重要的传播路径。

接着，让安全路径比旧路径更省事。给代理提供接受凭据引用的操作接口，让会话审批易于理解，对敏感凭据保留逐次审批，并且只返回继续工作所需的结果。如果开发者必须暴露明文才能完成例行自动化，说明工作流仍然存在漏洞。

秘密应该把有用的生命周期花在受保护的存储中，以及实际使用它的进程里。它不应该因为复制很方便，就绕道经过剪贴板。
