github-sensitive-data-cleanup
Scan and remove sensitive data (secrets, API keys, private domains/IPs, PII) from GitHub repository history. Use this skill whenever the user says "scan sensitive data", "clean git history", "remove secrets from repo", "sanitize GitHub history", "清理敏感数据", "历史重写", "force push", "泄露", or needs to repair a public repo after accidental secret/private context leakage. Also use before any force push to a public repository to verify visibility, backup, and scan results.
1实现原理 · 为什么它能做到
整个 skill 是「流程编排(SKILL.md)+ 四个可执行 Python 脚本 + 三份参考文档」的组合;SKILL.md 只给步骤与纪律,机械动作全部委托给 scripts/ 下的脚本,脚本再用 subprocess 调 git / gitleaks / git-filter-repo / gh。
The bundled scripts automate the mechanical parts:
扫描是四层设计:Layer 1 gitleaks(密钥/token)、Layer 2 自定义正则(内网 IP、手机号、repo 专属 PII)、Layer 3 从用户私有 gitleaks 配置提取的私有域名/IP 规则 + 可选 identities 文件、Layer 4 由 agent 人工执行的 AI 语义审查。脚本自身只做 Layer 1~3,Layer 4 只是「打标记要求人工做」。
Layer 4: AI semantic review flag (must be performed manually by an agent)
「扫全历史」不靠 gitleaks 单独完成:自定义层与 Layer 3 层用 `git rev-list --all` 取全部 commit,再分批(batch_size=500)喂给 `git grep --perl-regexp`,逐批收集命中的 commit hash——目的是绕开命令行长度上限并避免只扫最近 N 个提交。
and to ensure the entire history is searched (not just the newest N).
私有规则不进公开仓库:Layer 3 不复制任何私有域名清单,而是运行时从用户自己的 gitleaks TOML 里只抽两条规则 ID(private-domain-context / private-ip-context)的 regex,外加一个可选的逐行 identities 文件(每行 re.escape 后当字面量)。
LAYER3_RULE_IDS = ["private-domain-context", "private-ip-context"]
重写前强制备份门禁:先 a) 确认 git-filter-repo 在 PATH 且可执行,b) 工作树必须干净(`git status --short` 有任何输出即 abort),c) `git bundle create --all` 备份并立刻 `git bundle verify`,任一步失败即退出且不进入重写。
raise RuntimeError(f"Backup verification failed: {verify.stderr}")
重写本体是 `git-filter-repo --force --replace-text <file>`,可选追加 `--replace-message <file>` 覆盖提交信息通道——这是 v1.1.0 修掉的事故(只改 blob 会让实体名留在 commit message 里)。
cmd += ["--replace-message", str(message_replacements_path)]
验证也是双通道,专门针对「两个检查共享同一个盲区」的失败模式:blob 通道对所有 commit 跑 git grep;message 通道跑 `git log --all --format=%H%x1f%B%x1e` 解析出每条提交信息再匹配,并且失败时输出命中的 commit hash(前 10 个)而非只给计数。
"--format=%H%x1f%B%x1e", "--no-color"],
推送是 fail-closed 门禁:先 `gh repo view --json visibility,isPrivate,stargazerCount,forkCount,owner,name`(用 cwd 推断仓库,不猜 owner/repo 字符串);gh 失败或返回缺 key 一律 abort,不做任何 fallback 猜测;public 且 forkCount>0 时打印 WARNING;没有 --yes 直接退出;推送先 --force-with-lease,只有 stderr 出现 stale info 才允许回退一次 --force。
if "stale info" in result.stderr.lower():
2核心能力
3外部依赖
| Kind | Dependency / Endpoint |
|---|---|
| cli | gitleaks(Layer 1 密钥扫描) |
| cli | gitleaks detect(子进程实参:--source / --report-format json / --report-path / --verbose) |
| cli | git-filter-repo(历史重写,须在 PATH 且 --version 可执行) |
| cli | git(bundle create --all / bundle verify / status --short / show-ref --heads / rev-list --all / grep --perl-regexp / log --all / push --force-with-lease|--force) |
| cli | gh(GitHub CLI,用于仓库可见性与 fork 数核查) |
| api | GitHub REST API(由 gh repo view 隐式访问;源码内无字面 URL,端点归属 GitHub) |
| network | PyPI(文档命令 `uv run --with gitpython ...` 会要求 uv 从包索引获取 gitpython;四个脚本实际零 gitpython import,属文档声明依赖——[INFERENCE] 端点归属,源码内无 URL) |
| package | gitpython(SKILL.md 与四个脚本 docstring 的命令示例中声明;代码无 import,属未使用的声明依赖) |
| cli | uv(文档规定的运行器:uv run --with gitpython / uv run scripts/...) |
| cli | date(取 UTC 时间戳写入报告 scanned_at) |
| cli | brew(仅安装说明:git-filter-repo / gitleaks / gh) |
| cli | BFG Repo-Cleaner(参考文档里的备选工具,需 Java;未被脚本调用) |
| network | 用户侧的私有规则/身份清单文件(非网络端点,但属外部运行时输入):私有 gitleaks.toml、identities.txt、repo 根 .pii-patterns |
4管控建议与判定依据
- scripts/: 执行本地脚本或命令: scripts/scan_repo.py:shutil.which("gitleaks") 后 subprocess 调 `gitleaks dete
- 网络端点: 声明外部网络访问: safe_push.py → gh CLI → GitHub REST API(api.github.com):读取仓库 visibility/isP
- 本地工作目录: 文件读写与工件输出: scan_repo.py:写 --output 指定的 JSON 报告(会 mkdir 父目录);gitleaks 报告先写临时文件再删除。
5第二遍独立确认
- [ok] 文件清单全量核对(有无未列出的隐藏脚本/数据文件) — glob(hidden=true, gitignore=false)结果仅 9 个文件:SKILL.md、evals/evals.json、references/{ai_semantic_review_prompt,incident-lessons,tooling_notes}.md、scripts/{rewrite_history,safe_push,scan_repo,verify_cleanup}.py。无隐藏文件、无二进制、无 .pii-patterns 样本、无内置 hook;仓库级的 PII Guard hook(~/scripts/git-pii-guard)与 .githooks/ 属仓库/机器环境,不在 skill 目录内,skill 只是要求不得绕过它。
- [ok] external_deps 逐条反查调用点 — 全部命中:gitleaks → scan_repo.py `gitleaks_bin = shutil.which("gitleaks")` + verify_cleanup.py 同名调用;git-filter-repo → rewrite_history.py `shutil.which("git-filter-repo")` 且随后 `--version` 探测;git → show-ref/status/bundle create/bundle verify/rev-list/grep/log/push 各自 subprocess.run 均可定位;gh → safe_push.py 的 `["gh", "repo", "view", "--json", ...]`;date → scan_repo.py `["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"]`;uv/brew 只出现在文档命令与 Prerequisites,无对应代码调用(已在条目 note 中说明)。
- [ok] 安全结论反例搜索:是否存在漏掉的网络调用、隐藏外发或混淆内容 — 未找到反例,安全结论成立(记录检索范围以免过度外推):对 skill 目录 grep `requests|urllib|socket|curl|wget|http://|https://` 零命中,且全目录无任何 http(s):// 字面 URL(连「联系 GitHub Support」一段也只用文字);无 base64/fromhex/codecs/eval/exec/__import__ 等混淆或动态执行构造,无 60 字符以上的不透明长 token;对 git / git-filter-repo / gitleaks / gh / date 之外的二进制调用为零。唯一出网路径是 safe_push.py 经 gh 访问 api.github.com(uv --with gitpython 的包索引访问属 uv 行为而非脚本行为,已在 external_deps 标注 [INFERENCE] 端点)。证明边界:这是对该 commit 下 9 个文件的静态核对,不含运行时行为。
- [ok] 实现原理是否夸大:SKILL.md「What the Bundled Scripts Do」逐条对代码 — 四条声明全部落地:scan_repo.py 确实同跑 gitleaks 与自定义 grep 层并输出 JSON;rewrite_history.py 确实先备份再 `git filter-repo --replace-text`,且 `--message-replacements` 确实映射到 `--replace-message`;verify_cleanup.py 确实同时查 blob(git grep 全 commit)与 commit message(`git log --all --format=%H%x1f%B%x1e`),使漏改 message 的重写必然失败;safe_push.py 确实调 gh repo view 并走 lease-first。SKILL.md 未出现代码做不到的承诺。
- [ok] 7 条不可协商安全规则在 SKILL.md 的位置与代码落地情况 — SKILL.md 第 64~77 行「Safety Rules (Non-Negotiable)」编号 1~7:scan-before-decide / backup-before-rewrite / gh repo view 验可见性且不得从 URL 或目录名推断 / 绝不 --no-verify / --force-with-lease 优先且仅在 remote ref stale 时回退 --force / 重写后必须重新扫描 + AI 语义审查 / 公开且带 forks 需格外小心。落地核对:规则 2、4、5 有代码门禁(备份+verify、脚本从不加 --no-verify、lease→stale info→force 一次);规则 3 有硬门禁(gh 失败或元数据缺 key 即 abort);规则 1、6、7 依赖 agent 遵守文档与人工动作(scan 结果、--yes、fork 通知),不由代码强制——这是规则与实现的唯一不对称处。
- [ok] safe_push.py 的 public+forks 判定与 force-with-lease 回退条件 — 判定式为 `if not info["isPrivate"] and info["forkCount"] > 0:`,命中时只向 stderr 打印 `WARNING: This is a PUBLIC repository with {forkCount} forks.`,不阻断流程;唯一的硬门禁是缺 --yes 时 exit(1)。回退条件为 `if "stale info" in result.stderr.lower():`(对应 git 对 force-with-lease 拒绝时的 (stale info) 文案),回退仅一次 (--force),再失败即 exit(1)。注意:脚本不校验 remote/branch 是否与默认分支一致,也不在回退前复跑验证——--repo/--remote/--branch 参数的正确性由操作者负责。
- [discrepancy] 文档声明的依赖 gitpython 是否真实被使用 — 四处 docstring 与 SKILL.md 多处命令写 `uv run --with gitpython scripts/...`,但四个脚本的 import 块逐行核对后只有 argparse/json/re/shutil/subprocess/sys/tempfile/tomllib/pathlib,零 `import gitpython` 或 `from git import`。即文档要求 uv 从包索引拉一个不使用的第三方包(多余供应链接触面),去掉 `--with gitpython` 不影响任何功能;此外同一份 SKILL.md 内部不一致:Step 4/5/6 的命令写 `uv run scripts/...`(不带 --with),Step 1/1.5 与「What the Bundled Scripts Do」写 `uv run --with gitpython`。
- [discrepancy] --gitleaks-config 是否被传给 gitleaks 本体 — run_gitleaks 的 cmd 只有 `detect --source --report-format --report-path --verbose`,没有 `--config`;用户私有 gitleaks.toml 仅被 parse_gitleaks_rules 解析出 private-domain-context / private-ip-context 两条 regex 供 Layer 3 的 git grep 使用。SKILL.md 的说法(该 flag 用于从私有配置读取这两条规则)与代码一致,不存在夸大;但若用户以为私有配置会整体驱动 gitleaks 扫描(例如自定义 allowlist/其它私有规则),实际不会——gitleaks 走自身默认配置与仓库本地 .gitleaks.toml。此项为「文档未声明但易误读」的行为差异,非虚假声明。
6结论
aa1ae4b1f420e74a…