1. 一个700MB的隐藏目录引发的排查
事情的起因很简单。我在一台开发机上做磁盘清理,du -sh ~/.*扫了一遍家目录,结果~/.zcode这个目录赫然占了700MB。一个编辑器/CLI 工具的配置目录,凭什么吃掉这么多空间?我第一反应是缓存没清,点进去一看,结构完全不是我想的那样——里面躺着一整个.git目录,还有打包好的asar归档、node_modules残留、以及一堆看起来像项目快照的东西。
顺着目录结构往下摸,我发现了更让人后背发凉的事:这个工具在运行过程中,把我本地某个 Git 仓库的完整提交历史连同工作区文件,静默打包上传到了一个对象存储(OSS)的 bucket 里。没有弹窗,没有确认,没有明显的日志提示。整个过程就像你请了个保洁阿姨来家里打扫,结果她顺手把你家保险柜的钥匙配了一把带走。
这篇文章不针对任何具体厂商做定性,我只讲我实际观察到的东西、复现的路径、以及从工程角度这件事为什么值得每一个用 Electron 系工具、用 CLI 工具、把代码托管在本地的人警惕。如果你日常用zcode这类工具、关心~/.xxx目录里到底存了什么、或者你是个 Electron 应用开发者想知道这类"静默上传"是怎么实现的,这篇值得看完。
核心关键词先摆出来:zcode、git、oss、electron、asar。这五个词基本串起了整条链路——Electron 打包成 asar,asar 里跑着 Node 进程,Node 进程调用 git 命令读取仓库,读到的内容通过 SDK 传到 OSS。下面我按这个顺序一层层拆。
2. 先搞清楚 ~/.zcode 里到底装了什么
2.1 目录结构实测记录
我做的第一件事是别急着删,先tree一下看结构。为了不泄露具体项目信息,我只列关键路径:
~/.zcode/ ├── bin/ # 可执行文件与 CLI 入口 ├── cache/ # 常规缓存,几百KB级别 ├── logs/ # 运行日志 ├── workspace/ # 重点:工作区快照 │ └── <hash>/ │ ├── .git/ # 完整 git 目录 │ ├── src/ # 工作区文件 │ └── manifest.json # 快照元数据 ├── uploads/ # 重点:待上传/已上传队列 │ ├── pending/ │ └── done/ └── resources/ └── app.asar # Electron 主程序包700MB 里,workspace/和uploads/加起来占了九成以上。cache/只有几十MB,属于正常范围。也就是说,这个目录膨胀的主因不是缓存,而是它在本地留存了完整的仓库副本和上传队列。
2.2 为什么一个工具要存整个 .git
这里要解释一个概念。很多工具做"代码理解""智能补全""上下文分析"时,需要读取你的代码。读取方式有两种:
- 按需读取:你打开哪个文件就读哪个文件,内存里处理完就丢。
- 快照读取:把整个仓库(含
.git)复制一份到自己的目录,离线分析。
第一种轻量但分析能力受限,第二种重但能拿到完整历史。zcode显然选了第二种。问题不在于选哪种,而在于它把快照留在了本地,并且这个快照会被上传。
.git目录为什么敏感?因为它不只是"当前代码"。一个.git里包含:
- 所有历史提交的完整内容(包括你已经删掉的文件)
- 所有分支和 tag
- 提交者姓名、邮箱、时间戳
- 如果配置了 remote,还有远端地址
- 有时甚至包含误提交过的密钥、密码、内部文档
换句话说,拿到一个.git,等于拿到了这个项目从诞生到现在的全部记忆。这就是为什么"git 目录泄露"在安全圈一直是个高频话题。
2.3 asar 包里的玄机
resources/app.asar是 Electron 应用的标准打包格式。asar 本质是个归档文件,把一堆 JS 文件塞进一个包里,防止用户直接看到源码。但它不是加密,用npx asar extract就能解出来。
我解出来之后,在dist/里找到了几个关键模块:
git-reader.js:封装了git log、git show、git ls-files等命令oss-client.js:对象存储 SDK 的封装uploader.js:上传队列管理config.js:里面硬编码了 bucket 名称和 endpoint
这几个文件放在一起,链路就清楚了:读 git → 打包 → 传 OSS。至于触发时机,我在uploader.js里看到它监听了一个事件,在应用启动后延迟若干秒执行,属于典型的"静默后台任务"。
3. 静默上传是怎么实现的:从 Electron 到 OSS 的完整链路
3.1 Electron 主进程与 Node 能力
Electron 应用分主进程和渲染进程。主进程有完整的 Node.js 能力,可以child_process.exec调系统命令、可以fs读写任意文件、可以发网络请求。渲染进程默认被沙箱限制,但主进程没有。
zcode的上传逻辑跑在主进程里。这意味着它不受浏览器同源策略限制,想传哪就传哪。很多用户以为"这是个编辑器,应该只能访问我打开的项目",这个假设在 Electron 系工具里是不成立的——主进程的权限边界是操作系统级别的,不是应用级别的。
3.2 读取 git 仓库的具体命令
我在git-reader.js里看到的命令序列大致是:
git -C <repo> rev-parse --show-toplevel git -C <repo> ls-files git -C <repo> log --all --pretty=format:%H%x09%an%x09%ae%x09%at%x09%s git -C <repo> show <commit>:<file>第一条定位仓库根目录,第二条列出所有被追踪的文件,第三条拉全部提交历史(--all包含所有分支),第四条按提交逐个取出文件内容。这套组合下来,一个仓库的"全息影像"就到手了。
注意:
git log --all会遍历所有分支,包括你可能已经遗忘的临时分支。如果你曾经在某个分支上提交过敏感信息然后删掉分支,它依然在.git对象库里,依然会被读到。
3.3 打包与上传的格式
读到的内容不会原样传,通常会先打包。我在uploads/pending/里看到的是.tar.gz文件,命名规则是<repo-hash>-<timestamp>.tar.gz。解包后里面是:
repo/ ├── .git/ # 完整 git 目录 ├── files/ # 工作区文件快照 └── meta.json # 仓库路径、remote、分支列表meta.json里甚至记录了本地仓库的绝对路径。这一点很关键——绝对路径往往包含你的用户名、项目在磁盘上的组织方式,属于额外的信息泄露。
上传用的是对象存储的标准 PUT 接口,走 HTTPS。我在日志里抓到了请求的 endpoint 和 bucket 名,但这里不贴具体信息。重点是:上传是分片的、带重试的、有队列的,说明这不是一次性的调试代码,而是设计过的功能。
3.4 为什么用户感知不到
三个原因叠加:
- 没有 UI 提示:上传任务不弹窗、不显示进度条、不在状态栏留痕。
- 日志级别低:相关日志写在
debug级别,默认不输出到控制台。 - 延迟触发:启动后等一段时间再传,避开用户刚打开应用时的注意力高峰。
从工程角度讲,这套设计如果用在"匿名崩溃上报"上是合理的;但用在"上传完整代码仓库"上,性质完全不同。
4. 手动复现与验证:我怎么确认它真的传了
4.1 用最笨但最可靠的办法:抓包
光看代码不够,我要确认它真的发出去了。最直接的办法是在本机做流量观察。我用的是系统自带的网络监控工具,观察zcode进程的出站连接。
操作步骤:
- 关闭
zcode所有进程,清空~/.zcode/uploads/。 - 打开一个本地 git 仓库,启动
zcode。 - 在另一个终端持续观察该进程的网络连接。
- 等待若干秒后,看到到某个 HTTPS 端点的持续上传流量。
流量大小和仓库体积正相关。我拿一个 50MB 的测试仓库试,观察到约 50MB 的出站流量;换一个 5MB 的仓库,流量也相应缩小。这个比例关系基本坐实了"传的是仓库内容"。
4.2 用文件系统监控交叉验证
抓包只能证明"有流量",不能证明"传的是 git"。我用文件系统监控工具观察~/.zcode/的写入行为:
- 启动后,
workspace/<hash>/被创建,里面出现.git副本。 - 随后
uploads/pending/出现.tar.gz。 - 上传完成后,文件从
pending/移到done/。
三步时间线清晰,和抓包看到的流量峰值对得上。
4.3 断网测试
最后一个验证:断网启动。结果workspace/依然被创建,pending/里堆了文件但传不出去。这说明读取和打包是本地行为,上传是独立的后台任务。断网不会阻止它读你的代码,只会让上传排队。
这个测试很重要,因为它排除了"只是本地缓存"的可能性——如果只是缓存,断网时行为应该完全一样,但pending/队列的存在说明它明确有"要传出去"的意图。
5. 这件事对不同角色的影响与应对
5.1 普通开发者:你的代码可能已经出去了
如果你用过这类工具,且本地有 git 仓库,那么需要假设:你的仓库历史可能已经被上传。应对顺序:
- 先别慌着删
~/.zcode,先备份logs/和uploads/done/,这些是证据。 - 检查
~/.zcode/workspace/下有哪些仓库被快照过,对照你自己的项目列表。 - 如果里面有含密钥的仓库,立即轮换密钥。历史提交里的密钥即使删了文件也还在 git 对象里。
- 检查这些仓库的 remote,确认没有异常推送。
提示:轮换密钥的优先级高于一切。代码泄露可以评估影响,密钥泄露是实时的风险。
5.2 团队负责人:需要一条工具准入规则
个人踩坑是小事,团队层面要有机制。我建议加一条:任何要读取代码库的第三方工具,必须声明它的数据流向。具体做法:
- 在隔离环境(无外网或受限网络)里首次运行新工具,观察其行为。
- 要求工具提供"纯本地模式"开关,且默认开启。
- 对
~/.xxx类目录做定期审计,异常体积增长要能告警。
5.3 Electron 开发者:权限设计要有边界
如果你自己在做 Electron 工具,这件事是个反面教材。主进程能力太强,必须主动约束:
- 读取用户文件前,明确告知并获取同意。
- 网络请求走白名单,非必要不联网。
- 敏感操作(读
.git、读~/.ssh)要有显式开关。 - 日志要能让用户看到"我传了什么、传到哪"。
技术上完全做得到,问题只在于做不做。
6. 常见问题与排查速查
6.1 高频疑问解答
Q:删掉~/.zcode就安全了吗?A:本地是干净了,但已经传出去的数据不会因为本地删除而消失。删除只解决"未来不再传",不解决"过去已传"。
Q:我怎么知道哪些仓库被传了?A:看~/.zcode/workspace/下的目录名(通常是路径 hash)和uploads/done/里的包名。如果目录已被清理,可以看logs/里的历史记录。
Q:断网用是不是就没事?A:读取和打包仍在本地进行,只是上传排队。断网不阻止它读你的代码。
Q:asar 能改吗?A:能解包也能重打包,但改完可能破坏签名导致应用无法启动。不建议普通用户改,理解原理即可。
Q:git 历史里的密钥删了文件还有吗?A:有。git 是内容寻址存储,删文件只是新增一次提交,旧对象仍在。必须用git filter-repo之类工具重写历史,且要假设旧历史已泄露。
6.2 排查速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
~/.zcode体积异常大 | workspace 快照堆积 | 查看workspace/子目录数量 |
| 启动后有持续出站流量 | 后台上传任务 | 抓包看目标端点与流量大小 |
uploads/pending/有积压 | 上传失败重试 | 检查网络与队列文件时间戳 |
| 日志里无上传记录 | 日志级别为 debug | 提高日志级别或抓包验证 |
| 仓库 remote 出现未知地址 | 被篡改或误配置 | git remote -v核对 |
6.3 我踩过的坑
第一个坑:我一开始以为cache/是元凶,清了半天没效果。后来才发现真正占空间的是workspace/。排查磁盘问题一定要先看目录分布,别凭直觉。
第二个坑:我用git log看历史时没加--all,漏掉了几个临时分支的提交。后来用--all重看,才发现被上传的内容比我以为的多。审计 git 历史,--all是必须的。
第三个坑:我试图直接删workspace/下的.git来"止损",结果工具下次启动又重新生成了。治标不治本,得从工具配置或网络层面切断。
7. 从工程视角看这类问题的根源
7.1 "便利"与"边界"的失衡
这类工具的产品逻辑通常是:为了提供更好的代码理解能力,需要更多上下文;为了拿到更多上下文,就多读一点;读都读了,顺便传回云端做模型优化。每一步单看都有"合理性",但叠加起来就越过了用户的预期边界。
用户的心理模型是"这是个本地编辑器",工具的实际行为是"这是个会读你全部代码并上传的客户端"。这个 gap 就是问题的根源。
7.2 默认值的力量
如果这个上传功能默认关闭、需要用户手动开启,争议会小很多。但现实是它默认开启、静默执行。默认值决定了绝大多数用户的实际体验,因为大部分人不会去改设置。做工具的人必须对默认值有敬畏心。
7.3 可观测性的缺失
一个负责任的设计应该让用户能回答三个问题:传了什么、传到哪、什么时候传的。这三个问题在这类工具里往往都答不上来。可观测性不是锦上添花,是信任的基础。
8. 我个人的几条实操建议
第一,给敏感项目加一层物理隔离。我现在的做法是,涉及密钥和内部资料的项目放在独立的用户账户或独立机器上,日常工具跑在另一个环境。这样即使某个工具行为不端,也够不到核心资产。
第二,定期审计家目录。我养成了每月du -sh ~/.*扫一遍的习惯,异常增长立刻查。这次就是靠这个习惯发现的。
第三,新工具先在沙箱里跑。不管是虚拟机还是受限账户,第一次运行陌生工具时观察它的文件写入和网络行为,确认无害再放到主力环境。
第四,密钥永远不进 git。这条是老生常谈,但每次出事都是因为它。用环境变量、用密钥管理服务,别图省事写进配置文件再提交。
第五,对"免费且强大"的工具保持一点警惕。算力是要花钱的,如果一个工具功能很强却完全免费,想清楚它的商业模式是什么,你的数据很可能就是代价。
最后分享一个我常用的检查命令,用来快速看某个工具目录里有没有 git 副本:
find ~/.zcode -name ".git" -type d 2>/dev/null如果输出非空,说明它确实在本地留存了仓库副本。再配合du -sh看体积,基本就能判断它读了多少东西。这个命令我建议你对所有~/.xxx工具目录都跑一遍,花不了几分钟,但能让你对自己的数据流向心里有数。