先说这篇文章跟站内那几篇沙箱拆解的分工。我们写过 Google Agent Compute Engine、Anthropic 的 srt 和 Replit 换掉容器的 microVM 引擎,那三篇讲的都是别人怎么在云上造沙箱。这篇不同:devvm 装在你自己的机器上,cd 进项目目录就得到一台只挂这个目录的 VM,agent 在里面跑。
它的价值不在隔离技术有多新,在于它把隔离从一个需要主动去做的事,变成了你本来就有的那个动作。你总要进项目目录开始干活。
先说它解决的具体问题
你在项目里跑 npm install,跑测试,或者让 Claude Code 自己动手改代码。这些代码以你的身份运行,读得到 ~/.aws/credentials、~/.ssh/id_rsa、~/.config/gh/hosts.yml,也读得到你的其他项目。npm install 的执行权和你本人的执行权,在操作系统眼里是同一件事。
devvm 把这批动作搬进一台跑着自己内核的 VM,只挂载当前项目目录。官方文档的措辞是,跑在 VM 里的代码读不到你的凭据,不会在这台机器上触发 git hook,也够不到你的其他项目。
它是什么,以及现在的状态
devvm 由 devvmhq 维护,MIT 许可,Go 写的 CLI,VM 跑在 Lima 上,中间隔了一层 backend 接口以便换实现。
我自己核过仓库的数字,比二手文章里的新:637 颗星,2026 年 11 月 6 日建库,也就是说写这篇时它只有九天大。版本号 0.1.1-rc.3 挂在 main 上,正式 release 是 v0.1.0。仓库里还留着 testdata/v<release>/ 这样的冻结状态目录,这是我在别人仓库里不常见到的做法。
装起来
macOS 走 Homebrew,Linux 走官方的安装脚本:
# macOS:装 devvm,顺便装 Lima
brew trust --formula devvmhq/tap/devvm
brew install devvmhq/tap/devvm
# Linux(脚本在 macOS 上也能用)
curl -fsSL https://github.com/devvmhq/devvm/releases/latest/download/install.sh | bash
两条路选一条,不要都装,官方明写装了两次会得到两份 devvm。脚本在装 Lima、QEMU 和申请 /dev/kvm 权限之前都会问一句;它自己不需要 sudo,并且在装任何东西之前先校验下载件的 checksum 和签名。它接受几个参数,写成 bash -s -- 后面接:--yes 全部回答是、--version 0.1.0 装指定版本、--no-deps 只装 devvm、DEVVM_PREFIX=/some/dir 换安装位置。
装完跑一次体检:
devvm doctor
它列出 Lima 需要的每一样东西和缺的那几样,每样配一个该怎么做,最后打警告(比如内存低于推荐值)。官方文档有一句我觉得值得单独拎出来:devvm 自己从不在你的机器上装东西,每个需要 backend 的命令都会先检查一遍,缺东西就停在第一个缺的地方。
Linux 侧的实际门槛是这些:arm64 或 x86_64、建议 8 GiB 内存加 4 核、Lima、QEMU、能访问 /dev/kvm(官方给的解法是 sudo usermod -aG kvm $USER 然后重新登录)、OpenSSH 8.7 以上。另外第一台 VM 需要 8 GiB 空闲磁盘,因为要放 base 镜像、VM 自己的镜像和共享工具。机器小一点也能跑,devvm 会给 base 少分一些 CPU 和内存。
第一台 VM,以及在 VM 里跑 agent
cd ~/some-project
devvm # 显示这台 VM 会拿到什么,确认或自定义
(devvm) ~/some-project $ claude
提示符前面的 (devvm) 就是信号:你现在站在 VM 的 shell 里。agent 要在这里启动,因为它在哪里启动就运行在哪里。
选 agent 有三种写法:自定义 VM 时在列表里勾、devvm --agents claude,codex 直接指定、已经建好的 VM 用 devvm tools 或 devvm tools add opencode。官方文档的表格里列了三个,Claude Code(claude)、Codex(codex)、opencode(opencode);仓库 README 里还提到 pi 和 Cursor CLI。
有个细节值得注意:这些 agent 是共享工具,只读挂载进 VM,所以每个 VM 都关掉了它们的自动更新和遥测。Claude Code 那边是 DISABLE_AUTOUPDATER=1 加 telemetry off,opencode 那边是 OPENCODE_DISABLE_AUTOUPDATE=1。版本由 devvm 统一管。
凭据为什么能留在外面
这是整个设计里最值得拆的一层,也是我认为它比「开个 VM」多出来的那部分。
devvm 把所有凭据留在宿主上:你的 GitHub token、Claude Code 的 token、Codex 和 opencode 的登录、Claude Code 的 MCP 密钥。VM 里拿到的是占位符。
真实的请求路径是这样。VM 里的进程向 api.github.com 发请求,带的是占位符;请求只能走 devvm 在这台机器上为它起的代理;代理把占位符换成真 token 再发出去;响应原路回来。
官方文档给了一张流程图描述这个过程,而设计上最关键的一句是:只有代理会把真值放进请求,所以任何绕过代理、从 VM 直连出去的请求,带的是占位符,那个 host 会拒绝它。也就是说,「我用不了真凭据」和「我绕不过代理」是同一个机制的两面,而不是两条独立的防线。
这也解释了 open 层为什么危险。open 是唯一不被限制在代理里的层,它直接跑在 backend 自己的网络上,能到这台机器的 localhost 和你的局域网。devvm 对它的处理很克制:环境变量照样设代理,NO_PROXY 里写着 host.lima.internal(VM 眼中的宿主),每次启动会检查代理是否设上并且能应答,不满足就警告你,然后照样把 VM 开起来。
网络四层:默认那一档通常就是对的那一档
新 VM 默认拿到 allowlist 层。四层从松到严是这样:
open:这台机器能到的它都能到,含宿主的localhost和局域网,代理不过滤。唯一不被限制的层。internet:所有公网 host、任意端口,除了你明确 deny 的;不碰宿主地址和宿主网络。allowlist:只到你白名单上的 host,除了你 deny 的。none:只到它自己 agent 的 host、它 secret 和登录的 host,以及它的 VM 间链接。
切换的命令是 devvm network internet 这种形式,新 VM 用 --network TIER 指定,想让所有新 VM 换默认值就 devvm network -g none。在受限层之间切换立刻生效(代理带着新规则重启,VM 不停);换进或换出 open 层则要等下一次启动。
allowlist 的规则是按顺序读成一张表:devvm 对所有 VM 的内置规则(OS 包源、mise 的版本列表、Sigstore 和 GitHub 的信任根、Node 下载、GitHub 本身),然后是 VM 镜像里每个共享工具的规则,再是每个内置层的规则(比如 docker 层放行 Docker 的包源和 Docker Hub、ghcr.io),最后才是你自己的全局策略 ~/.config/devvm/policy 和这台 VM 专属的那份。你用 devvm network allow 加的白名单挂在最后两层。
三层之外的实用性有两处要提前知道。一是每个 agent 的允许名单是逐个列明的,Claude Code 是 api.anthropic.com、claude.ai、downloads.claude.ai、platform.claude.com、mcp-proxy.anthropic.com;Codex 是 api.openai.com、chatgpt.com、auth.openai.com;opencode 只放行了它自己的服务,模型 provider 的 API 你得自己 devvm network allow。二是 none 层只放行 VM 里 agent 需要的 host,所以如果你用 opencode 配了第三方的模型端点,none 会拦掉它。
怎么确认它真的在起作用
一篇上手文如果不告诉你验证方法,你装完只会有一个模糊的安心感。devvm 在这一块给的东西挺具体,我把它们整理成一套动作。
装完先看 devvm doctor,它只报告缺什么,不缺就不吭声,所以它的空输出本身就是通过。
VM 建好后看 devvm ls,它会显示每台 VM 的网络层,取值是 open、internet、allowlist、none 之一。如果你看到的是 unenforced,那说明这台 VM 当前没被限制住,前面说的每次启动自检没过。
想看这台 VM 的白名单到底放行了什么,直接跑 devvm network,它先打印层,再把那台 VM 的规则列出来。要加一条就 devvm network allow <host>,全局的加 -g。想让新 VM 都换一个默认层,devvm network -g allowlist 这种形式;devvm network -g --json 会把结果打成 policy 加 source 两个字段,方便你脚本里读它到底是从哪来的。这里有个容易踩的地方:config.toml 里的 network 会压过你用 -g 存下的默认值,一旦你在那个文件里写了,devvm network -g 会拒绝改并告诉你文件在哪。
真正被拦的时候,在 VM 里跑 devvm-why,它显示代理最近拦了什么。这个命令我是从官方文档读到的,它比让你去翻代理日志实在得多,也是我建议你第一次配 opencode 时先用一下的命令,因为它能直接把「模型端点没在白名单里」这件事指出来。
最后一条验证在 VM 外面做:从一个受限层的 VM 里试着直连公网。按官方说法,受限的 VM 只够得到自己那个代理,直连会失败。这条能自己验,不需要看文档的脸色。
容器和 VM:这条边界到底画在哪
这个工具的名字里有个 test,容易让人把它跟 Docker 归到一类。它们画的线不一样,值得说清楚。
容器隔离的是命名空间和 cgroup,容器里的进程和宿主共享同一个内核。所以内核漏洞面是共享的,容器逃逸这个类别就是这么来的。我们之前在 Replit 那篇里算过一笔账:把共享内核的容器换成每个沙箱一颗内核的 microVM 之后,需要防的内核 CVE 从 6,211 条掉到 240 条以内,躲掉了 96.1%。
devvm 选的是 VM 这一侧。它跑在 Lima 上,VM 有自己的内核,官方安全文档最后一行专门把这件事拎出来当前提:这里的一切都建立在 VM 的边界上,VM 运行的是自己的内核,与这台机器分开。
这条选择换来的是更硬的边界,代价你也看得到。一台 VM 要 8 GiB 内存和 8 GiB 磁盘起步,启动和文件共享都要过一层虚拟化,而你必须自己解决「怎么在宿主和 VM 之间来回」的问题。devvm 的解法是把项目目录以相同路径挂进去,再加 --clone 这条更严的路。
所以判断标准其实很简单。如果你要防的是「这段代码可能读到我不该给它看的东西」,VM 边界是对的层级,容器的共享内核解决不了这个问题。如果你要的只是「依赖别装乱我的宿主机」,容器更划算。
agent 的配置和历史是分开的
这一层容易忽略,但如果你同时用好几个 agent,它是省事的那部分。
devvm 在宿主上维护一个自己的 agent store,分成两个目录。设置放 ~/.config/devvm/agents/<agent>/,登录放 ~/.local/share/devvm/logins/<agent>/,权限是 700 的目录配 600 的文件,macOS 上还会把这个登录目录排除出 Time Machine。每个 VM 从 store 里拿设置和登录。
重点在于它不读你 home 目录里 agent 自己的文件,也就是 ~/.claude、~/.claude.json、~/.codex、~/.config/opencode 这些。所以一台 VM 拿到的设置和登录,跟你这台机器上装没装那个 agent 无关。唯一会去读那些文件的是 devvm agent import,而且它只拷设置和登录,不拷历史,也不拷 ~/.claude/.credentials.json;从 ~/.claude.json 里只留设置、账号状态和 MCP server,你的 API key 和按项目的条目不会进 store。
历史是另一回事。每个 VM 的 transcript、session 和数据库都留在那个 VM 里,不跨 VM 共享。要搬的话有 devvm history export 打成一个 tar.gz,导入时会有一次 diff 和确认。
最后有个我觉得设计得很聪明的地方。每台 VM 里都有一份写给 agent 读的 devvm 说明,在 /usr/share/doc/devvm/README.md,VM 里跑 devvm-help 就能打印。它告诉 agent 这台 VM 是什么、网络层和凭据怎么工作、被拦了该请你运行哪条命令,比如 devvm network allow 或 devvm agent login。这份文件属于 root 且只读,VM 用户不用 sudo 改不动它,用 sudo 改的也只撑到下次启动,因为每次启动发现它和 devvm 自带的不一样就会重装。让 agent 知道自己在哪,同时又不能让 agent 自己改写这句话,这个平衡不容易做。
一个要单独提醒的局限:.git 保护只有 macOS 有
官方在「已知局限」里明写:Linux 宿主没有 .git 保护。
在 macOS 上 devvm 会把 .git 的 hooks 和 config 锁住,并且会告诉你锁拦不住什么。注意这个措辞,它没有说锁住了,说的是锁住这些、另外这些锁不住、我告诉你。而在 Linux 上这一层不存在,所以如果你在 Linux 上跑,.git/hooks 仍然是可以被 VM 里的代码写入的,而它会在你用宿主的 git 做操作时执行。
想要更硬的做法是 --clone。VM 拿到的是仓库自己的一个克隆,你的目录根本不被挂载,它写了什么都不进你的目录和 .git,你要用 git fetch devvm 把它的成果取回来再自己看。官方把这条放在「安全习惯」的第一位,用来跑你不认识的仓库或者关掉检查的 agent。
官方那页「Staying safe」列了七条习惯,我没必要转述全部,但其中两条值得原样记住。一是用细粒度的 GitHub token,并且只给 VM 绑定它真正需要的 secret,因为 VM 能用它的 secret 做任何事,哪怕它从没见过这个 secret。二是别把凭据放进项目目录和 store 的设置目录,因为 VM 看得到那两个地方里的每一个文件,包括 .env。
我量了一遍仓库,还看了它最近的改动
数字分两类,我标清楚:下面这些是我把仓库 clone 下来自己数的,命令和特性来自官方文档原文,我没有在真机上跑过 devvm,因为它需要 Lima 和 /dev/kvm。
| 项目 | 数量 |
|---|---|
| Go 生产代码 | 92,576 行 / 404 个文件 |
| Go 测试代码 | 103,158 行 / 374 个文件 |
| 测试与生产之比 | 1.11 |
| docs 目录 | 13,816 行 Markdown / 139 个页面 |
| 仓库总文件数 | 1,575 |
| 端到端测试 | 57 个文件 |
两个比例我觉得比总量更值得看。
测试代码比生产代码还长。一个九天大、版本号还在 0.1.x 的项目,测试行数是 103,158 行对 92,576 行。最大的一批文件里除了一个生成的 internal/msg/messages_gen.go(6,345 行),其余全是测试:tests/e2e/network_test.go 3,158 行、tests/e2e/project_test.go 1,949 行、internal/tools/tools_test.go 1,789 行。网络那一块有 3,158 行的端到端测试,跟它声称的四层网络加代理加白名单是对得上的。
五分之一的生产代码在处理边界。我把安全相关的包单独加起来:netpolicy 2,420 行、gitprotect 3,021 行、proxy 6,787 行、agentlogin 3,035 行、secrets 1,685 行,再加 trust 462、safefile 275、hostsafety 263、redact 134、sshagent 627,合计 18,709 行,占生产代码的 20.2%。
这个比例是这篇文章里我唯一想让你记住的数字。一个把「凭据留在外面」当卖点的工具,如果安全边界只占它代码的百分之二三,那卖点就是文案;占五分之一,说明它真的把大部分工程花在了你打不开的那些门上。internal/safefile/ 那 275 行尤其能说明态度,按包名读,它是每一次读取「VM 能写或能替换的文件」都要走的那条路。
改动速度也值得提一句,因为它决定你现在装下去的版本会不会很快变。我读到的 0.1.1-rc.3 变更记录里有一条 breaking change:你自己写的 layer 现在跑在 devvm 的 prelude 之后,devvm 会往你的 #! 行位置放一行自己的东西,给你三个变量($devvm_arch、$devvm_libc 和 devvm_sha)。同一个版本还要求每个已有的 VM 各自跑一次 devvm update,在那之前老镜像建出来的 VM 会提示自己过时;第一次 update 或新建 VM 会把每个共享工具重新建一遍,要几分钟。另外 --clone 的 VM 只有在重建之后才会拿到 git-lfs,重建的顺序是先 devvm clone off、再 devvm update、再 devvm clone on。这三条都写在同一份记录里,说明这个项目现在还在换骨架。
把编辑器、终端和日常动作接进来
装完之后你每天真正会敲的命令不多,但有几个值得一开始就知道。
devvm 在项目目录里是「进这台 VM 的 shell」,也是「建这台 VM」。devvm ls 列出你有哪些 VM 和各自的网络层。devvm tools 管这台 VM 里的工具链和 agent,加一个 agent 用 devvm tools add opencode。devvm update 把 VM 升到更新的共享工具版本,devvm restart 重启它(换进或换出 open 层之后需要这一次),devvm network 看和改层与白名单。
SSH 那条线比我想的有用。官方要求 OpenSSH 8.7 以上,原因写得很直白:为了能按名字 ssh 进 VM,以及让编辑器直接连上去。也就是说你不必把编辑器塞进 VM,可以在宿主上照常用它,让它在 VM 里跑语言服务器和终端。这台 VM 有自己的 SSH 配置,devvm 管着。
SSH 密钥这一项默认是关的,想开有两种,区别不小。devvm ssh-agent on 把宿主的 SSH agent 转发进去,VM 里的进程在你连接期间可以请求你全部的密钥。devvm ssh-agent devvm 换成 devvm 自己的 agent,VM 只能用你主动加进那个 agent 的密钥。官方在「安全习惯」里给的建议是保持转发关闭,或者用后面这一种,并且只加 VM 真正要用的密钥。这个取舍值得你自己做一次,因为它直接决定 VM 里的代码能不能拿你的密钥去推东西。
VM 之间的服务互通走另一条路。目标 VM 先在自己的目录里 devvm link publish 发布端口,客户端 VM 再在自己的目录里加一条链接,两次操作都要做。官方的约束是客户端只能连与自己同层或更严的目标,而且写成一句话:只有当你愿意让两边互相持有对方的东西时,才把两台 VM 链起来。
最后是升级这条线。devvm upgrade 升 devvm 自己,devvm update 升 VM 里的共享工具,两者不是一回事。共享工具是按需构建的,所以第一次 update 或新建 VM 会把每个共享工具重新建一遍,官方提醒要几分钟,这一点在你急着干活的时候会硌手。
还没做到的地方
我不想把这一节写成免责声明,它们对要不要用是有影响的。
Intel Mac 没测过,官方明写在已知局限里。Linux 没有 .git 保护,上面说过了。版本是 v0.1.0,README 自己也说还在早期。
VM 间的链接有一套约束我建议用之前先读一遍。规则是客户端只能连到与自己同层或更严的目标,目标必须先在它自己的目录里 devvm link publish 过端口,而且链接要在两边的目录里各运行一次命令才算建立。官方同时写了一句很坦白的:这个约束保住了客户端的层不被绕过,但它保不住目标的数据,受限层上的客户端可以读目标服务给它的任何东西,然后通过自己这层允许的 host 发出去。结论就是原文那句,只有当你愿意让两边互相持有对方的东西时,才把两台 VM 链起来。
官方还有一页专门列没覆盖到的东西,分「收窄了但没关死」和「留给你」两类,包括允许的 host 接受什么、你的 secret 允许什么(比如 push 和 API 调用)、以及你自己加进去的公司 CA 能读到什么。一页安全文档肯把没覆盖的部分单列成表,这件事本身值得给分。
谁该用
如果你每天都在几个项目之间切,而且已经让 agent 直接在你机器上跑,devvm 是那种装完就改变默认行为的工具。它的成本是一次安装加一份 8 GiB 磁盘,换来的是「npm install 和你的 ~/.aws 不在同一个世界里」这件事变成默认。
如果你只用一台机器、一个仓库,而且 agent 只做只读的问答,那它现在带来的收益有限,--clone 那条路单独用就够了。
如果你的机器是 Intel Mac,先等它测过。
相关阅读:Google Agent Compute Engine 拆解讲的是云端怎么做「没有钥匙的 microVM」,那篇拆云厂商的实现,这篇是你自己机器上的上手动作,两篇不重叠;Replit 换掉容器引擎那篇算的是换隔离机制之后内核漏洞面降了多少,是同一件事的另一端。