沙箱与网络

agentrun 用沙箱限制 agent 能写的文件、能读的敏感文件和命令能访问的主机,用运行时参数限制它能读到的配置与账号。本页写 --sandbox、--network 与 --deny-read 三组选项、各运行时分别怎么关进沙箱,以及每种组合剩下的风险。

代码位置

文件主要类型与函数本页对应的章节
src/sandbox/mod.rsSandbox(new)、Wrapper、resolve_mode、check、PiState、Hidden、hidden、Wrapped、wrap、wrapper_failure、start_failure、exit_text沙箱模式、判断沙箱能不能用、读取限制
src/sandbox/bubblewrap.rsprobe、wrap_bwrap、ProxyForward判断沙箱能不能用、用 bubblewrap 包裹命令(由 wrap 调用)、读取限制
src/sandbox/seatbelt.rsprobe、write_seatbelt_profile、wrap_seatbelt判断沙箱能不能用、用 Seatbelt 包裹命令(由 wrap 调用)、读取限制
src/network/mod.rs 的 ProxyEndpoint、proxy_environment;rule.rs 的 HostRule、check_usage、Policy、SERVICE_PORT;address.rs 的 is_private、url_host;upstream.rs 的 Upstream、ProxyAddress;proxy.rs 的 FilterProxy(bind、endpoint、serve_session);http.rs 的 handle_http;socks.rs 的 handle_socks网络、过滤代理、不支持 IPv6
src/runtime/mod.rsDENIED_HOME_ENTRIES、default_denied_reads、denied_reads、proxy_needed读取限制、网络
src/runtime/claudecode.rs、src/runtime/codex.rs、src/runtime/pi.rs、src/runtime/dsh.rsclaudecode.rs 的 ClaudeCode(launch)、SERVICE_HOST、login_file、history_paths;codex.rs 的 Codex(launch)、SERVICE_HOSTS、SERVICE_HOST、sandbox_command、history_paths;pi.rs 的 Pi(launch)、model_service;dsh.rs 的 Dsh(launch)、service_host、user_home、history_paths、ENV_FILE各运行时怎么关进沙箱、各运行时的做法、模型服务的主机
src/doctor.rsrun、connectdoctor 对沙箱与网络的检查,见运行

过滤代理在 agentrun 进程里的独立线程上运行,每个连接一个处理线程,建立转发后再加一个复制下行数据的线程,线程数没有上限;Policy 只做放行判断,不知道连接来自哪个运行时。理由:所有运行时共用同一份判定规则与同一种 network 事件。普通 HTTP 请求的转发、CONNECT 与 SOCKS5 的隧道都设了空闲时限:客户端关闭发送方向以后,上游从最后一次发来数据起 30 秒(proxy.rs 的 UPSTREAM_IDLE_TIMEOUT)内没有再发来数据,复制下行数据的线程就结束,所以上游一直不回复也不关闭时,处理线程与复制线程不会留到 agentrun 退出。客户端没有关闭时,上游停顿多久都不中断转发。

沙箱模式

沙箱由操作系统决定:macOS 用 Seatbelt,Linux 用 bubblewrap(bwrap);codex 在两个系统上都用它自己的沙箱,它在 Linux 上也是用 bwrap 实现的。--sandbox(环境变量 AGENTRUN_SANDBOX,选项优先)决定沙箱做不到时怎么办。

on(默认)relaxoff
沙箱可用用沙箱用沙箱不用
沙箱缺失或启动失败启动前报错退出(退出码 2),写明原因与安装命令不用沙箱照常运行不尝试
--network 的限制无法执行启动前报错退出(退出码 2)照常运行,不限制网络不限制网络
没有沙箱时的记录不会出现start 事件记 sandbox: "none" 与 network.enforced: false,text 与 rich 开头一行提示同 relax

默认 on 的理由:本机 Linux 没装 bwrap 时,agent 不会在没有隔离的情况下运行而无人察觉;docker 里的结果也不会取决于镜像碰巧装了什么。名字用 on 与 relax,是有意鼓励调用方使用 on。off 给明确知道自己在容器里的启动方用。

沙箱与网络共用这一个选项,因为网络限制本身靠沙箱执行:沙箱没有运行,网络限制也一定落空。分成两个选项会出现「沙箱放宽、网络严格」这类说不清的组合。

判断沙箱能不能用

只看 bwrap 是否存在不够:docker 容器、关闭了非特权用户命名空间的系统、被 AppArmor 限制的系统里,bwrap 装了也启动不了。所以 on 与 relax 在每次运行前都实际检查一次:

启动前发现的沙箱问题按 rejected 处理(退出码 2);运行中才发现的,例如 pi 或 dsh 的包裹命令启动失败,按 failed 处理(退出码 1)。--dry-run 也做这些检查,--debug 写出每项检查的结果。

这些检查由 sandbox::check 执行。检查期间它通过 checking 回调把检查进程交给信号处理,收到信号时检查进程按进程、信号与超时清理。沙箱可用时,检查的结果是 Sandbox,它的 wrapper 字段是 Wrapper,记录用哪种方式包裹命令,用 bubblewrap 时还记录找到的 bwrap 与 socat 的路径。沙箱不可用时,--sandbox on 下 check 返回错误,这次运行按 rejected 拒绝;--sandbox relax 下结果仍是 Sandbox,wrapper 为 Wrapper::None,不可用的原因记在 reason 字段。

bwrap 启动失败的常见原因

检查失败时,报错写出 bwrap 的第一行报错,并提示 In a docker container use --sandbox off. On Ubuntu 23.10 or later, allow bwrap to create user namespaces with an AppArmor profile,后面附本节的链接。bwrap 的报错文字随调用者身份变化,所以提示同时写出两种情况,不按报错文字区分。

环境bwrap 的报错做法
docker 默认权限(不加 --privileged)的容器以 root 运行时为 Creating new namespace failed: Operation not permitted;以普通用户运行时为 No permissions to create new namespace 开头的一句用 --sandbox off,由容器提供隔离。能让 bwrap 在容器里启动的 docker 选项没有实测
Ubuntu 23.10 及以后的默认设置,AppArmor 限制普通程序创建用户命名空间普通用户运行时为 setting up uid map: Permission denied;root 不受这项限制为 /usr/bin/bwrap 单独加一个 AppArmor profile(见下)
# 这项限制由 sysctl kernel.apparmor_restrict_unprivileged_userns=1 打开
# /etc/apparmor.d/bwrap
abi <abi/4.0>,
include <tunables/global>

profile bwrap /usr/bin/bwrap flags=(unconfined) {
  userns,
}

写入后运行 sudo apparmor_parser -r /etc/apparmor.d/bwrap 加载(Ubuntu 24.04 实测)。只给 bwrap 放行,不把上表里的这项 sysctl 改成 0。理由:改成 0 会让所有程序都能创建用户命名空间。

agentrun 不自动安装 bwrap 与 socat。理由:安装需要 root 和各发行版不同的包管理器,运行 agent 时修改系统,出了问题不容易查清原因。报错信息与 agentrun doctor 给出安装命令。

没有沙箱时的提示

relax 下沙箱没有运行、或者 off 时,text 与 rich 格式在第一行输出:

[note] sandbox not running (--sandbox relax: bwrap cannot start: No permissions to create new namespace). agentrun does not restrict what the agent reads or writes or which hosts it reaches

jsonl 格式不加额外的行,由 start 事件的 sandbox 与 network.enforced 字段记录。理由:relax 不打断运行,但事后能查出这次运行没有隔离。沙箱没有运行时,读取限制只剩 claude-code 的权限规则,见读取限制。

各运行时怎么关进沙箱

四个运行时在沙箱里都只能写工作目录(--cwd)与会话临时目录(见会话临时目录),pi 另外能写用户 pi 状态目录里的五个条目。能读的范围见读取限制。

运行时沙箱打开时沙箱没有运行时
claude-code用 claude-code 自己的沙箱(macOS 是 Seatbelt,Linux 是 bwrap 加 socat),由 --settings 打开。只作用于 Bash 命令:命令只能写两个目录、不能联网,沙箱里的命令自动放行,不允许沙箱外执行命令。Edit 与 Write 不经过沙箱,改用权限模式 dontAsk 加按路径放行的规则,只放行写两个目录权限模式 auto,每条 Bash 命令由 claude-code 的分类器判断
codexcodex 自己的沙箱(macOS 是 Seatbelt,Linux 是 bwrap 加一个 seccomp 网络过滤),用一个自定义权限配置:整个文件系统可读,工作目录与会话临时目录可写,/tmp 与 $TMPDIR 不可写,网络关闭内置权限配置 :danger-full-access
pipi 没有沙箱,由 agentrun 把整个 pi 进程包裹起来:macOS 用 sandbox-exec 加一份编译进 agentrun 的 Seatbelt 配置,Linux 用 bwrap。另外放行用户 pi 状态目录里的五个条目:auth.json、auth.json.lock、models-store.json、models-store.json.lock、settings.json.lock,见pi 使用用户的状态目录。网络见下一节不包裹
dshdsh 自带的沙箱只限制写文件,agentrun 用 DSH_PERMISSION_MODE=danger-full-access 关掉它,与 pi 一样把整个 dsh 进程包裹起来:macOS 用同一份 Seatbelt 配置,Linux 用 bwrap,没有另外放行的条目。dsh 的私有目录在会话临时目录里,见dsh 的私有配置目录不包裹,仍为 danger-full-access,dsh 不限制写入

pi 状态目录的放行按平台实现,输入都是 src/sandbox/mod.rs 的 PiState:

pi、dsh、doctor 的沙箱与网络检查、测试里的 FakeAdapter 都通过 sandbox::wrap 包裹,--dry-run 时不写 Seatbelt 配置。理由:包裹逻辑分开写时,改了 bwrap 参数、socat 转发或 Seatbelt 配置的一处,另外几处不会跟着变,doctor 与测试检查的就不再是运行时实际使用的包裹。

doctor 的沙箱与网络检查通过 runtime::model_service_host、runtime::service_hosts、runtime::sandbox_state、runtime::default_denied_reads、runtime::proxy_needed 与 codex::sandbox_command 取运行时的参数,沙箱检查里是否另在沙箱中启动运行时取自 runtime::starts_inside_wrapper。只属于 codex 自己沙箱的检查(需要 codex 可执行文件、不检查私有地址、fake-ip 提示)按 Wrapper::Codex 判断,不按运行时判断。理由:分开写时改了一个运行时的启动参数,doctor 检查的就不再是运行时实际使用的参数。

各运行时的完整参数见运行。几个容易被改坏的决定:

读取限制

agent 默认读不到下表的路径。--deny-read PATH 另外加一个文件或目录,可重复;--no-default-deny-read 去掉默认列表,只保留 --deny-read 给出的路径。目录的全部内容都读不到。

默认列表路径
主目录里的密钥与账号(DENIED_HOME_ENTRIES)~/.ssh、~/.aws、~/.gnupg、~/.config/gh、~/.netrc、~/.kube、~/.docker/config.json、~/.config/gcloud、~/.azure
各运行时的登录文件claude-code 的 $CLAUDE_CONFIG_DIR/.credentials.json(未设时 ~/.claude/.credentials.json)、codex 的 $CODEX_HOME/auth.json(未设时 ~/.codex/auth.json)、pi 的 $PI_CODING_AGENT_DIR/auth.json(未设时 ~/.pi/agent/auth.json)、dsh 的 $DSH_HOME/.env(未设时 ~/.dsh/.env,dsh 自己从这里读 DEEPSEEK_API_KEY)。运行 pi 时不含 pi 自己的登录文件
会话记录claude-code 的 $CLAUDE_CONFIG_DIR/projects(未设时 ~/.claude/projects,含每个项目的会话记录与 memory 目录)、codex 的 $CODEX_HOME/sessions 与 $CODEX_HOME/history.jsonl(未设时在 ~/.codex 下)、dsh 的 $DSH_HOME/sessions(未设时 ~/.dsh/sessions)
运行时沙箱运行时沙箱没有运行时
claude-code--settings 的 permissions.deny 给每个路径写两条规则 Read(//<路径>) 与 Read(//<路径>/**),路径里的 \ * ? [ ] { } 前面加反斜杠,按字面匹配。Read、Grep、Glob 读这些路径时被拒绝,Grep 搜索上层目录时跳过它们;sandbox.filesystem.denyRead 列出同样的路径,Bash 命令读不到(Linux 上目录显示为空,文件读取报 Permission denied)。路径是符号链接时,链接与目标都写(claude 2.1.303 云端实测)--settings 只有 permissions.deny。Read、Grep、Glob 照样被拒绝;Bash 命令不在沙箱里,命令里直接写出这些路径时被 claude-code 拒绝,间接的写法由分类器判断,没有保证(claude 2.1.303 云端实测)
codex自定义权限配置的 filesystem 表给每个存在的路径加一项 "<真实路径>"="deny",已经在另一个被拒绝的目录之内的路径不再单独加。用户登录文件或它所在的目录在列表里时(按真实路径比较),私有 CODEX_HOME 里指向它的符号链接 auth.json 按链接本身的路径再加一项,不依赖 codex 是否解析符号链接。codex 能看到这些拒绝项,读写被拒路径的命令它不执行,所以这些命令没有 tool 事件(codex 0.160.1 macOS 实测)。--deny-read 的真实路径含 * ? [ ] { } 时是用法错误,detail 为 --deny-read <路径>: codex cannot deny a path that contains * ? [ ] { },解析后的路径与给出的不同时,在给出的路径后加 (resolves to <真实路径>):codex 把这类表项当作模式,文档没有转义写法,"a*b"="deny" 既挡不住字面路径 a*b 也挡不住 aXb(codex 0.160.1 macOS 实测)。默认列表不检查不限制
pi存在的路径先解析符号链接,已经在另一个被拒绝的目录之内的路径不再单独处理,与输入顺序无关。理由:Linux 上父目录先换成只读的空目录后,子路径不存在也无法创建,bwrap 启动失败。Linux:目录用 --tmpfs 加 --remount-ro 换成只读的空目录,其他文件用 --ro-bind /dev/null 覆盖(读取报 Permission denied),写在所有 --bind 之后,所以工作目录与状态目录里的路径也能挡住。macOS:Seatbelt 配置末尾加 (deny file-read* (subpath "<真实路径>") ...)不限制
dsh与 pi 相同。dsh 的整个进程在沙箱里,命令与 dsh 自己的 read、glob、grep 工具都读不到这些路径:read 读被覆盖的文件报 not a regular file,读被换成空目录里的文件报 not found,glob 在这样的目录里找不到文件,grep 搜索上层目录时报 Permission denied(0.2.1-rc.2 云端实测)不限制

路径在 sandbox::hidden 里解析成 Hidden(真实路径与是否目录),pi、dsh、doctor 与 codex 都用它。几个容易被改坏的决定:

网络

--network 决定 agent 的命令与网页工具能访问哪些主机。它的限制只在沙箱运行时生效;沙箱没有运行时按沙箱模式处理。

取值命令能访问网页工具
none(默认)不能联网。pi 与 dsh 只能访问模型服务的主机关闭
full任何公网主机打开网页搜索与网页读取
custom--allow-host 列出的主机,pi 与 dsh 另加模型服务的主机只打开网页读取,限制在列出的主机

--allow-host HOST[:PORT] 可重复。HOST 是完整主机名,或 *.example.com(匹配所有子域名,不匹配 example.com 本身)。不写端口时允许 443 与 80。以下情形是用法错误:custom 但没有 --allow-host;给了 --allow-host 但不是 custom;主机名里有 * 但不是以 *. 开头;单独一个 *;主机为空或端口不是 1 到 65535 的整数;主机是 IPv6 字面量(见不支持 IPv6)。理由:单独的 * 与 full 同义,只保留一种写法,start 事件里一眼能看出是否全开。

full 与 custom 默认拒绝解析到私有地址的主机,主机名或 IP 被逐字列在 --allow-host 里时放行;*.example.com 这类通配规则匹配到的主机仍然检查,因为允许用户自建子域名或泛解析的域名能让某个子域名解析到内网地址。私有地址是 0.0.0.0/8、10.0.0.0/8、100.64.0.0/10、127.0.0.0/8、169.254.0.0/16、172.16.0.0/12、192.168.0.0/16。198.18.0.0/15 不在其中:本机 DNS 由代理工具做 fake-ip 时,公网域名解析到这个网段,把它当作私有地址会让放行列表里的主机也被拒绝。

custom 不打开网页搜索的理由:claude-code 的网页搜索与 codex 的网页搜索在服务商的服务器上执行,无法按主机限制,而且搜索词本身可以带出文件内容。

过滤代理

沙箱打开且需要按主机放行时,agentrun 在启动运行时之前在自己的进程里启动一个过滤代理,会话结束时关闭。代理运行在沙箱外,沙箱里的进程只能连到它。

沙箱agent 进程:运行时与它执行的命令只能连接 127.0.0.1 上的随机端口过滤代理:在 agentrun 进程里,沙箱外放行经上游代理或直连拒绝not_allowed拒绝private_address主机在放行列表里主机不在放行列表里解析到私有地址每个「主机、端口、放行或拒绝」组合第一次出现时输出一个 network 事件
沙箱里的进程只能连到过滤代理,由代理按主机决定放行或拒绝。

不支持 IPv6

过滤代理与网络限制只支持 IPv4:只在 127.0.0.1 监听;解析目标主机只取 IPv4 地址,没有 IPv4 地址时按连接失败处理;目标是 IPv6 字面量时拒绝,reason 为 not_allowed;--allow-host 的主机是 IPv6 字面量时是用法错误;SOCKS5 的 IPv6 地址类型回复「不支持的地址类型」。理由:Cloud Managed 容器的内核没有 IPv6,IPv6 的路径无法在云端验收。已知后果:在 localhost 先解析为 ::1 的主机上,claude-code 在沙箱外用 TCP:localhost:<端口> 连接过滤代理可能失败,由本机 Linux 实测确认。

没有直接用各运行时自带的网络设置,因为它们能力不一致:claude-code 沙箱的 allowedDomains 不接受单独的 *,表达不了 full(claude 2.1.289);codex 按主机放行要开启它自带的实验性代理,而且这个代理拒绝解析到私有网段的主机;pi 没有任何网络控制,dsh 自带的沙箱不限制网络。各自的做法含义不同,也没有统一的连接记录可供排查。

各运行时的做法

运行时nonefull 与 custom网页工具
claude-code沙箱全禁,不启动代理沙箱设置 sandbox.network.httpProxyPort 与 socksProxyPort 指向过滤代理。设了这两个端口后 claude-code 不再按 allowedDomains 过滤,沙箱里命令的流量交给过滤代理,主机由过滤代理判定(claude 2.1.289 云端实测),所以 allowedDomains 保持为空。例外:claude-code 给沙箱里的命令设置的 NO_PROXY 列出了 localhost、127.0.0.1、169.254.0.0/16、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,目标是这些网段里的 IP 字面量时命令不经代理,在没有网络的命名空间里直接连接失败,过滤代理看不到这次请求,没有 network 事件(claude 2.1.289 云端实测);解析到私有地址的主机名仍由过滤代理拒绝full:放行 WebFetch 与 WebSearch。custom:每个主机放行一条 WebFetch(domain:HOST),不放行 WebSearch
codex沙箱全禁,不启动代理full:权限配置的网络打开,直接联网,不经过代理,没有 network 事件。custom:开启 codex 自带的代理,放行 --allow-host 列出的主机;codex 进程的 HTTPS_PROXY 指向过滤代理,codex 的代理把放行的请求转给它,过滤代理按同一份列表再判断一次并输出 network 事件。codex 的代理关闭 SOCKS5,因为 SOCKS5 的连接不经过上游。codex 访问模型与刷新令牌的请求也经过过滤代理,过滤代理另外放行 codex 的服务主机,见模型服务的主机。codex 的代理配置不写 allow_local_binding:本机 DNS 由代理工具做 fake-ip 时,公网域名解析到 198.18.0.0/15,codex 的代理把这个网段当作私有地址,放行列表里的主机也被拒绝。加 allow_local_binding=true 能通过,但加上后沙箱里的命令能绕过代理直接连接本机回环地址上的端口,所以这个选项由调用方在 -- 之后用 -c permissions.agentrun.network.allow_local_binding=true 自己打开。agentrun 的过滤代理不把这个网段当作私有地址full:web_search="live"。none 与 custom:web_search="disabled"
pi包裹断开网络,只能连代理,代理只放行模型服务同 none,代理按取值放行pi 没有网页工具,靠 bash 里的 curl
dsh同 pi同 pi。dsh 的 node 进程与它执行的命令都按代理变量经过过滤代理(实测)补丁关闭 web_search 与 web_fetch,靠 bash 里的 curl,理由见运行页的 dsh 一节

claude-code 的 WebFetch 在 claude 进程里运行,不经过沙箱也不经过代理,只能靠权限规则限制主机。沙箱打开时权限模式是 dontAsk,放行规则以外的主机被拒绝;沙箱没有运行时权限模式是 auto,放行规则以外的主机由分类器判断,云端实测分类器放行了不在列表里的主机,所以这时 custom 下的 WebFetch 不受主机限制,见剩余风险。claude-code 进程本身不设置代理变量,它访问 Anthropic 的流量不经过过滤代理。

claude-code 的沙箱运行时,三种取值下会话环境都设置 SOCAT_DEFAULT_LISTEN_IP=4。理由:claude-code 在断网的网络命名空间里用 socat 把沙箱里的端口转发到它的代理 socket,没有指定地址族;内核没有 IPv6 时(Cloud Managed 容器),socat 1.8 选了 IPv6 而启动失败,claude-code 丢弃它的输出,沙箱里的命令连不上任何代理。这个变量让 socat 改用 IPv4 监听。

模型服务的主机

pi 的进程整个在沙箱里,它访问模型服务也要经过代理。pi 不输出服务商的地址(--list-models 与 auth check 都没有,pi 1.0.1),所以 agentrun 自带一张表,按 --model 的前缀判断服务商,没给 --model 时读 pi 设置里的 defaultProvider。pi 的 launch 与 doctor 的网络检查用同一个函数 pi::model_service 得到服务商与主机。模型服务的主机只在 443 端口放行,并且跳过私有地址检查:

pi 的服务商放行的主机
deepseekapi.deepseek.com
anthropicapi.anthropic.com
openaiapi.openai.com
googlegenerativelanguage.googleapis.com
openrouteropenrouter.ai

服务商不在表里时,agentrun 不知道该放行哪个主机。none 下启动前报错(退出码 2),detail 为 cannot tell which host pi's model service uses (provider: <服务商或 unknown>). Use --network custom --allow-host <host of the model service>;没给 --model 且 pi 设置文件不存在、不是 JSON 或没有 defaultProvider 时服务商按 unknown 处理;custom 下只放行 --allow-host 列出的主机;full 不受影响。

dsh 的进程也整个在沙箱里,它的模型服务主机由 dsh::service_host 给出,launch 与 doctor 的网络检查都用它:会话环境里 DEEPSEEK_BASE_URL 不为空时取这个地址的主机(network::url_host::// 之后、第一个 /、? 或 # 之前的部分,去掉用户信息后按过滤代理解析请求目标的同一规则取主机,即去掉端口、转成小写、去掉末尾的点),否则为 api.deepseek.com。与 pi 相同,这个主机只在 443 端口放行并跳过私有地址检查;模型服务用其他端口时,用 --network custom --allow-host HOST:PORT 放行。DEEPSEEK_BASE_URL 读不出主机时(没有 ://、主机为空、不是合法的主机名、端口为空、不是数字或大于 65535,或者主机是 IPv6 字面量,见不支持 IPv6),none 下启动前报错(退出码 2),detail 为 cannot tell which host dsh's model service uses from DEEPSEEK_BASE_URL. Use --network custom --allow-host <host of the model service>;custom 下只放行 --allow-host 列出的主机;full 不受影响。dsh 的网页搜索也只连 api.deepseek.com,这是关闭它的理由之一。

codex 的进程不在沙箱里,none 与 full 下它直接访问模型服务。custom 下 codex 进程的 HTTPS_PROXY 指向过滤代理,它自己的请求也经过过滤代理,所以过滤代理另外放行下面四个主机,不看用户用哪种方式登录:

codex 的请求放行的主机
ChatGPT 登录时访问模型(macOS 实测)chatgpt.com、ab.chatgpt.com
刷新 ChatGPT 登录的令牌auth.openai.com
API key 登录时访问模型api.openai.com

后两个主机取自 codex 0.159.2 内置的地址,没有实测。只放行前两个时,令牌过期的那次运行刷新失败,API key 登录完全不能用。

配置与账号

agent 不应读到用户自己的运行时配置、插件、规则、MCP 服务器与会话记录,也不应把记录写进用户的会话。各运行时都用参数关闭这些来源,详见运行:

四个运行时默认都不保存会话。调用方给出 --session-dir 时,运行结束后会话记录移到那个目录,--resume 只从那里读,不读用户自己的会话记录。运行期间 codex 的记录在私有目录里,pi 与 dsh 的记录在会话临时目录里;claude-code 的新会话写在用户配置目录的 projects/ 里,结束后才移走,带来的风险见剩余风险,做法见保存与继续会话。

账号由登录方式决定。会话环境按运行时删除会改变登录方式的变量,容器里的凭据由环境变量写入登录文件,见会话环境。

剩余风险

运行时沙箱运行时沙箱没有运行时
claude-code内置的 Read 能读读取限制以外的文件,内容会进入模型上下文。Edit 与 Write 只能写工作目录与会话临时目录,由权限规则限制,不由沙箱限制(云端实测)。macOS 上沙箱里的命令能写 /tmp/claude-<uid>,这个目录由同一用户的所有 claude 会话共用,写在里面的文件运行结束后不删除(macOS 实测);Linux 上这个目录在会话临时目录之内,没有这项风险(本机 Linux 与云端实测)Bash 命令只经分类器判断,能写容器内任何位置、能联网,能用间接的写法读取读取限制里的路径;Write 能写任何位置;--network custom 下 WebFetch 能读放行列表以外的主机(均为云端实测)。边界是容器
codex能读读取限制以外、用户可读的任何文件,内容会进入模型上下文。系统级配置(/etc/codex 与 macOS 的托管配置)无法关闭,照常生效(codex 0.159.2 源码)。--network full 下命令直接联网,不经过滤代理,没有私有地址检查,169.254.169.254 这类地址可以访问(用户定)能读、写容器内任何位置,能联网,边界是容器
pi能读读取限制以外、用户可读的任何文件;能改写用户 pi 状态目录里的 auth.json 与 models-store.json;Linux 上还能在状态目录里新建原本没有的条目,例如 extensions/,用户下次在沙箱外运行 pi 时会加载。agent 可以把状态目录里的 auth.json 换成指向其他文件的符号链接,之后带 AGENTRUN_PI_AUTH 运行时,agentrun 把凭据写到那个文件,这个文件可以在沙箱之外,见凭据。网络只通向模型服务,所以读到的文件只能经模型服务带出能读、写容器内任何位置,能联网,边界是容器
dsh能读读取限制以外、用户可读的任何文件。dsh 启动时把工作目录里 .env 的变量合并进它自己与命令的环境,会话环境里已有的变量优先,没有参数能关闭;agent 能写工作目录,写进 .env 的变量会进入之后每次运行的命令环境。账号不受影响,因为会话环境里总有 DEEPSEEK_API_KEY;.env 里有 dsh 拒绝读取的变量名(DSH_ 开头、DEEPSEEK_BASE_URL、代理变量、PATH 等)时 dsh 启动失败(0.2.1-rc.2 实测)。网络只通向模型服务,读到的文件只能经模型服务带出;每个模型请求带 x-deepseek-harness-user-id 与 x-deepseek-harness-session-id 两个请求头,私有 DSH_HOME 下用户 id 每次运行随机生成能读、写容器内任何位置,能联网,边界是容器

读取限制按路径匹配。限制范围以外已有的硬链接指向同一个文件时,通过硬链接能读到内容。工作目录里被拒绝读取的目录,沙箱运行时 claude-code 的命令写进去报告成功,但写入的文件在运行结束后不存在(Linux 上这个目录在沙箱里换成了空目录,云端实测);pi 写进去时报只读错误;codex 的拒绝项优先于工作目录的写权限,文件不会写出(codex 0.160.1 macOS 实测)。

读取限制只针对文件。会话环境里的变量(例如 DEEPSEEK_API_KEY 与容器里的令牌)agent 都能读到;默认列表以外的凭据文件,需要用 --deny-read 自己加上。pi 的会话记录目录(状态目录里的 sessions/)不在默认列表里。

保存会话(--session-dir)的剩余风险:

过滤代理本身的剩余风险:

不走代理的程序在沙箱里连不上网。例如 git clone git@github.com:... 走 SSH,即使用了 --network full 也会失败;改用 HTTPS 地址可以成功。