退出会话时,它删掉了你没提交的代码

你同时开了三个会话并行干活,其中一个正在改一个文件——改到一半,还没提交,你想先睡一觉明天接着弄。你退出会话,顺手关掉终端。第二天回来,.worktrees/ 里那个目录不见了,分支也没了。
这不是你记错了目录。worktree: true 的退出清理有一条判据漏了:它只看提交,不看文件。
先看 worktree 模式在做什么
用 hermes -w(或者在配置里写 worktree: true)启动会话时,每个 CLI 会话会在仓库的 .worktrees/ 下开一棵独立的工作树,配一条自己的分支:
worktree: false # 默认:只有传 -w 时才开
worktree_sync: true # 默认从刚 fetch 到的远端 tip 开分支;false 则从本地 HEAD
这样多个 agent 并行干活时,各自提交、推送、开 PR,互不打架。默认还特意从远端最新 tip 拉分支,让你开出来的 PR 只包含自己那点改动,而不是继承了本地克隆落后的那一堆提交。
问题出在退出那一步。
文档承诺的和代码做的,不是一回事
配置文档里关于 git worktree 隔离的那一段写得很清楚:
Clean worktrees are removed on exit; dirty ones are kept for manual recovery. (干净的工作树在退出时删除;脏的保留下来供手动恢复。)
而 cli.py 里的 _cleanup_worktree 是这么写的——它自己的注释就是判据:
def _cleanup_worktree(info: Dict[str, str] = None) -> None:
"""Remove a worktree and its branch on exit; kept only when it has unpushed commits."""
「有未推送的提交」才保留。判断函数 _worktree_has_unpushed_commits() 跑的是:
git log --oneline HEAD --not --remotes
也就是比较「HEAD 上有哪些提交是远端没有的」。工作区状态从头到尾没有被读过一次。 于是:
| 退出时工作树里有什么 | 实际结果 |
|---|---|
| 新建的未跟踪文件,从未提交 | 工作树被删除 |
| 已跟踪文件被修改,未提交 | 工作树被删除 |
| 有提交但未推送 | 工作树保留 |
最后那一步是 git worktree remove <路径> --force 加 git branch -D <分支>。--force 的含义就是「忽略工作区里未提交的改动」。
为什么会有两种判据
Hermes 里其实有两条处理工作树的路径,而且它们的标准不一样。
一条是退出清理,在 cli.py,也就是上面这个——只认提交。
另一条是启动时的瘦身,在 hermes_cli/worktree_ops.py,它懂「脏」这个概念:_worktree_is_dirty() 跑 git status --porcelain(包括未跟踪文件)来判断,出错时偏向「脏」;分类结果直接记成 dirty,并且属于 _PRESERVE_REASONS(保留原因)。那段代码的注释写得很直白:
Guards at every tier and age: dirty trees are never removed. (每个档位和年龄都设了护栏:脏工作树永不被删。)
所以这不是「Hermes 不懂区分脏与干净」,而是两条路径的判据不一致:后台瘦身守着脏文件,退出清理只守着提交。用户踩到的正是后者。
怎么保护自己
在这条判据补齐之前,下面是能立刻用上的做法。
- 退出前提交一次,哪怕只是 WIP。 未推送的提交会让工作树被保留:
git add -A && git commit -m "wip: 还没写完" - 或者 stash,并且带上未跟踪文件。
git stash -u里的-u很关键——没有它,新文件仍然只存在于工作树里。 - 别把「会话还开着」当成文件安全。 未提交的改动只在会话存活期间是安全的,一退出就进入被
--force清掉的风险区。 - 并行会话时随手看一眼状态。
git -C .worktrees/<名字> status --porcelain,输出非空就说明这棵树在退出时会被删。 - 让会话里的 agent 自己提交并推送。 worktree 会话本来的设计就允许 agent 提交、推送、开 PR;推上去之后,退出清理会因为「有未推送的提交」这条判据而保留这棵树,你也就多了一层保险。
- 定期清点。 检查 v0.20.5 引入的 worktree 体检那一套逻辑覆盖了什么、没覆盖什么,心里有数比事后找文件强。
如果已经发生了: 先 git worktree list 和 git branch 看分支还在不在——只要退出前有过哪怕一次提交,那部分内容还在 git 对象库里,可以从分支或 reflog 找回来。但未提交、未跟踪的文件被 --force 删除之后,git 这一层没有恢复路径;能不能找回来取决于你的编辑器缓冲、系统快照或者备份,而不是 git。这一点必须说清楚,别抱着侥幸心理。
现状
issue #129482 于 2026-09-30 提交,标记为 P1,报告时的代码版本是 41755854,截至本文写成时仍处于打开状态。合理的修复方向就是让退出清理也接上 _worktree_is_dirty(),和启动瘦身用同一套判据;在这之前,请按上面第 1、2 条操作。
顺带说一句:这类「看似干净、其实丢东西」的坑,Hermes 里已经修过好几个。嵌套仓库回滚护栏解决的是回滚时点到没被快照的目录,会话边界讲的是会话之间到底共享什么——都值得纳入你的日常习惯。
小结
这一次的教训不是「worktree 不能用」,而是删除操作要按最保守的判据来:判断「这棵树能不能删」时,看工作区里有没有东西,比看提交历史里有没有东西要重要得多。一个未提交的半成品文件,价值往往高于十个已经推送的提交。
在代码补齐这条判据之前,最省事的自保方式只有一句话:退出前先提交,或者先 stash。