VS Code 1.141:三大平台的 Agent 沙箱,以及 worktree 占盘这本账

·11 分钟阅读·Evergreen Tools Team

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 什么时候会失败?

当目标消息已经开始处理时。队列按顺序执行,一旦某条消息进入处理阶段就不能被替换或取消。委派逻辑必须先读消息状态再决定动作,否则会在长任务上出现「以为改了其实没改」的情况。