2353 字
12 分钟
一封 GitGuardian 邮件,四台机器的 Token 轮换

那天下午我正在 /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 的命令:

Terminal window
openclaw config set gateway.auth.token "810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb"

这条命令在那个文件里出现了至少五次,因为对话来来回回调试了好几轮。每一轮 agent 都把完整命令贴了一遍。

更糟的是,TOOLS.md 里还硬编码了 Bark 推送通知的 API Key,HEARTBEAT.md 里也有。这些文件本来是 OpenClaw agent 的本地配置笔记,不该出现在公开仓库里,但 agent 创建的时候没加 .gitignore,于是一股脑全推上去了。

泄露范围#

先盘点一下到底泄露了什么:

Secret文件出现次数
OpenClaw Gateway Auth Tokenmemory/2026-02-02-windows-node-setup.md5+ 次
Bark Push API KeyTOOLS.md, HEARTBEAT.md各 2 次
Google OAuth Client IDTOOLS.md1 次

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 重写整个历史。先装工具:

Terminal window
brew install git-filter-repo

创建替换规则文件:

/tmp/replacements.txt
810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb==>REDACTED_OPENCLAW_TOKEN
TwwEUMR9QCUU4jmFXnoFKK==>REDACTED_BARK_TOKEN

执行替换:

Terminal window
git filter-repo --replace-text /tmp/replacements.txt --force
Parsed 294 commits
New 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 确实从历史中消失了:

Terminal window
git log --all -p -S "810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb"
# (无输出,干净了)

然后 force push:

Terminal window
git remote add origin https://github.com/StevenLi-phoenix/toolbox.git
git fetch origin
git 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 的配置文件。

于是第二轮清理:

Terminal window
git rm --cached TOOLS.md IDENTITY.md
echo "TOOLS.md" >> .gitignore
echo "IDENTITY.md" >> .gitignore
echo "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,这次直接从历史中删除整个文件:

Terminal window
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:

~/.openclaw/openclaw.json
"auth": {
"mode": "token",
"token": "810a4849d0f1dee843d46371926fab2d7520b14a0ae156eb"
}

生成新 token 并替换:

Terminal window
openssl rand -hex 24
# 83c434194a8871d11acf4deb76377498120748047d7bbf52
sed -i '' 's/810a4849.../83c43419.../g' ~/.openclaw/openclaw.json

重启 gateway 使新 token 生效:

gui/501/ai.openclaw.gateway
/usr/local/bin/openclaw gateway restart

Bark 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 不一样。

Terminal window
# 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:

~/.git-hooks/pre-commit
#!/usr/bin/env bash
set -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 1
fi
Terminal window
chmod +x ~/.git-hooks/pre-commit
git 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 addgit commit 之前拦截:

~/.claude/settings.json (hooks.PreToolUse)
{
"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 commitgit 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.mdAGENTS.md 这些本地配置文件全推到了公开仓库里,TOOLS.md 里硬编码了 API token 也没加 .gitignore。这些问题 Opus 不会犯——不是说 Opus 完美,而是它对项目结构和安全边界有基本的判断力。

便宜模型在简单任务上看着没问题,但在需要判断力的地方——“这个文件该不该被 git 追踪”、“这段内容里有没有 secret”、“这个目录结构合不合理”——差距就出来了。省下的模型费用,最后全花在了清理上,还搭上一个多小时的人工时间。

AI agent 生成的文件默认没有安全边界。 Agent 把对话记录(包含明文 token)写到了工作目录的 memory/ 文件夹里,而这个目录恰好在一个 git 仓库内。agent 不知道也不关心这个目录是不是 git 追踪的——它只管写文件。能力强的模型至少会注意到这一点,能力弱的模型连想都不会想。

防护的层次应该是:

  1. .gitignore — 最基本的,agent 生成的配置文件不应该被追踪
  2. git pre-commit hook — 第二道防线,commit 时扫描 secret 模式
  3. Agent prompt 约束 — 软约束,告诉 agent 不要在可能被追踪的文件里写 secret
  4. GitGuardian — 最后一道防线,万一前面都没挡住,至少能告诉你出事了

这次是 GitGuardian 兜底了。如果没有它的邮件,这个 token 可能还在 GitHub 上躺着。

另外一个经验:清理 secret 的时候,不要只想着”替换 token 字符串”。要退一步想,包含 token 的整个文件是否应该从仓库中移除。我第一轮只替换了 token 文本,第二轮才意识到 TOOLS.md 本身就不该在公开仓库里,第三轮才把它从 git 历史中彻底删除。三次 git filter-repo,三次 force push。本来一次就能搞定的事情。

所以结论很简单:涉及安全边界的任务,别用便宜模型。省下的钱不够你擦屁股的。

一封 GitGuardian 邮件,四台机器的 Token 轮换
https://blog.lishuyu.top/posts/gitguardian-secret-leak-cleanup/
作者
猫猫魔女
发布于
2026-04-08
许可协议
CC BY-NC-SA 4.0