<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>新手入门 on AI博士 万戈</title>
        <link>https://www.yesmiracle.net/tags/%E6%96%B0%E6%89%8B%E5%85%A5%E9%97%A8/</link>
        <description>AI博士万戈的技术博客，聚焦 Agentic AI、AI Infra 与 Agent Security，分享 AI 基础设施与工程落地实践。</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <managingEditor>admin@yesmiracle.net (万戈)</managingEditor>
        <webMaster>admin@yesmiracle.net (万戈)</webMaster>
        <lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.yesmiracle.net/tags/%E6%96%B0%E6%89%8B%E5%85%A5%E9%97%A8/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Git 从零讲清：AI 为什么能让一个网站代码库涨到 50G</title>
        <link>https://www.yesmiracle.net/post/20261024-git-basics-why-ai-worktrees-eat-disk/</link>
        <pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261024-git-basics-why-ai-worktrees-eat-disk/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261024-git-basics-why-ai-worktrees-eat-disk/cover.svg" alt="Featured image of post Git 从零讲清：AI 为什么能让一个网站代码库涨到 50G" /&gt;&lt;p&gt;一个简简单单的网站代码库，本地占了 50G。&lt;/p&gt;
&lt;p&gt;遇到这种情况，多数人的第一反应是「AI 又乱搞了」。但真实原因往往更平淡：AI 建了一堆 workspace，而没有任何人删过它们。而这件事之所以会发生，是因为你不懂 Git 的工作模型，于是没法判断「哪些目录是正常的、哪些是多出来的」。&lt;/p&gt;
&lt;p&gt;所以这篇从最基础的地方讲起，最后落到那 50G 是怎么来的、以及怎么清掉。文里的磁盘数字都是我在一个真实的网站仓库上实测的，不是估的。&lt;/p&gt;
&lt;h2 id=&#34;一git-的最小模型三个区域&#34;&gt;一、Git 的最小模型：三个区域&lt;/h2&gt;
&lt;p&gt;Git 真正需要你先记住的东西只有三个区域。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;区域&lt;/th&gt;
          &lt;th&gt;是什么&lt;/th&gt;
          &lt;th&gt;文件在哪&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;工作区&lt;/td&gt;
          &lt;td&gt;你打开编辑器看到、能改的那些文件&lt;/td&gt;
          &lt;td&gt;你的项目目录&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;暂存区&lt;/td&gt;
          &lt;td&gt;一份「我准备把哪些改动提交进去」的清单&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;.git/index&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;仓库&lt;/td&gt;
          &lt;td&gt;已经提交过的历史&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;.git/&lt;/code&gt; 里面&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;日常三条命令正好对应这三个区域之间的关系：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git add &amp;lt;文件&amp;gt;      &lt;span style=&#34;color:#75715e&#34;&gt;# 工作区 → 暂存区：把这次改动放进「准备提交」清单&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git commit -m &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;...&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 暂存区 → 仓库：把清单上的内容做成一个提交，永久记下来&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git checkout &amp;lt;分支&amp;gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 仓库 → 工作区：把某个历史状态取出来，覆盖到工作区&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这张图是理解后面一切的基础。&lt;strong&gt;尤其要记住第三行&lt;/strong&gt;：Git 可以从仓库里重新生成工作区的文件。这意味着工作区在 Git 的设计里是一个&lt;strong&gt;可以随时重建的临时产物&lt;/strong&gt;，而不是数据本体。数据本体在 &lt;code&gt;.git&lt;/code&gt; 里。&lt;/p&gt;
&lt;p&gt;这个认知后面会用到，因为它直接决定了「删工作区」这件事在 Git 的模型里是不是安全。&lt;/p&gt;
&lt;h3 id=&#34;为什么要有暂存区&#34;&gt;为什么要有暂存区&lt;/h3&gt;
&lt;p&gt;新手最常见的疑问是：为什么不直接提交，中间非要加一层？&lt;/p&gt;
&lt;p&gt;因为「改了什么」和「这次提交想包含什么」经常不是一回事。你在一个文件里同时修了一个 bug、顺手改了个错别字，暂存区让你把这两件事拆成两个提交。它把「编辑」和「组织历史」拆成了两个独立动作。&lt;/p&gt;
&lt;p&gt;觉得麻烦的话，&lt;code&gt;git commit -a&lt;/code&gt; 可以跳过暂存区把已跟踪文件的改动一起提交。但知道它存在比一直用 shortcuts 更重要，因为迟早你会需要只提交文件里的一部分改动（&lt;code&gt;git add -p&lt;/code&gt;）。&lt;/p&gt;
&lt;h2 id=&#34;二一次提交到底做了什么&#34;&gt;二、一次提交到底做了什么&lt;/h2&gt;
&lt;p&gt;这里有个反直觉的事实：&lt;strong&gt;Git 存的是快照，不是改动。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多人以为 Git 记录的是「第 1 行改了什么」，其实每次提交 Git 都存下&lt;strong&gt;那一时刻所有文件的内容&lt;/strong&gt;。听起来很浪费，但它用两个机制把浪费消掉了。&lt;/p&gt;
&lt;p&gt;机制一，内容寻址。 Git 里的每个对象都用它自己内容的哈希值命名。两个文件内容完全一样，就只存一份，因为它们算出同一个哈希。所以「存了 100 个版本的快照」实际占用的空间远小于 100 倍。&lt;/p&gt;
&lt;p&gt;机制二，对象只有四种。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;对象&lt;/th&gt;
          &lt;th&gt;存什么&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;blob&lt;/td&gt;
          &lt;td&gt;一个文件的内容&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;tree&lt;/td&gt;
          &lt;td&gt;一个目录的结构（哪些文件名指向哪些 blob/tree）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;commit&lt;/td&gt;
          &lt;td&gt;指向一个 tree，加上作者、时间、提交信息、父提交&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;tag&lt;/td&gt;
          &lt;td&gt;给某个对象起的带注释的名字&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;于是「一次提交」在磁盘上就是：一堆 blob（内容）加一层 tree（目录结构）加一个 commit（指向那层 tree）。没改动的文件不会产生新 blob，新的 tree 只是复用旧的哈希。&lt;/p&gt;
&lt;h3 id=&#34;分支是什么&#34;&gt;分支是什么&lt;/h3&gt;
&lt;p&gt;这是新手最容易想复杂的地方。分支&lt;strong&gt;不是一份代码副本&lt;/strong&gt;，它是一个文件，里面只有一行：一个提交的哈希值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cat .git/refs/heads/master
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 输出类似：4a2097c8f3...（40 个字符）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;就这样。所以「建一个分支」几乎不占空间，也不用复制代码。你在新分支上提交，Git 只是把这个文件里的哈希改成了新的提交。原来的提交还在，因为它被别的引用指着。&lt;/p&gt;
&lt;p&gt;HEAD 是另一个小文件，它通常不直接存哈希，而是写「我现在在哪个分支上」：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cat .git/HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ref: refs/heads/master&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;理解这两条之后，「切换分支」为什么快也就清楚了：改一个文件里的一个哈希，然后把工作区的文件换成那个提交对应的内容。&lt;/p&gt;
&lt;h2 id=&#34;三git-里到底有什么&#34;&gt;三、&lt;code&gt;.git&lt;/code&gt; 里到底有什么&lt;/h2&gt;
&lt;p&gt;按重要性排：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;路径&lt;/th&gt;
          &lt;th&gt;作用&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;objects/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;对象库，所有 blob/tree/commit 都在这。松散对象直接放，打包后进 &lt;code&gt;pack/&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;refs/heads/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;本地分支（每个分支一个文件，一行哈希）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;refs/tags/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;标签&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;HEAD&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;当前在哪个分支&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;index&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;暂存区&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;logs/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;reflog，记录 HEAD 的移动历史（救命用的）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;config&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;仓库配置&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;worktrees/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;链接工作区的元数据，后面会重点讲&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;看对象库的实际占用，用这条命令：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git count-objects -vH
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它输出的是这样（这是我在那个真实仓库上跑出来的）：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;count: 1621                    # 松散对象的个数
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;size: 8.21 MiB                 # 松散对象占用
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;in-pack: 3574                  # 已打包对象的个数
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;packs: 2                       # pack 文件个数
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;size-pack: 71.54 MiB           # pack 占用
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;prune-packable: 17             # 已打包但松散副本还在的，可清
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;garbage: 0                     # 无效文件
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;size-garbage: 0 bytes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;size&lt;/code&gt; 加 &lt;code&gt;size-pack&lt;/code&gt; 大约是 80 MiB，正好对应那个仓库 &lt;code&gt;.git&lt;/code&gt; 目录的 82M。&lt;strong&gt;这个数字是健康的&lt;/strong&gt;：818 个跟踪文件的网站仓库，历史全加起来 82M。&lt;/p&gt;
&lt;p&gt;记住这个量级。等下你就知道 50G 跟它差了几个数量级。&lt;/p&gt;
&lt;h2 id=&#34;四worktreeai-为什么会给你长出-workspace&#34;&gt;四、worktree：AI 为什么会给你长出 workspace&lt;/h2&gt;
&lt;p&gt;现在进入正题。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt; 是 Git 2015 年就有的功能，官方定义是：一个&lt;strong&gt;独立的工作目录，有自己的文件和分支，但与主检出共享同一份仓库历史和远端&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;官方术语里，&lt;code&gt;git clone&lt;/code&gt; 出来的那个叫「主工作区」（main worktree），&lt;code&gt;git worktree add&lt;/code&gt; 出来的是「链接工作区」（linked worktree）。一个仓库有一个主工作区，加上零个或多个链接工作区。&lt;/p&gt;
&lt;p&gt;命令不多：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree add &amp;lt;路径&amp;gt; -b &amp;lt;新分支&amp;gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 建一个&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree list                     &lt;span style=&#34;color:#75715e&#34;&gt;# 列出全部&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree remove &amp;lt;路径&amp;gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 删一个&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree prune                    &lt;span style=&#34;color:#75715e&#34;&gt;# 清理已经不存在的记录&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree lock / unlock            &lt;span style=&#34;color:#75715e&#34;&gt;# 锁定/解锁（防误删）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为什么 AI 会用这个？因为&lt;strong&gt;并行&lt;/strong&gt;。你让 AI 同时干两件事，它需要两份互不干扰的文件，于是给每件事开一个 worktree。Claude Code 的官方文档把这条讲得很直白：每个会话一个 worktree，就有了一份独立的文件副本，一个会话做功能、另一个会话修 bug 不会互相踩。&lt;/p&gt;
&lt;h3 id=&#34;claude-code-的默认行为&#34;&gt;Claude Code 的默认行为&lt;/h3&gt;
&lt;p&gt;这是我认为每个用 AI 写代码的人都该知道的一段，因为官方文档自己写了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加 &lt;code&gt;--worktree&lt;/code&gt;（或 &lt;code&gt;-w&lt;/code&gt;）加一个名字，就会建一个隔离的 worktree 并在里面启动&lt;/li&gt;
&lt;li&gt;默认位置是仓库根目录下的 &lt;code&gt;.claude/worktrees/&amp;lt;名字&amp;gt;/&lt;/code&gt;，分支名是 &lt;code&gt;worktree-&amp;lt;名字&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;官方还给了一条提示：&lt;strong&gt;把 &lt;code&gt;.claude/worktrees/&lt;/code&gt; 加进 &lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt;，否则这些内容会作为未跟踪文件出现在你的主检出里&lt;/li&gt;
&lt;li&gt;然后是关键的一句：&lt;strong&gt;「worktree 是一次全新检出，所以要在里面初始化开发环境：让 Claude 装依赖，或者你自己在 worktree 目录里跑项目的安装命令。」&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后这句就是 50G 的起点。而在清理那一节，官方还写了另一句：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;非交互运行（&lt;code&gt;-p&lt;/code&gt;）没有退出提示，所以 Claude 不会清理它们的 worktree&lt;/strong&gt;，而且 Claude Code 在每个 worktree 上留了一把创建时拿的锁，直到后续某个会话的过期锁清扫才释放。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;「没有退出提示，所以不会清理」——这就是「AI 建了不删」的官方解释。这是设计上的取舍，谈不上 bug：无人值守跑的时候没人可问，于是干脆不删。&lt;/p&gt;
&lt;p&gt;Codex 也一样。&lt;code&gt;openai/codex&lt;/code&gt; 仓库里有一个独立的 &lt;code&gt;codex-rs/worktree/&lt;/code&gt; 模块（带 &lt;code&gt;src/&lt;/code&gt;、&lt;code&gt;tests/&lt;/code&gt;），说明它的 worktree 支持是内建能力，不是外挂。&lt;/p&gt;
&lt;h3 id=&#34;现在算账50g-是怎么来的&#34;&gt;现在算账：50G 是怎么来的&lt;/h3&gt;
&lt;p&gt;我在一个真实的网站仓库上做了实测。这个仓库 818 个跟踪文件：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;项&lt;/th&gt;
          &lt;th&gt;实测大小&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;工作区（不含 &lt;code&gt;.git&lt;/code&gt;）&lt;/td&gt;
          &lt;td&gt;265M&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;其中 &lt;code&gt;.git&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;82M&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;其中 &lt;code&gt;public/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;141M&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;其中 &lt;code&gt;resources/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;52M&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;建一个 worktree 后，主 &lt;code&gt;.git&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;仍是 82M&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;新建的 worktree 本身&lt;/td&gt;
          &lt;td&gt;72M&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两个结论值得记住。&lt;/p&gt;
&lt;p&gt;第一，worktree 不会让 &lt;code&gt;.git&lt;/code&gt; 翻倍。 建了之后主仓库的 &lt;code&gt;.git&lt;/code&gt; 还是 82M，因为对象库是共享的。这一点很重要，因为它意味着「worktree 很省」这个印象在 &lt;code&gt;.git&lt;/code&gt; 这一层是对的。它省是因为每个链接工作区里的 &lt;code&gt;.git&lt;/code&gt; 只是一个 51 字节的文件，内容是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gitdir: /opt/data/hugo-blog/.git/worktrees/wt-demo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一行指针，指回主仓库的对象库。对象存一份，多个工作区共用。&lt;/p&gt;
&lt;p&gt;第二，工作区文件是真复制。 新建的 worktree 是 72M，那是 818 个文件的完整检出。&lt;/p&gt;
&lt;p&gt;而真正让数字爆炸的是 &lt;code&gt;public/&lt;/code&gt; 那 141M 和 &lt;code&gt;resources/&lt;/code&gt; 那 52M：&lt;strong&gt;这些是构建产物，不是 Git 跟踪的文件&lt;/strong&gt;。所以新建的 worktree 一开始只有 72M，你一旦在里面跑一次构建或装一次依赖，它就会朝 265M 长。前端项目更极端，一个 &lt;code&gt;node_modules&lt;/code&gt; 通常几百 M 到 1G 以上。&lt;/p&gt;
&lt;p&gt;于是算一笔账：&lt;strong&gt;如果 AI 给你建了 50 个 worktree，每个都在里面装过依赖或跑过构建，就是 50 × 1G ≈ 50G。&lt;/strong&gt; 而主仓库的 &lt;code&gt;.git&lt;/code&gt; 始终只有几十 M。&lt;/p&gt;
&lt;p&gt;这就是那个 50G 的完整来源。它跟代码大小无关，也跟 Git 存了多少无关，它是&lt;strong&gt;工作区的副本数量乘以每个工作区里那些不受 Git 管理的部分的体积&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;五三步查清楚&#34;&gt;五、三步查清楚&lt;/h2&gt;
&lt;p&gt;别急着删，先看清楚状况。三条命令，从外到内：&lt;/p&gt;
&lt;p&gt;第一步是看目录体积的分布，目的是分清「Git 管的」和「你自己装出来的」：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;du -sh --exclude&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;.git .     &lt;span style=&#34;color:#75715e&#34;&gt;# 工作区总共多大&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;du -sh .git                 &lt;span style=&#34;color:#75715e&#34;&gt;# 历史占多少&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;du -sh * .&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;!.&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;* 2&amp;gt;/dev/null | sort -hr | head -15   &lt;span style=&#34;color:#75715e&#34;&gt;# 谁最大&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果最大的几个是 &lt;code&gt;node_modules&lt;/code&gt;、&lt;code&gt;public&lt;/code&gt;、&lt;code&gt;dist&lt;/code&gt;、&lt;code&gt;build&lt;/code&gt;、&lt;code&gt;target&lt;/code&gt;、&lt;code&gt;.venv&lt;/code&gt; 这类名字，那基本可以确定问题不在 Git，在重复的构建产物。&lt;/p&gt;
&lt;p&gt;第二步是看有哪些 worktree：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;正常输出是一行一个工作区，带路径、提交和分支。如果有十几行、路径在 &lt;code&gt;.claude/worktrees/&lt;/code&gt; 下面，那就是了。&lt;/p&gt;
&lt;p&gt;第三步是看对象库有没有异常膨胀：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git count-objects -vH
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对照前面那张字段表。重点看两个：&lt;code&gt;garbage&lt;/code&gt; 和 &lt;code&gt;size-garbage&lt;/code&gt; 应该接近 0；&lt;code&gt;prune-packable&lt;/code&gt; 有点数是正常的。如果 &lt;code&gt;size&lt;/code&gt;（松散对象）很大，说明很久没做过垃圾回收。&lt;/p&gt;
&lt;h2 id=&#34;六安全清理的顺序&#34;&gt;六、安全清理的顺序&lt;/h2&gt;
&lt;p&gt;按这个顺序做，每一步都可以先看再做。&lt;/p&gt;
&lt;p&gt;第 1 步，先看，再决定删哪个：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第 2 步，目录已经手动删掉的，用 prune 清记录。先 dry-run：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree prune -n -v     &lt;span style=&#34;color:#75715e&#34;&gt;# -n 是 dry-run，只看会删什么&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree prune -v        &lt;span style=&#34;color:#75715e&#34;&gt;# 确认后再真删&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一步值得单独说，因为它对应一个很常见的状态。我实测过：直接 &lt;code&gt;rm -rf&lt;/code&gt; 掉 worktree 目录之后，&lt;code&gt;git worktree list&lt;/code&gt; &lt;strong&gt;仍然会列出它&lt;/strong&gt;，并且标注 &lt;code&gt;prunable&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/opt/data/hugo-blog  4a2097c [master]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/tmp/wt-demo         4a2097c [worktree-demo] prunable
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;prune -n&lt;/code&gt; 会告诉你它准备删什么：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Removing worktrees/wt-demo: gitdir file points to non-existent location
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也就是说：&lt;strong&gt;手删目录之后 Git 不知道，那些元数据会一直留着&lt;/strong&gt;，让 &lt;code&gt;git worktree list&lt;/code&gt; 的输出越来越长，你也越来越看不清哪个是活的。&lt;/p&gt;
&lt;p&gt;第 3 步，正常的删除方式（推荐）：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree remove &amp;lt;路径&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果它报错说 worktree 被锁了（无人值守的 AI 会话留下的锁），先解锁：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree unlock &amp;lt;路径&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git worktree remove &amp;lt;路径&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第 4 步，清完之后做一次垃圾回收：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git gc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第 5 步，关于 &lt;code&gt;git clean&lt;/code&gt;，必须加一句警告。 它会删掉未跟踪的文件，而 &lt;code&gt;node_modules&lt;/code&gt;、&lt;code&gt;.env&lt;/code&gt;、你还没提交的新文件全都属于「未跟踪」。所以&lt;strong&gt;永远先跑 dry-run&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git clean -nd    &lt;span style=&#34;color:#75715e&#34;&gt;# -n = dry run，只列出会被删的东西&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git clean -fd    &lt;span style=&#34;color:#75715e&#34;&gt;# 确认清单没问题之后再执行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;git clean -fdx&lt;/code&gt; 里的 &lt;code&gt;-x&lt;/code&gt; 更狠，连 &lt;code&gt;.gitignore&lt;/code&gt; 里忽略的文件也一起删。它是清 &lt;code&gt;node_modules&lt;/code&gt; 最快的办法，也是误删本地配置最快的办法。&lt;/p&gt;
&lt;p&gt;第 6 步，顺手做一件预防的事。按官方提示，把 AI 的 worktree 目录加进 &lt;code&gt;.gitignore&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.claude/worktrees/&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; .gitignore
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样它们至少不会污染 &lt;code&gt;git status&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&#34;七为什么这件事本来不该发生&#34;&gt;七、为什么这件事本来不该发生&lt;/h2&gt;
&lt;p&gt;最后说一个判断，因为我觉得它比上面所有命令都重要。&lt;/p&gt;
&lt;p&gt;在 Git 的模型里，&lt;strong&gt;工作区是可以随时重建的&lt;/strong&gt;。第一节那张表里第三行 &lt;code&gt;git checkout&lt;/code&gt; 就是这件事：从仓库里重新生成文件。这个设计前提意味着，重建一个工作区的成本应该很低，低到「用完就扔」是自然的做法。&lt;/p&gt;
&lt;p&gt;而 AI 把「建」变得极便宜：一条命令。但它没有把「清」变得更便宜，因为清理需要知道&lt;strong&gt;这个工作区里有没有还没提交的东西&lt;/strong&gt;，那是需要判断的，不能自动化。于是建和清的成本出现了不对称，结果就是只增不减。&lt;/p&gt;
&lt;p&gt;所以新手真正需要跨过的那道坎，在于知道&lt;strong&gt;那条分界线在哪&lt;/strong&gt;：哪些目录是 Git 管的，哪些是你自己装出来的。命令本身不重要。&lt;/p&gt;
&lt;p&gt;这条线就是 &lt;code&gt;.gitignore&lt;/code&gt; 的边界。线的里面，删了能从仓库重建，随便删；线的外面（&lt;code&gt;node_modules&lt;/code&gt;、构建产物、&lt;code&gt;.env&lt;/code&gt;），删了要重新装、重新配，Git 帮不了你。&lt;strong&gt;50G 全部来自线的外面。&lt;/strong&gt; 而 &lt;code&gt;du -sh * | sort -hr | head&lt;/code&gt; 这条命令，就是把这条线画出来的工具。&lt;/p&gt;
&lt;p&gt;从这个角度回头看第一节那张三个区域的表，你会发现它不只是为了应付面试。它决定了你面对一个陌生的项目目录时，能不能在三秒内说出「这一半是代码，那一半是产物」。&lt;/p&gt;
&lt;h2 id=&#34;写在最后&#34;&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这篇的起点是一个很具体的麻烦，但落点是一条分界线。Git 的基础知识之所以值得学，不是因为你得手敲命令（AI 可以替你敲），而是因为&lt;strong&gt;没有它，你就没有能力验收 AI 做的事&lt;/strong&gt;。AI 建了 50 个 worktree 不删，这个行为本身在 Git 的模型里是合理的，不合理的是没人能判断出该删哪些。&lt;/p&gt;
&lt;p&gt;如果只想记一件事，就记这条：&lt;strong&gt;出问题先跑 &lt;code&gt;du -sh * | sort -hr | head&lt;/code&gt;，看最大的那几个是不是构建产物。&lt;/strong&gt; 是的话，问题跟 Git 无关，跟没人清理有关。&lt;/p&gt;
&lt;p&gt;最后交代证据边界：文中 Claude Code 和 Git 的行为描述来自两边官方文档；磁盘数字和 &lt;code&gt;prunable&lt;/code&gt; 那个状态是我在一个真实的网站仓库上实测的，命令和输出都贴在正文里，可以自己复现。&lt;/p&gt;
&lt;p&gt;相关阅读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261015-agent-scale-version-control-scion/&#34; &gt;《AI 一天合 3 万次代码，把 Git 逼到了极限》&lt;/a&gt;：那篇讲的是 Git 在 Agent 高并发下的基础设施压力，这篇讲的是同一套工具在你本机上留下的痕迹，两个方向不重叠。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261024-omarchy-linux-codex-opencode-claude-setup/&#34; &gt;《为什么是 Omarchy 而不是别的 Linux》&lt;/a&gt;：如果准备给 agent 配一台干净的开发机，那篇讲了系统和三个 agent 的完整安装路径。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261024-claude-code-review-six-layers/&#34; &gt;《Claude Code 团队的代码审查方法》&lt;/a&gt;：同一个工具的另一面，那篇讲的是团队层面怎么让 AI 写的代码可验收，这篇讲的是个人层面怎么让自己看得懂 AI 做了什么。&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
    </channel>
</rss>
