今天给我的 CLAUDE.md(AI coding agent 的全局规则文件)加了一条规则:
当需要读/改的代码在某台 SSH 主机上时,优先
git clone拉到本地再操作,别在远程机器上隔着 SSH 干活。
起因很具体。我让 agent 改一台远程机器上的服务代码,它的第一反应是 ssh host 'cat /path/to/file.py' 把文件读出来,看完想改,又准备拼一条 ssh host 'sed -i ...' 推回去。这是一条典型的反模式——不光 agent 会这么干,人手动操作远程代码时也常常顺着这个惯性走。
值得把背后的道理讲清楚,所以记一篇。
隔着 SSH 改远程代码,到底难受在哪
问题的本质是:你的工具链在本地,代码在远程,每次跨越这道边界都要付一次往返成本。
最直接的损失是工具失效。本地有一整套高效的文件操作能力——全文检索、跨文件跳转、批量替换、结构化编辑。一旦代码在远程,这些全用不了,只能退化成把命令字符串塞进 SSH:
# 想全局搜一个符号,本地一条 grep 的事,远程变成:ssh user@host "grep -rn 'def handle_request' /opt/app/"每搜一次是一个网络往返。想看上下文?再来一次。想看相邻文件?再来一次。一个在本地几秒钟用 grep 跳来跳去就能摸清的调用链,隔着 SSH 要敲十几条命令,中间还夹着 SSH 的连接延迟。
改文件更糟。远程编辑有两条路,都不好走:
一是 ssh host 'sed -i ...'。sed 的转义本来就难写,再套一层 SSH 的 shell 引号,双重转义,稍复杂的替换基本写不对,而且改错了没有撤销——-i 是原地覆盖。
二是 SSH 进去开 vim。能改,但你失去了所有本地编辑器的能力,也没法把改动当成一个可审阅的整体 diff 来看。
最关键的一点:你没法在远程方便地验证。 改完想跑个测试、跑个 lint、确认没改坏,这些都需要完整的项目环境和工具。隔着 SSH,你既看不到全貌,也跑不顺验证,改动就变成了”盲改”。
正确做法:clone 到本地,把边界一次性跨过去
与其每次操作都跨一次边界,不如把代码整体搬到本地,之后所有操作都在本地完成,改完再一次性推回去。边界只跨两次:拉下来、推回去。
如果远程代码在一个 git 仓库里,直接 clone:
git clone user@host:repo.git /tmp/work我习惯落到 /tmp/ 下——临时工作区,不污染家目录,也避开 iCloud 同步的目录(macOS 上别往 ~/Desktop 写工作产物)。
clone 完之后,全套本地工具立刻回来了:检索、跳转、批量重构、跑测试、跑 lint,全部正常。改动是一个干净的 git working tree,每一步都能 git diff 审阅,改错了能 git checkout 撤销。这才是正常的开发状态。
git over SSH 的语法,有两个坑
clone 远程仓库走 SSH 有两种写法,等价但有区别,踩过坑的人才知道:
# 写法一:scp-like 简写git clone user@host:path/to/repo.git
# 写法二:完整 URLgit clone ssh://user@host/path/to/repo.git第一个坑是端口。 如果远程 SSH 不是默认的 22 端口,scp-like 简写没法指定端口。你可能会想当然地写成 user@host:2222/repo.git,但 scp-like 语法里冒号的唯一含义是”分隔主机和路径”,冒号后面的 2222/repo.git 会被整个当成远程路径,而不是端口 2222。结果就是 git 用默认端口去连,连不上,或者连上了找不到路径。
要指定端口,必须用完整 URL 形式:
git clone ssh://user@host:2222/path/to/repo.git第二个坑是路径基准不一样。
- scp-like 的
user@host:repo.git(冒号后无前导斜杠)是相对远程用户 home 目录的路径; - scp-like 的
user@host:/opt/app/repo.git(有前导斜杠)才是绝对路径; ssh://user@host/opt/app/repo.git里/开头的路径就是绝对路径,要引用 home 目录可以写ssh://user@host/~/repo.git。
记混了就会 clone 到一个不存在的路径上报错。最稳的办法是直接复制远程那个仓库的绝对路径,用带前导斜杠的形式。
仓库很大就浅克隆
如果只是要改代码、不需要翻历史,可以用浅克隆只拉最新一个 commit:
git clone --depth 1 user@host:repo.git /tmp/work大仓库这能省下可观的带宽和时间(历史很长的仓库能快上一个数量级)。代价是 git log、git blame、git bisect 这些依赖历史的操作会受限——浅仓库里它们看不到过去。如果改着改着发现需要历史,再 git fetch --unshallow 补全即可。日常长期开发不建议浅克隆,一次性改个东西很合适。
改完 push 回去,这里又有一个坑
本地改完、测试过了,推回远程。如果远程是一个裸仓库(bare repo,专门做远端的那种),直接 git push 没问题。
但如果远程仓库本身就是工作目录——也就是你刚才 clone 的源头是一个非裸仓库(non-bare,带 working tree、有文件 checkout 出来的那种)——而且它当前 checkout 的就是你要 push 的分支,那么 push 会被拒绝:
! [remote rejected] main -> main (branch is currently checked out)这是 git 的默认保护,由 receive.denyCurrentBranch 控制,默认值 refuse。原因是:往一个已经 checkout 的分支 push,会让远程的 working tree(磁盘上的文件)和 HEAD 不一致——push 只更新仓库的引用,不会自动更新磁盘文件,远程那边就处于一个错乱状态。
正确的处理方式有几种:
- 推到一个非当前分支,再 SSH 进去
git merge; - 或者远程本来就该是个裸仓库,工作目录另开一个 clone;
- 实在要直接覆盖,可以在远程设
receive.denyCurrentBranch = updateInstead(push 后自动更新 working tree),但这只在远程 working tree 干净时才生效,不推荐随手用。
理解了这一层,就知道为什么”远程开发服务器”和”代码仓库”通常是分开的——仓库做裸仓库,部署目录从仓库 checkout,职责清晰。
那 sshfs、rsync、Remote-SSH 呢
有人会说,远程文件还有别的玩法。确实有,但针对”操作远程代码”这个场景,它们各有硬伤:
sshfs:用 FUSE 把远程目录挂载成本地文件系统,看起来最优雅——本地工具直接操作挂载点就行。问题是所有 I/O 都实打实地走 SSH 加密通道,延迟和开销都大,在上面跑全文检索或者让编辑器索引项目会非常卡。更烦的是连接脆弱,切换网络、机器休眠唤醒之后挂载点经常变成 Transport endpoint is not connected,得手动重挂。它也不是版本控制,改动没有 diff、没有撤销。
rsync:高效同步增量差异,适合做部署、做备份。但它不是为开发工作流设计的——没有版本控制,没有冲突检测,ad-hoc 用很容易把方向搞反、覆盖掉不该覆盖的文件。当成”把代码拉下来”的一次性手段可以,但它不如 git clone,因为 clone 天然带着完整的版本控制能力。
VS Code Remote-SSH / JetBrains Gateway:本地编辑器 UI 连远程 backend,代码实际跑在远程。对人来说这是很好的远程开发方案,连接稳定时体验接近本地。但它依赖在远程装一个 server 进程,依赖稳定的长连接,断网就断开;而且对 AI agent 这种命令行工作流没有意义——agent 要的是能用本地文件工具直接读写的文件,而不是一个图形编辑器的远程会话。
把它们和 git clone 放一起比,结论很清楚:只要代码在 git 仓库里,clone 到本地就是成本最低、能力最全、最不容易出错的方案。 它天然解决了”本地工具能不能用”和”改动怎么审阅、怎么回滚、怎么推回去”这两个核心问题,而其他方案要么牺牲性能,要么牺牲版本控制。
什么时候才该直接 SSH
规则要留退路,不能写死。以下几种情况,老老实实 SSH 进去更合适:
- 代码不在 git 仓库里——散落的脚本、手改过没提交的配置、临时文件,没东西可 clone。
- 要看的是运行时状态——进程在不在、日志在刷什么、环境变量、端口监听、磁盘占用。这些是机器的实时状态,不在代码仓库里,只能现场看。
- 数据/产物太大不适合拉本地——几十 GB 的数据集、构建产物,clone 下来不现实,该在远程处理就在远程处理。
判断标准其实就一句:你要操作的是”代码的内容”,还是”机器的状态”。 前者 clone 到本地,后者 SSH 进去。
小结
这条规则本身很短,但它压缩了一个实用判断:跨越本地与远程的边界是有成本的,能一次跨过去就别每次都跨。具体到操作:
- 远程代码在 git 仓库里 →
git clone user@host:/abs/path/repo.git /tmp/work,需要省时间加--depth 1; - 非标准端口用
ssh://user@host:PORT/path完整 URL 形式; - 本地改完、跑过测试,再 push 回去;非裸仓库的当前分支推不动,记得推到非当前分支或用裸仓库;
- 只有”机器状态”类的事(日志、进程、运行时)才直接 SSH。
写给 agent 的规则,本质上也是写给自己的工作流约定。把惯性里那条”隔着 SSH 凑合改”的路堵掉,默认走 clone,省下的是一连串看不见的网络往返和盲改风险。