xiaowei-system/skills/devops/git-repo-hygiene/SKILL.md

18 KiB
Raw Blame History

name description version triggers date
git-repo-hygiene Use when git 仓库治理:推送积压、.git 膨胀、忽略规则、**密钥/凭证泄露与清历史(filter-repo)**、remote 明文口令、自动快照。 1.8.0
密钥泄露
凭证泄露
密钥被提交
清 git 历史
filter-repo
强推
推送积压
.git 膨胀
gitignore
remote 明文口令
自动快照
2026-09-11新增 §7.1 凭据外置remote 里的明文口令 → ~/.git-credentials(600)§7.2 自动快照 + 扫描闸门 cron

git 仓库治理(本机多仓库)

何时用

  • 用户问「XX 推 gitea 了么 / 最近的改动都提交了么」
  • .git 体积异常大;git add -A 之后仓库膨胀
  • 自动化脚本(快照/守护)只 commit 不 push悄悄积压几十个 commit

1. 推送积压审计2026-09-10一次问出 4 个仓库全都积压)

逐个仓库看,别只看最显眼那个:

for d in ~/.hermes ~/mc ~/projects/gaokao-site ~/.obsidian/plugins/zhiyi-memory; do
  echo "── $d"
  git -C "$d" log --oneline @{u}.. 2>/dev/null | wc -l   # 未推条数
  git -C "$d" remote -v | head -1
  git -C "$d" status -sb | head -1                       # 有无 upstream
 done
  • 没设 upstream → git push -u origin <branch>
  • remote 指向不可达地址(换网/换 VPN 后失效)→ 改 remote不要新建仓库
  • 仓库内嵌的独立子仓库(.git 在子目录)要单独查、单独推
  • 根治而不是补推:找到自动化脚本补 push。本次根因 = config-protector.sh 的 auto-snapshot 只 commitpush(积了 18 个修法commit 后跟 timeout 60 git push origin main >/dev/null 2>&1 || log "push 跳过" —— 网络不通静默跳过,绝不阻塞快照主流程

2. .git 膨胀回收(实测 2.6G → 27M

git add -A 在大型 vault 上会一次性 stage 几万个文件(本次 46456 个 → .git 涨到 3.2G)。 git reset 只取消索引,对象仍在;而且 reflog 会保护它们,普通 gc 收不走:

git reflog expire --expire=now --all
git gc --prune=now
du -sh .git
  • 只删不可达对象,安全;但必须先 expire reflog否则 prune 不生效
  • 清理后四项验证:git count-objects -vHloose 应为 0git ls-files | wc -l(追踪数不变)、git log --oneline -1HEAD 完好)、git status -sb(与远端同步)

3. .gitignore 范围Obsidian vault / 大工作目录)

  • 先量规模再决定收什么:du -sh */ + git ls-files | wc -l
  • 10G 级工作目录不进 git → 迁 ~/projects/ 或走 SMB 备份
  • 内层独立仓库要显式排除,否则父仓库会把它当普通目录收进来
  • 文本笔记(.md)适合 gitPDF/Excel/DB/媒体一并排除
  • git add -A 之前先 --dry-run 数一遍文件数,数量级不对立刻停手

4. 判断远端是否也膨胀

Gitea API 核仓库体积:

curl -s -m 15 'http://<host>:3000/api/v1/repos/<owner>/<repo>' \
  | python3 -c "import sys,json;print(json.load(sys.stdin).get('size'))"

本次远端仅 26MB ⇒ 膨胀只发生在本地push 时未带上臃肿历史),所以本地 gc 即可,不必 force push 重写历史。

5. 密钥从 git 历史清除2026-09-10 实战mc vault 4 处真密钥已推 Gitea

工作区脱敏只挡住未来的 commit已入库的必须重写历史。完整配方 + 验证代码:references/secret-purge-from-history.md

三段式(一轮清不干净——实测跑了 3 轮才彻底):

  1. 第 0 步:先备份git bundle create ~/.hermes/backups/<repo>-pre-filterrepo-<date>.bundle --all27M 仓库秒级完成。filter-repo 不可逆,没有 bundle 不开工。
  2. 文字密钥git filter-repo --force --replace-text expr.txt,每行 literal:<密钥原文>==><REDACTED-secret>
    • 必须用 literal: 逐字替换。别图省事用正则sk-[A-Za-z0-9]{20,} 会把文档里的 https://x.com/task-driven-… 这类 URL 片段一起改掉。
  3. 二进制里的密钥--replace-text 不处理 .pyc/编译产物(本次一个 .pyc 把 key 当字符串常量嵌着——扫得出来、replace-text 换不掉)。补一轮 git filter-repo --force --invert-paths --path-glob '*.pyc' --path-glob '*__pycache__*',把整类构建垃圾连根删(本来就不该入库)。
  4. 老快照 commit 里的历史里那种「全库快照」commit 常把整棵旧目录提交过,里面躺着工作区早删掉的密钥(本次第 3 轮又挖出 3 个Agnes / DeepSeek / Gitee AI。做完 2、3 再全量扫一遍才暴露。

验证(必做,别信「跑完了」):扫所有 blob,不是只看 HEAD——

git log --all --oneline -S '<密钥原文>'        # 应无输出
git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype)' \
 | awk '$2=="blob"{print $1}' | while read b; do
     git cat-file blob "$b" | grep -qF '<密钥原文>' && echo "LEAK $b"; done

再用形状正则全库扫一遍(sk- / tskey- / github_pat_ / ghp_ / nvapi- / AKIA / xox[baprs]- / LTAI / glpat- / AIza逐条人工判真假:剩下的应当全是 sk-test-… sk-your-… xoxb-workspace-… AKIAIOSFODNN7EXAMPLE 这类文档占位符。

形状正则必须带负向断言:写 (?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{24,}。 不加前缀断言,https://yoheinakajima.com/task-driven-autonomous-agents 里的 sk-driven-… 和正文里 `task-*.md` 这类片段全会被当成密钥(实测 10 条命中里 3 条是这种假阳性)。 命中后逐条判:10-25 位 + 带 test/your/xxxx/example 语义的一律是占位符,本机已确认可忽略的: sk-test-* sk-your-* sk-xxxxxxxx sk-ant-api03-… sk-deepseek-… sk-or-v1-test/your… xoxb-workspace-… xoxb-your-… AKIAIOSFODNN7EXAMPLE Bearer eyJhbG…(单测夹具里的假 JWT

还要验「远端」,不只验本地--force 之后最容易在这里翻车——本地干净 ≠ 推上去的是干净那份):

git rev-parse HEAD origin/main          # 两者必须相等
git clone --bare --quiet <url> /tmp/verify && cd /tmp/verify
# 在 /tmp/verify 里重跑上面两段 blob 扫描(对象数应与本地一致)
git log --oneline -3                    # 应是重写后的新 hash
git cat-file -t <重写前的hash>           # 应报 Not a valid object name

远端仓库的旧 pack 对象在 Gitea 服务端 GC 前可能仍被取到 → 想彻底干净要么跑服务端 GC要么删库重建反正推的是全量新历史

两个必知行为

  • git filter-repo 跑完会删掉 origin remote(防误推)→ 强推前先 git remote add origin <url> 加回来。
  • 所有 commit hash 都会变 → 只能 git push --force。这是不可逆操作,可能被审批闸拦下:被拦就停手,把命令原文交给用户批准,不要换命令绕过
  • Gitea 侧 force push 只改 ref旧对象在服务端 GC 前仍可能被取到 → 真正止血是轮换密钥,重写历史只是补刀。清出的每个密钥都要列进轮换清单交用户。

⚠️ 泄漏面不止 git —— 同一批密钥在派生产物里各有一份2026-09-11 实测)

清完 git ≠ 清干净。同一条 Tailscale key 同时存在于 5 类存储工作区文件、git 历史/远端、 知识图谱节点名向量库LanceDB内容、同步端旧副本。逐面清单 + 图库/向量库处置 + 生产端到端验证脚本 → references/credential-leak-blast-radius.md。 ▶ 一键复扫脚本:scripts/scan-secret-blast-radius.py(工作区 / git 全对象 / sqlite 图谱 / 向量库四面扫,输出只打前 8 位)。

  • 知识图谱:实体名常常就是密钥原文(蒸馏把含 key 的句子当实体)→ SELECT id,name FROM graph_nodes 过形状正则; 本次 8 个节点 / 37 条边,还挖出一个审计只扫 git 时漏掉的第二个 key
  • 向量库:事件本体里就是明文(直接按字节扫 memories.lance/data/*.lance)。改动风险高 → 优先靠轮换让它失效, 不急着做写入手术LanceDB 有损坏前科)。
  • 凭证主库对照:清出来的 key 要回头对一下 ~/mc/牧尘/claw/key.md——本次图库里那个 key 压根不在主库
  • 收尾一句:清历史/清存储都只是补刀,轮换密钥才是真止血;每个清出的 key 都要进轮换清单交用户。

6. 预防把脱敏做进写入路径2026-09-11 织忆实战,清历史只算补刀)

密钥必须死在数据进入派生产物的那一刻,而不是事后从历史里刨。 本次事故链:蒸馏把含密钥的对话当事实入库 → 知识图谱按该文本建实体 → ObsidianSync 用实体标题当文件名/正文 → 镜像被 git 跟踪并推送。 只改文件名、只清历史,都不治根:下一轮蒸馏还会再长出来。

唯一真源:一个 redact 模块(图案正则 + 替换 + ContainsSecret每个落盘/入库边界各调一次。 同一套规则绝不复制三份 —— 复制出去的那份迟早不同步。

必查的边界(漏任何一个 = 等于没做):

  1. 入库质量门槛(事实判定前脱敏 + 过滤函数的返回值也要脱敏,调用方拿的是返回值)
  2. 实体/标题生成(命中就丢实体,连它的关系边一起丢,别留孤立节点)
  3. 文件名 sanitize + 正文(正文才是泄漏主战场,文件名只是显眼)
  4. 绕过质量门槛的直写路径(最易漏,本次泄漏的那条记忆正是从这里进去的)

实现细节Go RE2 无 lookbehind、假阳性护栏、幂等、测试清单references/write-side-redaction.md

配套检测护栏(凭证治理做完必须留检测,否则下轮又长回来): 把 vault 扫描从「只扫文件名」升级成「扫文件内容」+ 每日 cron + 凭证主库目录白名单 —— 见 notes-vault-curation §五。

7. 「这个目录要不要进 git」决策框架2026-09-11 用户问「笔记文件有必要推到 gitea

先分清两件事——它们不互为备份

用途 工具 能力边界
多端一致 / 灾难恢复 WebDAV·SMB 同步 覆盖式keep_newer→ 改错删错无法回滚,没有历史
版本历史 / 追溯 git + 远端 能答「这句话三个月前怎么写、谁改的、哪次改坏的」

回答模板:进 git 的理由是历史,不是「怕丢」——同一台机器上再放一个 git 远端不算备份(一起挂)。 实证价值:本次定位密钥泄漏范围(哪几个 commit、哪个快照里还躺着全靠 git 历史。

四条硬边界(缺一条就会变成事故):

  1. .git/ 绝不能进同步通道WebDAV/网盘)——多端互相同步 .git 会直接把仓库搞坏。 上线后用远端目录列表确认:PROPFIND 结果里不应出现 .git/(只有 .gitignore 这种文件是正常的)。
  2. 凭证「可以」进 git —— 前提是远端为自建私库2026-09-11 用户明确纠正,原写法「凭证永不进 git」已作废 牧尘原话:「敏感资料可以进 gitea但是属于私库就我一个人知道项目也是除非上传 GitHub我会单独要求你」。 因此红线不是「进了 git」而是「去了第三方 / 公开平台」
    • 允许:自建 Gitea 私库(先验 curl -s .../api/v1/repos/<owner>/<repo>private:true
    • 红线GitHub / 任何第三方公开平台 —— 只有用户单独、明确要求才可上传
    • 推论:「发现密钥被 git 跟踪」不等于事故。先问「谁可能读到」再定性,别一上来就按事故跑全流程
    • 机制层照旧,但靶心改成防「意外扩散」:凭证库进 .gitignore 是防误发布(不是防入库)
      • 写入侧脱敏§6+ 每日内容级密钥扫描(notes-vault-curation §五之补)。 真正要抓的是自动生成物(镜像/导出/日报/截图)把凭证带到用户没打算的地方——本次事故正是这条。
  3. 二进制不进 gitpdf/xlsx/docx/pptx/zip/db——体积爆炸且无 diff 价值。
  4. 范围要克制:本次 mc 仓库只跟踪 218 个文件(一个子区 + .gitignore161M 工作资料与凭证库全在 ignore 内。 审计口径:git ls-files | awk -F/ '{print $1}' | sort | uniq -c + grep -vE '^\s*#|^\s*$' .gitignore

什么时候主动叫停:用户开始往被跟踪区放敏感原文(合同/证件/客户名单时——git 有历史,删了还能翻出来。 处置不是停用 git而是把跟踪范围收缩到纯知识子目录

7.1 凭据外置remote URL 里的明文口令2026-09-11 实测修完 4 个仓库)

症状:remote.origin.url 长成 http://<user>:<pass>@<host>:3000/... → 口令明文躺在每个 .git/config 里, 随目录打包/同步/备份一起扩散(本次 4 个仓库全中mc / memoryweave / ~/.hermes / projects/*)。

审计一行(批量找出所有中招仓库):

for d in ~/mc ~/src/memoryweave ~/.hermes ~/projects/*; do [ -d "$d/.git" ] || continue
  git -C "$d" remote get-url origin 2>/dev/null | grep -q '@' && echo "⚠️ $d"; done

修法4 步,顺序不能反):

# 1) 写凭据文件(每行 http://user:pass@host:port→ 权限 600
chmod 600 ~/.git-credentials
# 2) 启用 helper
git config --global credential.helper store
# 3) 逐个仓库去掉 URL 里的口令
for d in <repo...>; do
  clean=$(git -C "$d" remote get-url origin | sed -E 's|://[^@]*@|://|')
  git -C "$d" remote set-url origin "$clean"; done
# 4) 验证鉴权仍通(读 + 写都要验)
git -C <repo> ls-remote --heads origin >/dev/null && echo 读OK
git -C <repo> push origin main          # 已同步时回 Everything up-to-date 且 exit 0 —— 仍走完整认证

两个坑(都实测过)

  • 已有 ~/.git-credentials ≠ 配好了本次该文件早已存在7 月建的),但权限是 664(同机可读) 且 credential.helper 根本没设置 → 等于白放。改完必须 git config --global --get credential.helper 复查。
  • 机器上没有 git-credential-libsecret不要硬上 keyringcron/无人值守下 keyring 可能是锁的, 会让后台推送静默失败store + 600 是无头环境最稳的选择。 (诚实口径:仍是明文,只是单点化 + 权限收紧,别对用户说成"加密了"。)

7.2 自动快照把「扫描闸门」做进定时推送2026-09-11 建成)

补 §1 的漏洞:只在 agent 干活时才 push 的话,用户自己在编辑器里写一周笔记,远端就掉队、版本历史出现空档。 做法 = 一个脚本(~/.hermes/scripts/<vault>-autopush.sh+ 每日 no_agent cron两道闸门

# 闸门①:密钥扫描未通过 → 不 commit 不 push输出告警把泄漏挡在 push 之前)
if ! python3 ~/.hermes/scripts/mc-structure-check.py --secrets-only > /tmp/scan.txt 2>&1; then
  echo "⚠️ 已跳过:密钥扫描未通过"; sed 's/^/  /' /tmp/scan.txt | head -15; exit 0; fi
# 闸门②:无变化 → 静默退出watchdog 模式,不打扰)
[ -z "$(git status --porcelain)" ] && exit 0
git add -A
git -c user.name=<bot> -c user.email=<bot> commit -q -m "chore(vault): 每日自动快照 $(date +%F)"
timeout 90 git push -q origin main 2>/dev/null && echo "✅ 已推 $(git log --oneline -1)" || echo "⚠️ 已提交但推送失败"
  • --dry-run 分支(只打印待提交项不落盘),交付前自测三态:无变化静默 / 造临时文件能识别 / 真跑无变化时输出必须为
  • cron 用 no_agent → 干净时零消息;但注意 no_agent 模式下 prompt 被完全忽略 要让告警自带说明就必须写进脚本自己 print(见 credential-leak-response Phase 5

8. 铁律

  • 只在确认规模add -A;大仓库一律显式路径 git add scripts/ skills/ MEMORY.md
  • commit message 前缀:skill(...) / fix(...) / chore(...) / refactor(...) / docs(...)
  • push 前确认 remote 可达VPN/局域网);失败要如实报出来,别标成功
  • 重写历史filter-repo / force push是最后手段先问用户开工前 git bundle 备份,收工后把清出的密钥列成轮换清单
  • 提交前 git diff --stat 逐文件核对行数2026-09-11 实测):在遗留仓库随手跑 gofmt -w / black / prettier 会把 4 行真改动放大成 352 行 diff。 判据:格式化工具 --check/-l 列出一大堆本来就没格式化的文件 ⇒ 保持格式化不是本仓库约定, 别单独格式化你碰到的那一个。发现混入噪音:git checkout -- <file> 回滚,只重放最小改动。
  • 既有测试腐化:先归因再动手,并披露2026-09-11 实测AC 要求「X 包测试全绿」 但该包根本编译不过时,先看 git diff --name-only 里有没有那个文件——没有就是既有腐化 (典型:函数改名、签名加参后测试没跟)。最小化修(只动测试文件)+ commit message 里显式披露。 副产品结论:一个包长期编译不过 = 它的单测长期没跑过,这个包以前的「测试通过」别当已验证。
  • 去破坏性组合命令2026-09-11 实测):把 git add -Agit filter-repogit push --force 和一大堆 && 拼成一条长命令,容易整个被审批闸拦下 → 前面的副作用也没发生。 拆成可独立验证的小命令,每步自己看结果。 另有一种解析层拦截(与审批无关):超长内联命令(大量 $(...)/heredoc/巨长单行)会被 BLOCKED (hardline): command parser limit or malformed executable payload 挡下, 命令根本没执行。恢复路径:命令已被存到 ~/.hermes/cache/blocked-scripts/blocked-<ts>-<hash>.sh 按提示 bash <该文件> 即可;或改写成 /tmp/x.sh + bash /tmp/x.sh。别内联原样重试。