2505 字
13 分钟
远程主机上的代码,别隔着 SSH 改 —— 先 clone 到本地

今天给我的 CLAUDE.md(AI coding agent 的全局规则文件)加了一条规则:

当需要读/改的代码在某台 SSH 主机上时,优先 git clone 拉到本地再操作,别在远程机器上隔着 SSH 干活。

起因很具体。我让 agent 改一台远程机器上的服务代码,它的第一反应是 ssh host 'cat /path/to/file.py' 把文件读出来,看完想改,又准备拼一条 ssh host 'sed -i ...' 推回去。这是一条典型的反模式——不光 agent 会这么干,人手动操作远程代码时也常常顺着这个惯性走。

值得把背后的道理讲清楚,所以记一篇。

隔着 SSH 改远程代码,到底难受在哪#

问题的本质是:你的工具链在本地,代码在远程,每次跨越这道边界都要付一次往返成本。

最直接的损失是工具失效。本地有一整套高效的文件操作能力——全文检索、跨文件跳转、批量替换、结构化编辑。一旦代码在远程,这些全用不了,只能退化成把命令字符串塞进 SSH:

Terminal window
# 想全局搜一个符号,本地一条 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:

Terminal window
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 有两种写法,等价但有区别,踩过坑的人才知道:

Terminal window
# 写法一:scp-like 简写
git clone user@host:path/to/repo.git
# 写法二:完整 URL
git 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 形式:

Terminal window
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:

Terminal window
git clone --depth 1 user@host:repo.git /tmp/work

大仓库这能省下可观的带宽和时间(历史很长的仓库能快上一个数量级)。代价是 git loggit blamegit 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 进去。

小结#

这条规则本身很短,但它压缩了一个实用判断:跨越本地与远程的边界是有成本的,能一次跨过去就别每次都跨。具体到操作:

  1. 远程代码在 git 仓库里 → git clone user@host:/abs/path/repo.git /tmp/work,需要省时间加 --depth 1
  2. 非标准端口用 ssh://user@host:PORT/path 完整 URL 形式;
  3. 本地改完、跑过测试,再 push 回去;非裸仓库的当前分支推不动,记得推到非当前分支或用裸仓库;
  4. 只有”机器状态”类的事(日志、进程、运行时)才直接 SSH。

写给 agent 的规则,本质上也是写给自己的工作流约定。把惯性里那条”隔着 SSH 凑合改”的路堵掉,默认走 clone,省下的是一连串看不见的网络往返和盲改风险。

远程主机上的代码,别隔着 SSH 改 —— 先 clone 到本地
https://blog.lishuyu.app/posts/远程代码先clone到本地/
作者
猫猫魔女
发布于
2026-06-07
许可协议
CC BY-NC-SA 4.0