Repository navigation
Windows native sandbox blocks all exec_command: 'permission roots or deny lists changed' #881
Description
Version / branch / commit
zero 0.6.0(npm@gitlawb/zero@0.6.0, win32-x64 binary)- Also present on
main@7f39a63
OS and environment
- Windows 11 (virtualized sandbox VM), PowerShell 5.1, elevated (Administrator)
- Native backend:
windows-restricted-token(zero sandbox setupcompleted successfully from an elevated terminal) - Go 1.26.5, Node 24.19
Steps to reproduce
-
Install
@gitlawb/zero@0.6.0and runzero sandbox setupfrom an elevated terminal. It printsWindows sandbox setup complete. -
Run
zero doctorfrom the same directory —sandbox.backendis[pass](the setup marker validates for the cwd workspace root). -
In any small project directory, run:
zero exec --output-format stream-json --max-turns 10 "Run 'go test ./...' and report the result."
-
The agent's
exec_commandtool call aborts before the command executes.
Expected behavior
exec_command should run inside the native Windows restricted-token sandbox (as zero doctor suggests the backend is ready), just as write_file/read_file/edit_file work.
Actual behavior
Every exec_command aborts immediately with:
zero-windows-command-runner.exe: windows sandbox setup is out of date: permission roots or deny lists changed
The run ends with status: incomplete. File tools work fine — only shell execution is blocked.
Root cause (traced on main @ 7f39a63)
internal/sandbox/runner.go—BuildCommandPlanbuilds the profile and then callspermissionProfileWithRuntime(profile, runtimeState)(line ~189).internal/sandbox/runtime_state.go—permissionProfileWithRuntimeappends the per-workspace sandbox runtime root (<LocalAppData>/zero/runtime/v1/<sha256(workspaceRoot)[:8]>) as an extraFileSystem.WriteRootsentry when it isn't already present.internal/sandbox/windows_command_runner_windows.go(restricted-token tier) validates the stored setup marker using the command's profile:ValidateWindowsSandboxSetupMarker(WindowsSandboxSetupConfigFromCommand(config)).- But
zero sandbox setup(internal/cli/sandbox.go→runSandboxSetup) writes the marker viaWriteWindowsSandboxSetupMarkerfrom a profile built without the runtime root. internal/sandbox/windows_setup.go—ValidateWindowsSandboxSetupMarkercomparesACLPlanHash/ACLPlanEntriesof the stored marker (no runtime root) against the freshly computed plan from the command profile (with runtime root) → permanent mismatch →permission roots or deny lists changed.
Evidence
- The runtime root appears after the first exec attempt:
C:\Users\<user>\AppData\Local\zero\runtime\v1\55c03089a6535cfb, where55c03089= first 8 hex chars ofsha256("<workspace>\zero-demo"). - The stored marker's
aclPlanEntriesis stable for a given workspace (e.g., 5 for a workspace-only profile), while the command-time plan adds the runtime-root entries → counts/hashes never agree. zero doctorreports the backend as[pass]because doctor builds its profile the same way as setup (without the runtime root), so the two sides of the validation disagree.
Suggested fix direction
Make the elevated Windows setup include the sandbox runtime root in the profile used both for applying ACLs and for writing the setup marker — i.e. prepare the runtime and apply permissionProfileWithRuntime before BuildWindowsACLPlan / WriteWindowsSandboxSetupMarker in the setup path (runWindowsSandboxSetup / runSandboxSetup), so the command-time profile matches the marker.
Relevant logs
[zero] zero-windows-command-runner.exe: windows sandbox setup is out of date: permission roots or deny lists changed
(zero doctor from the same directory: Overall: pass, sandbox.backend: [pass].)
Activity
Vasanthdev2004 commentedon Aug 13, 2026
Confirmed, and your root-cause trace is correct in every step. This is a real bug on
mainand the fix is already written, sitting in #808 waiting on review.I verified both halves rather than going from memory:
internal/sandbox/windows_setup.go:259onmaintoday.windowsSandboxProfileWithRuntimedoes not exist onmainat all.windows_setup.go:210for the marker that setup writes, andwindows_runner.go:359for the profile the command presents.That is exactly the asymmetry you identified. Setup fingerprinted the bare profile while every command arrived with the runtime root already appended by
permissionProfileWithRuntime, so a marker written seconds earlier was rejected and no command could run. It hit me the same way while building the identity work, which is why the fix lives in that PR rather than a standalone one.Two details from fixing it that your analysis did not have, and that matter if you are testing:
The augmentation has to add every runtime candidate, not the one the current process would select.
sandboxRuntimeRootForprefers the cache-derived root and falls back to a temp-derived one, and that choice is made per process, so setup granting only its own pick left a command that fell back writing to a tree with no ACE on it. Setup now covers both.The runner also cannot recompute the root itself. It runs re-exec'd as
zero __windows-command-runnerwithTMP/TEMPalready redirected into the sandbox runtime tree, soos.TempDir()there is not the operator's temp and the derivation comes out different. The profile is augmented in the parent and passed down.Workaround until #808 lands, with the trade-off stated plainly: turning the sandbox off in your user config with
"sandbox": {"enabled": false}restoresexec_command, at the cost of the confinement you set it up for. On an elevated VM you may be fine with that; on a real workstation I would not.Thanks for the report. The trace through
BuildCommandPlantoValidateWindowsSandboxSetupMarkeris more work than most issues get, and it is why this took two minutes to confirm rather than an afternoon.Vasanthdev2004 commentedon Aug 13, 2026
@baoyu0 update: the fix is now #901, a standalone PR against
main, rather than waiting on #808.When I replied earlier I pointed you at #808. Accurate, but not a good answer for you: #808 is a 12,000 line Windows principal-identity change with open architecture questions on it, so you would have been waiting on a design decision to get your shell back. #901 is 560 lines and does one thing.
Your suggested fix in the issue is what it implements, near enough word for word: prepare the runtime and apply the augmented profile before
BuildWindowsACLPlanandWriteWindowsSandboxSetupMarkerin the setup path. Two details on top of that, both of which broke it once while I was building:The augmentation has to grant every runtime candidate, not the one the current process would pick.
sandboxRuntimeRootForprefers the cache-derived root and falls back to the temp-derived one when the cache sits inside the workspace, and that choice is per process, so granting only setup's own pick left a command that fell back writing to a tree with no ACE on it.It also is not a pure lift onto
main, which matters if you build it yourself. The fix depends onfallbackSandboxRuntimeRootbeing deterministic, and onmainthat function is stillos.MkdirTempmemoized in a process-global map: setup grants temp root A, the next command derives root B, teardown cleans a third. So the derivation change came across with it. It is now a hash of the workspace and creates nothing, so every process reaches the same answer without sharing state.The workaround stands until this lands.
"sandbox": {"enabled": false}in your user config restoresexec_command, at the cost of the confinement you turned it on for. On the virtualized VM you described that is likely an acceptable trade for a few days; I would not run that way on a workstation.If you can build the branch and confirm it clears on your machine, that is worth more than my Windows 11 box agreeing with itself, and I will say so on the PR. No obligation, and it lands either way.
fix(sandbox): pin the Windows runtime root instead of deriving it twice
baoyu0 commentedon Aug 14, 2026
@Vasanthdev2004 verified #901 on my Windows 10 machine:
go build ./...— cleango test ./internal/sandbox/with the fix(sandbox): agree on the runtime root across the Windows setup marker #901-specific filters (Windows|RuntimeRoot|Marker) — all pass, including the runner-marker test that fails if the fix is revertedinternal/sandboxsuite: 4 unrelated failures (TestBuildCommandPlanRejectsOutsideDirectory,TestResolveCommandDirAllowsExtraRootCwd,TestScopeAddNormalizesSymlinkedRoot,TestPermissionProfileIncludesReadOnlyGrantAsReadRootOnly) — I confirmed these fail identically on plainmain, so they're a local-environment artifact (repo cloned under%TEMP%breaks workspace-path expectations), not a regression from this PR.Can confirm the fix clears on a real Windows box. Thanks for splitting #901 out of #808 — that was the right call.
fix(sandbox): agree on the runtime root across the Windows setup marker
fix(sandbox): pin the Windows runtime root instead of deriving it twice
fix(sandbox): agree on the runtime root across the Windows setup marker
fix(sandbox): pin the Windows runtime root instead of deriving it twice
gnanam1990 commentedon Aug 25, 2026
Approved. The setup-marker/runtime-root mismatch is confirmed on current
main, and PR #901 implements the focused fix. The reporter has also validated that branch on Windows 10. Please continue through #901 rather than opening a duplicate implementation.fix(sandbox): agree on the runtime root across the Windows setup marker
fix(sandbox): pin the Windows runtime root instead of deriving it twice
fix(sandbox): agree on the runtime root across the Windows setup marker
fix(sandbox): pin the Windows runtime root instead of deriving it twice