The protect-agent-folders lane (docs/certification/protect-agent-folders.md on
its branch, "What remains Muse Code's") ran Muse Code 1.4.2 with the
extension's arguments for a workspace under the user's profile
(muse serve --disable-sandbox --trust-workspace), in Manual
(promptUnmatched). Muse Code wrote .claude/settings.json inside the
workspace and a file outside it, and raised no approval for either.
src/core/backends/musecode/sandbox.ts:resolveShellSandboxreturns{ isSandboxed: false, reason: 'profileWorkspace' }formuseSpark.shellSandbox: autowhenisProfileWorkspaceholds, andserveArgumentsthen passes--disable-sandbox(MUSE_DISABLE_SANDBOX_ARG).isProfileWorkspaceis true only forplatform === 'win32'and a workspace inside%USERPROFILE%(isInsideDirectory, case-insensitive). So it is Windows-only.- The reason is PLAN.md D12, "Known CLI limitation" and "Resolution —
museSpark.shellSandbox" (2026-09-22): Muse Code's Windows sandbox could not run commands in a workspace underC:\Users\<user>; 1.3.0 and 1.4.0 started them inC:\Windows\System32\WindowsPowerShell\v1.0after about 34 s (meta-models/muse-code-sdk#26). The sandbox runs commands as separate local sandbox users, which then lack access to the profile tree; it is not an AppContainer setting the extension can change. The owner ruled out changing folder permissions ("change how the sandbox is used, never the user's folder permissions"). - A narrower exception is not available.
muse serve's posture is fixed for the host's lifetime and--disable-sandboxis all or nothing (shell OS sandbox, file-tool confinement and network together). Muse Code has no documented per-path grant that would let its sandbox enter one profile folder. Its own 1.4.x read worker ("Root:Read … through an optional background worker") is the only thing that grants the sandbox group access, and the retest below shows it is not enough on a fresh setup.
Local reads only (--help, muse schema generate-json-schema, the binary's
strings, muse config status) and Meta's docs, read 2026-10-04:
| Mechanism | What it allows |
|---|---|
MSP session/start, session/setApprovalMode |
Select one of allowAll, promptUnmatched, onRequest, denyUnmatched (closed enum, "select, never create"). session/start.config admits only mcpServers. 1.4.2 has no session/setPermissionProfile or permissionProfile/list; the next SDK site lists both as experimental and says "there is no inline-rule member and none may be added under this contract". |
muse serve flags |
Posture only: --disable-sandbox, --sandbox-network, --disable-write, --disable-shell, --trust-workspace. No --permission-profile (the TUI and exec have one), no --approval-judge. |
Settings (~/.config/muse/settings.json) |
Documented: permissions.default_profile (built-in :ask-me, :auto-review, :read-only, :unrestricted). Custom profiles with filesystem rules exist in the binary but are undocumented, and its startup text says that with --disable-sandbox "permission-profile filesystem and local-command network restrictions remain recorded but are not enforced". |
| Managed policy | execution.permission_profiles, execution.tool_rules, execution.approval_modes: administrator planes (system file, Windows machine policy); muse config status showed all absent. |
| Hooks | A PreToolUse hook can block a call. Sources: the user's settings hooks block, managed_hooks_path, or the repository's .muse/hooks.json (trusted workspaces only). |
| What the extension writes at launch | Only the environment (museCodeBackendManager.ts childEnvironment: the extension host's environment, the PowerShell module path, VS Code's proxy, museSpark.environmentVariables, loopback NO_PROXY) and the serve flags above. It never writes Muse Code's settings (PLAN D17; museSettings.ts reads them only). src/host/backend/environment.ts is the Model API prompt's git facts and has nothing to do with the CLI. |
So no mechanism a client can use makes Muse Code ask before these writes. Writing the user's settings file, a managed policy or the repository's hooks file would change machine-wide or repository state behind the user's back, and D17 rules out the first.
Yes, all of it (dev.meta.ai/docs/muse-code/permissions):
- "
--disable-sandbox: keep approval, but skip the sandbox. This flag also removes workspace confinement from the file tools, so write_file and edit_file can write anywhere on the filesystem." - "
untrusted… escalates shell execution only. File reads and in-workspace write_file and edit_file writes still pass in any mode." - "Inside the writable workspace, the .git, .muse, and .agents directories stay read-only."
Manual's "does not ask for workspace edits" (D69) is documented. Writing
outside the workspace without asking is documented as the effect of
--disable-sandbox; nothing says approval covers those writes, and it does
not. The earlier capture's outside target sat under %TEMP%, which Muse
Code's Ask me profile treats as writable temporary space when the sandbox
is on; the probes below used a second target outside %TEMP% as well.
scratchpad/mcwrite/probe-policy.mjs with fake-provider.mjs: a real
muse-bin-1.4.2-R4684.1.exe serve over the SDK's spawnMspConnection,
with an isolated XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_CACHE_HOME and
(except run t18) XDG_DATA_HOME under the run folder. The isolated
settings point endpoint_transport.base_url at a loopback stand-in
(auth: "bearer", META_API_KEY a dummy string, every other key and token
variable removed), which answers POST /responses with scripted function
calls in the Responses SSE order of test/unit/helpers/fakeModelApi.ts.
Every provider request reached the stand-in (39 to 47 POSTs per run in
its log) and none reached Meta; no sign-in existed in the isolated home.
The probe rejects every approval except one shell call to
(Get-Location).Path, so a file exists afterwards only if Muse Code wrote
it unasked. Steps: write_file notes.txt, .claude/settings.json,
.git/probe.txt, a file outside under %TEMP%, a file outside under
%LOCALAPPDATA%, then powershell (Get-Location).Path.
| Run | serve flags, mode |
notes.txt |
.claude/settings.json |
.git/probe.txt |
outside, %TEMP% |
outside, %LOCALAPPDATA% |
|---|---|---|---|---|---|---|
| t12 | --disable-sandbox, promptUnmatched |
written | written | asked (protectedWrite), not written |
written | written |
| t13 | --disable-sandbox, denyUnmatched |
written | written | asked, resolvedBy: policy |
written | written |
| t14 | --disable-sandbox, onRequest |
written | written | asked, not written | written | written |
| t15 | sandbox on, promptUnmatched |
written | written | asked, not written | absolute path is outside the workspace |
same |
| t16 | sandbox on, denyUnmatched |
written | written | asked, resolvedBy: policy |
refused, same text | same |
| t17 | sandbox on, workspace C:\Users\<user>\mcwrite-probe-ws |
written | written | asked, not written | refused | refused |
| t18 | as t17, workspace under C:\Users\<user>\Coding, the owner's data root |
written | written | asked, not written | refused | refused |
The shell call was asked in every mode; under denyUnmatched the approval
was raised and then resolved denied/policy. Runs t1–t11 were the rig's
bring-up (no tool ran; also 0 model calls).
- The owner's machine: in t15, t17 and t18 the sandboxed shell printed
the workspace (
Microsoft.PowerShell.Core\FileSystem::\\?\C:\Users\…) in about 5 s (security_mode.resolve sandbox="normal", backendwindows_elevated). Its sandbox log shows complete read refreshes (refresh-finished). - The Windows 11 rig, freshly set up (the 1.4.2 binary copied in,
muse sandbox windows setuprun elevated,status=ready; the probe started in the interactive session through a scheduled task):- A workspace outside the profile (
C:\mcwrite-probe-ws) ran the shell in place (v7). - The first call in a profile workspace failed after 120 s: "…: ACL
publication lock Global\TbhWindowsSandboxAclPublication: timed out: owner
S-1-5-21-…: wait timed out after 120000 ms" (v6; captured in
failureReasonandvisibleOutput, both carrying "sandbox enforcement unavailable"). Muse Code's read worker (__tbh_internal_windows_sandbox_read_worker) held the lock for about 70 minutes while it granted read access to the profile's top-level entries, and endedrefresh-partial. - After it ended, the call in a profile workspace never finished (v8, v9:
8 minutes each; the last trace event
sandbox.prepare … outcome="prepared"). - Runs over SSH (session 0, no interactive desktop) hung or failed with "The pipe has been ended", in and out of the profile, and are not counted.
- A workspace outside the profile (
So #26 is not reliably fixed, and the version-gated posture built first (keep the sandbox for a profile workspace on 1.4.2 or later) was dropped.
- The sandbox-off warning (
conversationController.tsnoteShellSandbox): whenever the host runs without the sandbox, for either reason, the first Muse Code conversation of the window showssandboxOffProfileWarning(autoin a profile workspace) orsandboxOffSettingWarning(theoffsetting) at warning level. The text: the file tools can write anywhere the account can, outside the workspace too, without asking in any mode, Plan included; commands run as the user with the user's network; approval still covers commands and writes to.git,.museand.agents; a workspace outside the profile keeps the sandbox. Once per window:shouldWarnSandboxOff, a closure inextension.ts. It replaces the info notice that said "Approval prompts still apply". - The posture gains
isUnsupportedWorkspace(sandbox.ts); the wrong-folder warning formuseforced in a profile workspace now reads it instead of recomputing, and says commands "can start in the PowerShell folder instead of the project, or never finish". The controller's unuseduserProfileDirdependency is gone. - The preparing notice (
noteSandboxFailure): a shell failure whose text holdsSANDBOX_PREPARING_MARKER("ACL publication lock") showssandboxPreparingNotice(wait and try again) and does not offer the setup, which has already run. - Diagnostics (
report.ts):muse code file writes: workspace only (writes outside it fail); inside it, only .git, .muse and .agents ask, oranywhere this account can write, without asking, in every mode (shell sandbox off); …. The spawn log adds a warning line when the sandbox is off. - The Modes menu: Plan on Muse Code reads "Muse plans first; Muse Code
refuses commands, but its file tools can still edit files without
asking"; the Model API keeps "present a plan before editing"
(
modelApiPermissionModeDetails.plan). - The settings text for
autoandoffsays the file tools can write outside the workspace without asking. - All new and changed strings are in the 14 tables;
check:l10nreports 0 problems. - Docs: README (permission modes table, the sandbox paragraph, "Plan on Muse Code", protected writes, the settings row, two troubleshooting entries), SECURITY.md, docs/PRIVACY.md, CHANGELOG, PLAN.md (D7 and D12 corrections, §9).
| Test | File | What it pins |
|---|---|---|
| warns that the file tools can write anywhere when auto turned the sandbox off | conversationController.test.ts |
The exact warning, level warning, for profileWorkspace |
| warns the same way when the user chose off | same | sandboxOffSettingWarning for setting |
| warns once per window: the window claims the warning for its first conversation | same | Two controllers sharing the claim: one warning in all |
| warns once per session when the sandbox is forced on where it may not run commands | same | The wrong-folder warning from isUnsupportedWorkspace |
| stays quiet where the sandbox runs | same | No notice outside the profile, off Windows, or forced on outside the profile |
| says the sandbox is still preparing, and offers no setup, when the lock timed out | same | The captured failureReason; no onSandboxUnavailable |
| resolveShellSandbox (two tests) | sandbox.test.ts |
isUnsupportedWorkspace for each mode and place |
| says that without the sandbox the file tools can write anywhere without asking | supportReport.test.ts |
The Diagnostics line for both postures |
| says on Muse Code that Plan does not stop the file tools | permissionModes.test.ts |
The Muse Code Plan line, distinct from the Model API's |
| the Modes menu | App.test.tsx |
The menu shows the new Plan line |
Run on the Kubuntu rig (scratchpad/rig-gate/rig-test.sh kubuntu, slot
mcwrite-a): sandbox, museCodeBackendManager, launch,
supportReport, permissionModes, conversationController, App,
sandboxSetup, acpRuntime, --maxWorkers=2: 9 files, 730 passed.
On the rig through scratchpad/m71rv/rig-run.sh: check:l10n 0 problems,
deadcode, cycles, build (bundle budgets, split, host globals,
notices) and duplication (0 clones) passed. On this host: eslint and
prettier on the changed files, tsc -p tsconfig.json and
tsc -p test/unit/tsconfig.json, all clean. The full npm run quality was
not run; CI runs it.
scratchpad/mcwrite/drill.cjs broke one thing at a time, the run went to
the rig, and the file was restored from a backup whose SHA-256 it matched
(sha256sum -c after the batch: all OK).
| Drill | Broken | Result (exit 1 each) |
|---|---|---|
| D | The old rule: only profileWorkspace warns |
"warns the same way when the user chose off" and "warns once per window" failed (2 failed, 9 passed) |
| E | The window's claim ignored (if (true)) |
"warns once per window" failed (1 failed) |
| F | The preparing branch never matches | "says the sandbox is still preparing" failed (1 failed) |
| G | The Diagnostics line always says "workspace only" | "says that without the sandbox …" failed (1 failed, 6 passed) |
| H | The old Plan line back for Muse Code | permissionModes and the App Modes menu test failed (2 failed) |
| I | isUnsupportedWorkspace always false |
both resolveShellSandbox tests failed (2 failed, 12 passed) |
| J | The wrong-folder warning never shown | "warns once per session when the sandbox is forced on …" failed (1 failed) |
Drills A–C covered the dropped version gate (the gate in
resolveShellSandbox, the manager passing the version, the version read
from muse-bin-<version>.exe); each failed its tests (4, 2 and 2 failed)
and the code was removed with the gate.
scratchpad/mcwrite/live-turn.mjs: the owner's signed-in Muse Code 1.4.2,
muse-spark-1.3-contributor, Manual (promptUnmatched), each in a fresh
empty folder under the scratchpad (under the profile). The prompt asked for
notes.txt in the workspace, %LOCALAPPDATA%\mcwrite-live-outside…\notes.txt
outside it, and (Get-Location).Path with the shell (allowed once). Stated
beforehand: about 8 to 16 model attempts each, at most about 25.
| Turn | serve flags |
Outside write | Inside write | Shell | Model attempts (trace) |
|---|---|---|---|---|---|
| A | --trust-workspace (sandbox on) |
tool failed: absolute path is outside the workspace |
written | asked, ran in the workspace | 6: 2 main-loop steps, 4 reminder children |
| B | --disable-sandbox --trust-workspace (auto's posture here) |
wrote 7 bytes to C:\Users\…\AppData\Local\mcwrite-live-outside-off\notes.txt, no approval |
written | asked, ran in the workspace | 6: 2 main-loop steps, 4 reminder children |
Spend: 12 model attempts in all. The outside files were deleted afterwards. Turn B is the posture the warning describes; turn A is the one every workspace outside the profile keeps.
- Asking before (or refusing) file-tool writes outside the workspace roots when the shell sandbox is off.
- A read-only Plan:
denyUnmatchedrefusing file writes, or the Read-only profile selectable over MSP (#43). - Protecting other agents' configuration folders, or letting a client name protected paths.
- #26 on a fresh setup.
Upstream draft (not filed; the lead files it):
scratchpad/upstream/07-musecode-unasked-writes.txt, with the body in
07-musecode-unasked-writes.txt.body and a comment for #26 in its notes.
The lead filed it the same day as
meta-models/muse-code-sdk#86
(open at integration).
Merged into docs/truth-audit-0120 with main and fix/protect-agent-folders.
The full npm run quality on the Kubuntu rig (label
integ-hardening-aaa2d146, the merge's tree 65d1acbc) found what this
branch's targeted runs had not: test/e2e/museCode.e2e.test.ts ("spawns
the configured binary…") asserted that a clean start logs no warning, and
its harness runs off, so the backend manager's new sandbox-off warning
failed it (1 failed, 7101 passed). The test now pins that one warning and
nothing else (log.warn.mock.calls equals the single sandbox-off line),
which also gives the warning a test of its own. The same merge updated
test/e2e/modelApi.live.e2e.test.ts to the new ConversationDeps (no
userProfileDir) and ShellSandboxPosture (isUnsupportedWorkspace)
shapes for typecheck:e2e.
Run (Kubuntu, rig-test.sh) |
Result |
|---|---|
integ-hard, snapshot 7e4c3294: museCode.e2e, museCodeBackendManager |
2 files, 32 passed, exit 0 |
Drill integ-hard-drill, snapshot a54cb3e4: the warning's if made false && … |
1 failed, 15 passed, exit 1 ([] vs one) |
Restored from a copy, SHA-256 7e6bfa45…a67e7 before and after |
byte-exact |