VS Code 1.141: Agent Sandboxing on Three Platforms and the Worktree Disk Problem
💡 Tool Tip:JSON Formatter, Regex Tester, Hash Generator, Text Diff Checker
VS Code 1.141 shipped on October 7, 2026. 1.141.1 patched security issues and 1.141.2 cleaned up defects. What makes this release worth writing about is a pair of contradictory things arriving together: the agent host gained cross-platform sandboxing on Windows, macOS and Linux, limiting what it can reach on the filesystem and the network, and the one-worktree-per-session model finally presented its bill. The first is security. The second is operations. They belong in the same release because capability and containment have to ship at the same rate.
Whether a sandbox is a control or a setting depends on whether you tested it.
Where the sandbox is actually enforced
The switch is chat.agent.sandbox.enabled, available globally and per session through the Sandboxing for terminal toggle in the Permissions menu. The stated purpose is to limit how supported agent operations access files and network resources, reducing the impact of model mistakes, prompt injection, untrusted dependencies and locally launched tool servers.
Enforcement takes one of two forms depending on the operation and the platform: operating-system protections, or checks inside the agent host process. That sentence is worth reading twice, because it implies not every operation gets OS-level isolation. Do not hear "sandbox is on" and think "container".
Microsoft is also unusually direct about the boundary: sandboxing adds a layer of protection but does not replace endpoint security and does not provide a standalone security boundary. I would copy that sentence straight into a team runbook, because the failure mode is people assuming the switch retires their other controls. On Windows, required OS updates have to be installed first, or the feature simply is not there.
// .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.
The most interesting default: MCP servers
One line in the release notes is easy to skim past. When sandboxing is enabled, locally launched MCP and language servers are sandboxed by default. That is the largest security win in this release. An MCP server is, structurally, someone else's process with read and write access to your filesystem and the ability to make network requests, and over the past few months these have been wired into development environments at a rate nobody audited.
Filesystem permissions refer to paths on the machine running the agent. That has two consequences: local sessions are constrained on your machine, and connected remote sessions are constrained on the remote host. If you open a local window against a remote host, the path rules you wrote apply to the remote machine's filesystem, not your laptop's.
Network is the other half. Prompt injection rarely ends with a deleted file. It ends with data leaving. So I default to denying outbound traffic and allowlisting only the package registries. Verify the exact setting keys against the 1.141 notes before committing them, because a silently ignored key is worse than no key at all: it makes you believe you are protected.
# 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.
Test the sandbox or you have a setting, not a control
I have watched too many teams flip a security switch and never verify it again. Sandboxes are especially prone to this, because a policy with one character out of place defaults to permissive and nobody gets a notification.
The direct way to check is a probe inside an agent session, not in your own terminal. The agent host is the process being constrained; your shell is not. The probe does three things: write a file inside the workspace (should succeed), write to a path outside it (should fail), read .env (should fail), plus one network connection attempt.
The expected result is unambiguous: inside the workspace ALLOWED, outside BLOCKED, .env BLOCKED, and any host not on the allowlist BLOCKED. If any line disagrees with that, your policy is not in effect. I would put this probe in onboarding, because it accomplishes more than a document that says sandboxing has been enabled.
# 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.
Prompt injection usually ends with data leaving, not a deleted file.
Worktrees: the quietest disk killer of the agent era
One git worktree per agent session is a good design. It gives you isolation, rollback and parallelism. The problem is that nobody owns reclamation. In a medium-sized repository a single worktree can be hundreds of megabytes to several gigabytes. Ten sessions a day is tens of gigabytes a week.
1.141 adds Chat: Open Worktree Cleanup, which shows the disk cost of each inactive session, lets you filter by inactivity, and supports automatic cleanup for sessions whose pull request has merged. There is also chat.agentSessions.sessionStorageCleanupSuggestion.enabled, which makes VS Code prompt you when session storage grows large.
Cleanup marks the selected sessions as done and deletes their worktrees, and restoring a session recreates the worktree, so it is reversible in principle. Four statuses are protected: active, running, needs-input and pinned. That list is sensible, but it does not protect a session that has uncommitted changes. That part is on you, which is why my script adds it as an explicit refusal.
# 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.
Cross-session delegation and the one irreversible edge
send_message now supports four actions between and nested sessions on the same agent host: steer an active conversation, queue a follow-up as its own turn, replace a queued message while preserving its position, and cancel a queued message before processing begins. Queued messages run in order, and once a message starts processing it can no longer be replaced or cancelled.
That rule sounds like trivia. It is the core constraint when you design a delegating agent. An instruction already in flight is immutable, so your delegation logic has to read message state before choosing an action. The refusal branch in the snippet is the minimal reproduction: a2 is already started, so the replace is rejected.
The same release lets you pick up external sessions. A conversation started in the ChatGPT app or the Codex CLI can continue in the VS Code Agents window on the same machine with history intact, and you can switch back later. The limit is that only one application can send to that chat at a time. If ChatGPT or Codex CLI still has it open, VS Code shows a banner explaining why sending is unavailable and offers Retry after you fully quit the other side. That is an honest design, and much better than failing silently.
# 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.
Hundreds of megabytes to several gigabytes per worktree, ten sessions a day.
📌 Frequently Asked Questions
With sandboxing on, can the agent still read my whole project?
Under default policy it probably can, but you can and should narrow it. The release notes say filesystem permissions and network access can be customised further. Remember that filesystem permissions refer to paths on the machine running the agent, so in a remote session they apply to the remote host.
Does the sandbox replace endpoint security?
No. Microsoft states it plainly: sandboxing reduces the impact of model mistakes, prompt injection, untrusted dependencies and local tool servers, but it does not replace endpoint security and does not provide a standalone security boundary. Treat it as one layer of defence in depth, not the only layer.
In a remote session, whose filesystem do the permissions constrain?
The machine running the agent. Local sessions are enforced on your machine; connected remote sessions are enforced on the remote host. Write your path rules against the remote host's directory layout rather than your local one.
Does deleting a worktree lose code?
Cleanup marks sessions as done and deletes the worktree, and restoring the session recreates it, so conceptually it is reversible. Anything uncommitted is genuinely gone, though, and the protected list only covers active, running, needs-input and pinned statuses. It does not check for uncommitted changes.
When does send_message's replace action fail?
When the target message has already started processing. Queued messages run in order and become immutable once they begin. Delegation logic must read message state before acting, otherwise long-running tasks end up in a state where you believe you changed an instruction that was never changed.