那天下午我正在 /tmp/temp 里折腾一个不相关的东西,突然收到一封 GitGuardian 的邮件。
“GitGuardian has detected the following OpenClaw Auth Token exposed within your GitHub account.”
仓库是 StevenLi-phoenix/toolbox,一个放了两百多个单文件 HTML 小工具的公开仓库。我愣了一下——这仓库里怎么会有 token?
然后想起来了。OpenClaw 的 agent 会在工作目录里自动创建 memory/ 文件夹,把对话记录、配置笔记什么的全扔进去。有一次在 Windows 上配置 OpenClaw 节点,整个对话过程被原样记录到了 memory/2026-02-02-windows-node-setup.md 里——包括我在 PowerShell 里敲的那条带明文 token 的命令:
openclaw config set gateway.auth.token "810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb"这条命令在那个文件里出现了至少五次,因为对话来来回回调试了好几轮。每一轮 agent 都把完整命令贴了一遍。
更糟的是,TOOLS.md 里还硬编码了 Bark 推送通知的 API Key,HEARTBEAT.md 里也有。这些文件本来是 OpenClaw agent 的本地配置笔记,不该出现在公开仓库里,但 agent 创建的时候没加 .gitignore,于是一股脑全推上去了。
泄露范围
先盘点一下到底泄露了什么:
| Secret | 文件 | 出现次数 |
|---|---|---|
| OpenClaw Gateway Auth Token | memory/2026-02-02-windows-node-setup.md | 5+ 次 |
| Bark Push API Key | TOOLS.md, HEARTBEAT.md | 各 2 次 |
| Google OAuth Client ID | TOOLS.md | 1 次 |
OAuth Client ID 本身不算 secret(它是公开的 identifier),但跟其他信息放在一起也不好看。真正危险的是前两个:OpenClaw gateway token 能完全控制我的 agent 网关,Bark key 能给我的手机推送任意通知。
清理 git 历史
光从文件里删掉 token 不够。git log 里每一个历史 commit 都还留着明文。GitHub 的文档说得很清楚:仅仅 force push 新代码不会清除旧 commit,它们在服务器上还能通过 SHA 直接访问,大约 30 天后才会被垃圾回收——而且如果有 PR 或 issue 引用了这些 commit,甚至永远不会被回收。
用 git filter-repo 重写整个历史。先装工具:
brew install git-filter-repo创建替换规则文件:
810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb==>REDACTED_OPENCLAW_TOKENTwwEUMR9QCUU4jmFXnoFKK==>REDACTED_BARK_TOKEN执行替换:
git filter-repo --replace-text /tmp/replacements.txt --forceParsed 294 commitsNew history written in 0.12 seconds; now repacking/cleaning...Completely finished after 0.30 seconds.294 个 commit 全部重写,0.3 秒搞定。git filter-repo 会自动删除 origin remote(防止你不小心 push 到错误的地方),需要手动加回来。
验证 token 确实从历史中消失了:
git log --all -p -S "810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb"# (无输出,干净了)然后 force push:
git remote add origin https://github.com/StevenLi-phoenix/toolbox.gitgit fetch origingit push --force-with-lease origin master用 --force-with-lease 而不是 --force,这样如果 remote 上有别人的新 commit 不会被覆盖。虽然这个仓库只有我一个人用,但养成习惯总没错。
WARNING
git filter-repo重写历史后,所有 commit hash 都会改变。如果有人 clone 过这个仓库,他们的本地副本跟 remote 就不兼容了。对于多人协作的仓库,这是个大问题——需要通知所有人重新 clone。
第一次没删干净
Push 完以为搞定了,结果发现 TOOLS.md 里还有刚轮换过的新 Bark token——我把旧 token 替换成了新的,但文件本身还在 git 追踪里。公开仓库里不应该有任何包含 token 的配置文件。
于是第二轮清理:
git rm --cached TOOLS.md IDENTITY.mdecho "TOOLS.md" >> .gitignoreecho "IDENTITY.md" >> .gitignoreecho "memory/" >> .gitignore# ... 把所有 agent 配置文件都加进去git commit -m "fix: remove TOOLS.md/IDENTITY.md from repo, add .gitignore"git push origin master但这还不够——TOOLS.md 的历史版本里还是有 token。于是第三次跑 git filter-repo,这次直接从历史中删除整个文件:
git filter-repo --path TOOLS.md --path IDENTITY.md --path HEARTBEAT.md \ --path AGENTS.md --path SOUL.md --path USER.md \ --path MEMORY.md --path memory/ \ --invert-paths --force--invert-paths 的意思是”保留除了这些路径以外的所有东西”。这次彻底了。
教训:清理 secret 的时候,要想清楚包含 secret 的文件本身是否也应该从历史中移除,而不是只替换文件内容里的 token 字符串。
四台机器的 Token 轮换
光清 git 历史不够,泄露的 token 必须立即作废。问题是这些 token 散布在四台机器上。
OpenClaw Gateway Token(mbp)
SSH 到 mbp,找到配置文件里的旧 token:
"auth": { "mode": "token", "token": "810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb"}生成新 token 并替换:
openssl rand -hex 24# 83c434194a8871d11acf4deb76377498120748047d7bbf52
sed -i '' 's/810a4849.../83c43419.../g' ~/.openclaw/openclaw.json重启 gateway 使新 token 生效:
/usr/local/bin/openclaw gateway restartBark Push Token(本机 + mini + trackpi)
Bark token 散布的地方更多:
| 位置 | 文件 |
|---|---|
| 本机 | ~/.zshrc, ~/.claude/settings.json |
| mini | ~/.zshrc, ~/.claude/settings.json |
| trackpi | ~/trackpackage/tracker.py(硬编码在 Python 里) |
在 Bark app 里重新生成了 key,然后逐台替换。这里有个坑:macOS 的 sed -i 语法跟 Linux 不一样。
# Linux (trackpi)sed -i 's/OLD_TOKEN/NEW_TOKEN/g' file
# macOS (本机, mini)sed -i '' 's/OLD_TOKEN/NEW_TOKEN/g' file少了那个空字符串参数 '',macOS 的 sed 会把后面的 's/...' 当作备份文件后缀,然后把原文件清空。我就是这么把自己的 .zshrc 搞没的。幸好有个两个月前的备份 ~/.zshrc.bak.20260226165833,恢复了回来——但丢了两个月的改动。
CAUTION在 macOS 上用
sed -i之前,永远先备份:Terminal window cp file file.bak.$(date +%Y%m%d%H%M%S)或者直接用编辑器/脚本而不是 sed。
防护:让同样的事不再发生
清理完了,但如果不加防护,下次 agent 又会把 token 写进文件然后推上去。
mbp:全局 git pre-commit hook
mbp 跑的是 OpenClaw 而不是 Claude Code,没法用 Claude 的 hook 系统。但可以用 git 的全局 pre-commit hook:
#!/usr/bin/env bashset -euo pipefail
if git diff --cached --diff-filter=d 2>/dev/null | grep -Eiq \ 'sk-[a-zA-Z0-9_-]{20,}|token\s*[:=]\s*[\"'\'']\s*[a-f0-9]{32,}|AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}'; then echo 'BLOCKED: Staged changes appear to contain secrets.' >&2 exit 1fichmod +x ~/.git-hooks/pre-commitgit config --global core.hooksPath ~/.git-hooks这样任何仓库在 commit 时都会扫描暂存内容,检测常见的 secret 模式(AWS AKIA key、GitHub PAT、长 hex token 等)。不是万能的——短 token 或者非标准格式可能漏过——但能挡住大部分情况。
mini:Claude Code PreToolUse hook
mini 跑的是 Claude Code,可以用它的 hook 系统在 git add 或 git commit 之前拦截:
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "jq -r '.tool_input.command' | grep -Eq '\\bgit\\s+(commit|add)\\b' && { staged=$(git diff --cached ...); ... exit 2; } || exit 0" }]}原理一样:拦截 Claude 的 Bash 工具调用,如果命令是 git commit 或 git add,就扫描暂存内容。
mbp OpenClaw:修改 AGENTS.md prompt
在 OpenClaw 的系统 prompt(~/.openclaw/workspace/AGENTS.md)里加了两段硬规则:
Secrets & Credentials:禁止 commit 任何 secret,commit 前必须扫描,发现问题立即报告而不是自己偷偷 force push 修复。
Chaos Prevention:禁止创建递归目录结构(比如 toolbox/toolbox/toolbox/),禁止不经批准批量创建文件或修改系统配置。
这些是写在 prompt 里的软约束,不像 git hook 那样能真正阻止操作——但至少让 agent 知道这些事不能做。
根因:贪便宜换模型
整个过程花了一个多小时,但最大的教训不是”别把 token 推到 GitHub”——这谁都知道。
真正的根因是换模型。
这个 toolbox 仓库之前一直是 Claude Opus 4.6 在维护的。Opus 做事稳,项目结构干净,该 .gitignore 的文件会 .gitignore,不该 commit 的东西不会 commit。后来 OpenClaw 的默认模型切到了 Codex 5.4-mini——便宜,快,token 用量低。寻思着这不就是个往仓库里加 HTML 文件的简单活吗,用个便宜模型跑跑得了。
结果就出事了。
Codex 5.4 干了这些好事:创建了递归的 toolbox/toolbox/ 目录结构(仓库套仓库),把 agent 的 memory/ 对话记录、SOUL.md、AGENTS.md 这些本地配置文件全推到了公开仓库里,TOOLS.md 里硬编码了 API token 也没加 .gitignore。这些问题 Opus 不会犯——不是说 Opus 完美,而是它对项目结构和安全边界有基本的判断力。
便宜模型在简单任务上看着没问题,但在需要判断力的地方——“这个文件该不该被 git 追踪”、“这段内容里有没有 secret”、“这个目录结构合不合理”——差距就出来了。省下的模型费用,最后全花在了清理上,还搭上一个多小时的人工时间。
AI agent 生成的文件默认没有安全边界。 Agent 把对话记录(包含明文 token)写到了工作目录的 memory/ 文件夹里,而这个目录恰好在一个 git 仓库内。agent 不知道也不关心这个目录是不是 git 追踪的——它只管写文件。能力强的模型至少会注意到这一点,能力弱的模型连想都不会想。
防护的层次应该是:
.gitignore— 最基本的,agent 生成的配置文件不应该被追踪- git pre-commit hook — 第二道防线,commit 时扫描 secret 模式
- Agent prompt 约束 — 软约束,告诉 agent 不要在可能被追踪的文件里写 secret
- GitGuardian — 最后一道防线,万一前面都没挡住,至少能告诉你出事了
这次是 GitGuardian 兜底了。如果没有它的邮件,这个 token 可能还在 GitHub 上躺着。
另外一个经验:清理 secret 的时候,不要只想着”替换 token 字符串”。要退一步想,包含 token 的整个文件是否应该从仓库中移除。我第一轮只替换了 token 文本,第二轮才意识到 TOOLS.md 本身就不该在公开仓库里,第三轮才把它从 git 历史中彻底删除。三次 git filter-repo,三次 force push。本来一次就能搞定的事情。
所以结论很简单:涉及安全边界的任务,别用便宜模型。省下的钱不够你擦屁股的。