VS Code 1.141:三大平台的 Agent 沙箱,以及 worktree 占盘这本账
VS Code 1.141 在 2026 年 10 月 7 日发布,1.141.1 修安全问题,1.141.2 修一批缺陷。这个版本真正值得写的是两件互相矛盾的事:一边给 Agent 加了跨平台沙箱,Windows、macOS、Linux 都支持,限制它对文件和网络的访问;另一边,Agent 每个会话一个 git worktree 的做法终于显出了代价——磁盘会被吃掉。前者是安全,后者是运维,两件事在同一个版本里出现不是巧合:能力越强,越需要边界和清理。
沙箱是设置项还是控制项,取决于你有没有验证过它。
沙箱到底在哪里强制执行
开关是 chat.agent.sandbox.enabled,可以全局开,也可以在会话层面用 Permissions 菜单里的「Sandboxing for terminal」单独控制。官方描述是:它限制受支持的 agent 操作对文件和网络资源的访问,降低模型失误、提示注入、不可信依赖和本地启动的工具服务器带来的影响。
执行方式分两种,取决于操作类型和平台:要么用操作系统的保护机制,要么用 agent host 进程内部的检查。这句话很重要,因为它的言外之意是——不是所有操作都有 OS 级隔离。别把「开了沙箱」理解成「进了容器」。
官方自己也把边界说得很直白:沙箱增加一层保护,但不替代端点安全,也不构成独立的安全边界。这句我建议直接抄进你的团队规范里,因为最容易出事的就是有人以为开了这个就不用管别的事了。另外 Windows 上需要先装好要求的系统更新,否则这个功能等于不存在。
// .vscode/settings.json — turn sandboxing on for everyone on the repo, then opt
// into narrower filesystem and network permissions. Paths are on the machine
// running the agent, which for remote sessions is the remote host, not your laptop.
{
"chat.agent.sandbox.enabled": true,
// Defaults are broad. Narrow them to what the task actually needs.
"chat.agent.sandbox.filesystem.allow": [
"${workspaceFolder}/**",
"${workspaceFolder}/.cache/**"
],
"chat.agent.sandbox.filesystem.deny": [
"${workspaceFolder}/.env",
"${workspaceFolder}/.env.*",
"${workspaceFolder}/secrets/**",
"~/.ssh/**",
"~/.aws/**"
],
// Network is where prompt injection turns into exfiltration.
"chat.agent.sandbox.network.allowHosts": ["registry.npmjs.org", "pypi.org"],
"chat.agent.sandbox.network.denyAllOtherHosts": true
}
// Verify the exact key names against the 1.141 release notes before you commit
// these — settings move, and a silently ignored key is worse than no key at all.
默认被沙箱的东西里,MCP 服务器最值得注意
发布说明里有一句容易被略过的话:开启沙箱后,本地启动的 MCP 服务器和语言服务器默认被沙箱化。这是这次更新里安全收益最大的一条。MCP 服务器的本质是「别人写的、能读写你文件系统、能发网络请求的进程」,而它们在过去几个月里被大量地、无审核地接进开发环境。
文件系统权限指的是「运行 agent 的那台机器上的路径」。这句话有两层含义:本地会话在你的机器上生效,远程会话在远程主机上生效。也就是说,如果你在本地开了一个连到远程主机的会话,你在设置里写的那套路径规则,作用对象是远程那台机器。
网络是另一半。提示注入最常见的终点不是删文件,是把东西发出去。所以我建议默认 deny 全部出站,只 allowlist 包管理器域名。代码里的键名请对着 1.141 发布说明核对一遍——设置项会变,一个被静默忽略的键比没有更危险,因为你会以为自己受保护了。
# Agent sessions create a git worktree per session. They are cheap to make and
# easy to forget. Audit before the drive fills up.
import json, pathlib, subprocess, shutil
def worktrees(repo: str) -> list[dict]:
out = subprocess.run(
["git", "-C", repo, "worktree", "list", "--porcelain"],
capture_output=True, text=True, check=True,
).stdout
entries, cur = [], {}
for line in out.splitlines():
if not line.strip():
if cur:
entries.append(cur)
cur = {}
elif line.startswith("worktree "):
cur["path"] = line.split(" ", 1)[1]
elif line.startswith("HEAD "):
cur["head"] = line.split(" ", 1)[1]
elif line.startswith("branch "):
cur["branch"] = line.split(" ", 1)[1]
if cur:
entries.append(cur)
for e in entries:
p = pathlib.Path(e["path"])
if p.exists():
e["size_mb"] = round(sum(f.stat().st_size for f in p.rglob("*") if f.is_file()) / 1e6, 1)
return entries
rows = sorted(worktrees("/Users/me/code/big-repo"), key=lambda e: e.get("size_mb", 0), reverse=True)
print(f"{'MB':>9} branch")
for e in rows:
print(f"{e.get('size_mb', 0):9.1f} {e.get('branch', '(detached)')}")
# VS Code also ships this as Chat: Open Worktree Cleanup, which shows size per
# session and protects active, running, needs-input and pinned sessions.
验证沙箱:不测就等于没有
我见过太多团队开了某个安全开关,然后一年都没验证过它是否真的阻断。沙箱尤其容易出这种问题:策略删错一个字符,它就默认全放开,而你不会收到任何通知。
最直接的验证方式是在 agent 会话里跑一次探针——不是在你自己的终端里跑,因为被约束的是 agent host 进程,你自己的 shell 不受约束。探针只做三件事:往工作区内写一个文件(应该成功)、往工作区外的路径写(应该被拒)、读 .env(应该被拒),再加一个网络连接测试。
期望结果很明确:工作区内 ALLOWED,工作区外 BLOCKED,.env BLOCKED,未在 allowlist 的域名 BLOCKED。只要有一项和预期不符,就说明你的策略没生效。这个探针我建议放进 onboarding 清单,比写一段「已启用沙箱」的文档有用得多。
# send_message grew four actions for agent-to-agent delegation. The ordering rules
# are the part that bites: queued messages run in order, and once a message starts
# processing you can no longer replace or cancel it.
ACTIONS = {
"steer": "correct an active conversation without waiting for it to finish",
"queue": "append a follow-up as its own turn, preserving order",
"replace": "swap a queued message while keeping its position in the queue",
"cancel": "drop a queued message before processing begins",
}
def plan_queue(messages: list[dict]) -> list[str]:
log = []
for m in messages:
action = m["action"]
if action not in ACTIONS:
log.append(f"skip {m['id']}: unknown action {action}")
continue
if m.get("started") and action in ("replace", "cancel"):
log.append(f"refuse {m['id']}: already processing, cannot {action}")
continue
log.append(f"ok {m['id']}: {ACTIONS[action]}")
return log
print("\n".join(plan_queue([
{"id": "a1", "action": "queue"},
{"id": "a2", "action": "replace", "started": True},
{"id": "a3", "action": "steer"},
{"id": "a4", "action": "explode"},
])))
# The refusal on a2 is the whole point. Design your delegating agent so it checks
# message state before it edits instructions it can no longer change.
提示注入的终点通常不是删文件,是把数据发出去。
worktree:Agent 时代最安静的磁盘杀手
每个 agent 会话一个 worktree,这个设计本身是对的——隔离、可回滚、能并行。问题是没人负责回收。一个中等规模的仓库,一个 worktree 可能几百 MB 到几个 GB,一天开十个会话,一周就是几十 GB。
1.141 加了 Chat: Open Worktree Cleanup,能看到每个非活跃会话的 worktree 占多少空间,按不活跃时长筛选,勾选后删除。还支持对 PR 已合并的会话配置自动清理,以及用 chat.agentSessions.sessionStorageCleanupSuggestion.enabled 让 VS Code 在存储涨到一定程度时主动提醒。
清理会把选中的会话标记为完成并删掉对应 worktree,恢复会话时会重新创建。有四种状态受保护:active、running、needs-input、pinned。这个保护名单是对的,但它保护不了「有未提交改动」的会话——那属于你自己的责任。所以我的脚本把「未提交」也加进了拒绝条件。
# A sandbox you have not tested is a setting, not a control. Prove it blocks the
# things it claims to block, and prove it still allows the things you need.
import pathlib, socket
def probe_writes(root: str = ".") -> dict:
results = {}
outside = pathlib.Path("~/.ssh/id_rsa_test").expanduser()
for label, target in {
"inside_workspace": pathlib.Path(root, ".sandbox_probe.txt"),
"outside_workspace": outside,
"dotenv": pathlib.Path(root, ".env").resolve(),
}.items():
try:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("probe")
target.unlink(missing_ok=True)
results[label] = "ALLOWED"
except PermissionError:
results[label] = "BLOCKED"
except OSError as e:
results[label] = f"ERROR {e.errno}"
return results
def probe_network(host: str = "example.com", port: int = 443) -> str:
try:
socket.create_connection((host, port), timeout=3).close()
return "ALLOWED"
except OSError:
return "BLOCKED"
print(probe_writes("."))
print("network:", probe_network())
# Expected with a tight policy: dotenv BLOCKED, outside_workspace BLOCKED,
# inside_workspace ALLOWED, network BLOCKED unless the host is allowlisted.
# Run this inside an agent session, not in your own terminal. The agent host is
# the process being constrained.
跨会话委派,以及那条不可逆的边界
send_message 现在支持四种动作:steer(纠正一个正在运行的会话)、queue(作为独立一轮追加)、replace(替换队列里的消息但保留位置)、cancel(在处理开始前取消)。队列按顺序执行,一旦某条消息开始处理,就不能再替换或取消。
这条规则听着琐碎,但它是设计委派型 agent 时的核心约束。一个正在被处理的指令是不可变的,所以你的委派逻辑必须先读状态再决定动作。代码里那个 refuse 分支就是这个问题的最小复现:a2 已经在处理,replace 会被拒绝。
同版本还支持接管外部会话:在 ChatGPT app 或 Codex CLI 里开始的对话,可以在同一台机器的 VS Code Agents 窗口里继续,历史完整保留,之后还能切回去。限制是同一时刻只有一个应用能向该对话发消息——如果 ChatGPT 或 Codex CLI 那边还开着,VS Code 会显示「This chat is open in another app」,你得先完全退出那边再点 Retry。这个交互设计很诚实,比静默失败好。
# Worktree cleanup policy. Auto-cleanup is offered for sessions whose pull request
# has merged, but "merged" is not the same as "safe to delete" when a branch is
# still open, so gate it on state you control.
import json, subprocess
def pr_state(branch: str) -> dict:
out = subprocess.run(
["gh", "pr", "list", "--head", branch, "--state", "all", "--json", "state,number,isDraft"],
capture_output=True, text=True,
)
if out.returncode != 0 or not out.stdout.strip():
return {"state": "NONE"}
return json.loads(out.stdout)[0]
def eligible(session: dict) -> tuple[bool, str]:
if session["status"] in {"active", "running", "needs_input", "pinned"}:
return False, "protected status"
pr = pr_state(session.get("branch", ""))
if pr["state"] != "MERGED":
return False, f"pr state {pr['state']}"
if session.get("uncommitted"):
return False, "uncommitted changes"
return True, "ok"
sessions = [
{"name": "s1", "branch": "feat/a", "status": "done", "uncommitted": False},
{"name": "s2", "branch": "feat/b", "status": "running", "uncommitted": False},
{"name": "s3", "branch": "feat/c", "status": "done", "uncommitted": True},
]
for s in sessions:
ok, why = eligible(s)
print(f"{s['name']}: {'DELETE' if ok else 'keep'} ({why})")
# Deleting a worktree marks the session done and frees the disk. Restoring the
# session recreates it, so cleanup is reversible — as long as nothing uncommitted
# lived only inside that tree.
一个 worktree 几百 MB 到几 GB,一天十个会话,一周几十 GB。
📌 常见问题 FAQ
开启沙箱之后,Agent 还能读我的整个项目吗?
默认策略下大概率可以,但你可以也应该收窄。发布说明明确说可以进一步自定义文件系统权限和网络访问。注意文件系统权限指的是「运行 agent 的那台机器上的路径」,远程会话下作用对象是远程主机。
沙箱能替代端点安全吗?
不能。微软自己在发布说明里写得很清楚:沙箱增加一层保护,帮助降低模型失误、提示注入、不可信依赖和本地工具服务器的影响,但它不替代端点安全,也不构成独立的安全边界。它是纵深防御的一层,不是唯一一层。
文件系统权限在远程会话里作用于哪台机器?
作用于运行 agent 的那台机器。本地会话在你的机器上强制执行,连接的远程会话在远程主机上强制执行。所以路径规则要按远程主机的目录结构来写,别照着本地路径写。
删除 worktree 会丢代码吗?
清理会把会话标记为完成并删除 worktree,之后恢复会话会重建它,所以概念上是可逆的。但有未提交改动的内容会真的消失,而保护名单只覆盖 active、running、needs-input 和 pinned 四种状态,不检查未提交改动。自己加一道检查。
send_message 的 replace 什么时候会失败?
当目标消息已经开始处理时。队列按顺序执行,一旦某条消息进入处理阶段就不能被替换或取消。委派逻辑必须先读消息状态再决定动作,否则会在长任务上出现「以为改了其实没改」的情况。