无人值守的 agent,先钉死隔离边界和出网白名单
我把 agent 放进无人值守模式的第一周就栽了,原因跟提示词毫无关系:我在宿主机上直接开了 skip-permissions。它跑测试的间隙顺手往 ~/.bashrc 里塞了几行初始化代码,直到我下次开终端才炸出来。问题不在于它坏,在于我给了它一整台机器的写权限,却只准备了提示词层面的约束。
所以这篇讲前置条件,不讲技巧:让 agent 少问甚至不问之前,先定死隔离边界和出网白名单。
权限管”能不能做”,隔离管”做完能碰到什么”#
官方文档把这两层分得很清楚:权限模式决定一次工具调用是否执行、是否先问你;隔离边界决定命令跑起来之后能碰到哪些文件、进程和网络。[1] 有提示词挡着时两者差别不明显,一旦开了 bypass,隔离边界就是唯一的兜底。
内置的 Bash 沙箱最轻,用操作系统原语约束每条 Bash 命令的文件和网络访问,配置成本很低。[1] 但它只覆盖 Bash 子进程。Read、Edit、Write 这类内置工具走权限系统,不经过沙箱;MCP server 和 command hook 是独立进程,直接在宿主机上跑,不受沙箱约束。[1][3]
反过来读就是坑:沙箱再严,hook 里的一个 curl 也能把仓库旁边的东西发出去。要让内置工具、MCP 和 hook 同处一个边界,得把整个 Claude Code 进程放进容器、VM 或沙箱运行时。[1]
按威胁模型挑一档#
官方给了对照表,我按自己的用法简化成三档。[1]
日常在本机干活、只想少弹几次窗,用内置 Bash 沙箱,/sandbox 配一下就行。要让 agent 无人值守,最低档是预配置的 dev container:跑在 Docker 里,项目目录挂进去,Claude Code 以非 root 用户运行。[1][2] 要跑你自己不完全信得过的代码库则完全不同,只有独立 VM 够用,它是唯一提供内核级隔离的选项。[1]
沙箱运行时不依赖 Docker,把整个 Claude Code 进程(含文件工具、MCP、hook)包进和内置沙箱同一套 Seatbelt 或 bubblewrap 隔离里,不过它仍是 beta research preview,配置格式可能变。[1]
快速上手:一份默认拒绝出网的 dev container#
下面这套我照着官方参考容器抄了一遍,改了两处,跑通之后再放进自己的仓库。
第一步,在仓库里建 .devcontainer/devcontainer.json,用官方 feature 装 Claude Code:
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
}
}
feature 那个版本号钉的是安装脚本,不是 Claude Code 的版本。要钉 CLI 版本,改成在 Dockerfile 里 npm install -g @anthropic-ai/claude-code@X.Y.Z,同时设 DISABLE_AUTOUPDATER=1。[2]
第二步,把参考容器的 init-firewall.sh 抄进 .devcontainer/,并在 runArgs 里加 --cap-add=NET_ADMIN 和 --cap-add=NET_RAW,容器内跑防火墙需要这两个能力。[2] 脚本本身不复杂:先建一个叫 allowed-domains 的 ipset,把 GitHub 官方 API 返回的 CIDR 段、以及 api.anthropic.com 这类域名解析出的 IP 放进去,然后把 INPUT/FORWARD/OUTPUT 的默认策略改成 DROP,只放行集合内的目标,其余一律 REJECT。[5]
第三步,把必须放行的域名列清楚。官方清单是 api.anthropic.com、claude.ai、claude.com、platform.claude.com,用 claude.ai 连接器的话再加 mcp-proxy.anthropic.com。[4] 不需要遥测就顺手关掉,containerEnv 里加 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1,它会一并关掉依赖 feature flag 拉取的功能。[2]
第四步,确认非 root。--dangerously-skip-permissions 在 root 下会被直接拒绝,Linux 和 macOS 都拦,容器里必须把 remoteUser 设成非 root 账号。[2][3]
第五步,不想让同事自己开这个开关,就别写在项目设置里。用 /etc/claude-code/managed-settings.json 下发 permissions.disableBypassPermissionsMode,它的优先级高于用户和项目设置。[2][3] 但 Dockerfile 就在仓库里,有写权限的人能删掉这一步;真要拦住得走 server-managed settings 或 MDM。[2]
四个会悄悄吃掉隔离的配置#
抄完逐条对一遍,这四条是我读文档时最意外的。
出网白名单按主机名放行,默认却不检查 TLS。内置代理只看客户端报上来的主机名,不解析加密内容,所以宽泛放行 github.com 这类大域名本身就构成外泄通道,沙箱里的代码还可能用 domain fronting 之类的手法连到白名单之外的机器。[3] 要更强的保证,只能自建终止 TLS 并检查流量的代理,再把 CA 证书装进沙箱。[3]
允许哪些 Unix socket 要逐个想清楚。文档点名 allowUnixSockets:放行 /var/run/docker.sock 等于把宿主机交出去。[3]
文件写入权限开得太宽,效果和提权差不多。允许写 $PATH 下的可执行文件目录、系统配置目录,或者 .bashrc、.zshrc 这类 shell 配置,都可能让沙箱内的代码在另一个安全上下文里被执行。[3] 这跟我第一周踩的坑属于同一类。
Linux 上的 deny 列表只在启动时构建一次。沙箱运行时会扫一遍项目根目录,但会话中新 git clone 或脚手架生成的嵌套仓库不在覆盖范围内。[1] 所以跑完后要 review 你保留为可写的路径,以及 Linux、WSL2 上会话新建出来的东西。[1]
最后一条不属于配置,但必须记住:隔离不改变发给模型的内容。你在沙箱里读了哪些文件,照样会传给你配置的 provider。[1]
验收清单#
容器里 whoami 不是 root;curl https://example.com 失败而 curl https://api.github.com/zen 成功,参考脚本自己就是这么自检的;--dangerously-skip-permissions 能正常启动;挂进去的只有项目目录和 ~/.claude 卷,~/.ssh、云凭据、docker.sock 都不在列表里;托管设置里关掉了 bypass 这个开关。[2][5]
无人值守的价值不是让 agent 随便跑,而是它跑的时候你真的走得开。前提是隔离边界和出网白名单先定下来。
Sources#
[1] https://code.claude.com/docs/en/sandbox-environments — Claude Code Docs: Choose a sandbox environment [2] https://code.claude.com/docs/en/devcontainer — Claude Code Docs: Development containers [3] https://code.claude.com/docs/en/sandboxing — Claude Code Docs: Configure the sandboxed Bash tool [4] https://code.claude.com/docs/en/network-config — Claude Code Docs: Network access requirements [5] https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh — anthropics/claude-code: reference init-firewall.sh