# Muse Code Permissions & Control Model, Explained

How Muse Code's approval modes, LLM approval judge, staged shell review, OS-enforced sandbox, and hook system actually work — what each catches, what runs outside the sandbox, and where the beta's docs are ahead of the binary.

# The Muse Code control model, explained

Muse Code shipped in beta on August 5, 2026 — Meta Superintelligence Labs' terminal coding agent, built on `muse-spark-1.2`, installed as a native binary (no source repository is published). Among the majors it arrived with the strongest default posture: an OS sandbox and approvals both on from the first run, and an append-only local event log underneath. This page is the reference: what Muse Code ships natively for controlling tool execution, exactly how each mechanism behaves per [Meta's own docs](https://dev.meta.ai/docs/muse-code), and where the model ends — including the places where the beta's documentation is ahead of the shipped binary.

*This page covers Muse Code's own controls. [Our Muse Code integration](/integrations/muse-code) is shipped (`@agenticcontrolplane/muse-code`); for the cross-harness picture, see [the controls comparison](/controls).*

## The two-layer model

Muse Code's [permissions doc](https://dev.meta.ai/docs/muse-code/permissions.md) draws one distinction cleanly, and everything else on this page hangs off it: **approval decides whether a command runs; the sandbox limits what it can access when it does.** The two layers are independent — disabling one leaves the other active — and both are on by default. That independence is the design's best property: most of the escape-hatch analysis below reduces to asking which layer a given flag removes.

## Approval modes and the judge

`--approval-mode` selects one of three postures:

| Mode | What holds for review |
|---|---|
| `on-request` (default) | Only the **dangerous set** — `rm -f`, `rm -rf`, or `sudo` preceding either. Everything else runs automatically, contained by the sandbox. |
| `untrusted` | Any shell stage without a matching allow rule. Escalates shell execution only. |
| `never` | Nothing. No prompts; the sandbox is the only control. |

In every mode, file reads and in-workspace `write_file`/`edit_file` pass without prompting — the sandbox is what bounds them. Read that default carefully: on-request is a *sandbox-first* posture, not an approval-first one. Three commands hold for a human; the rest of the shell runs.

Then there's the piece that makes Muse Code's approval layer unusual: a built-in **LLM approval judge** reviews prompt-bound calls automatically, so the human only sees what the judge escalates. `--approval-judge off` routes every decision back to the human. The fatigue math favors judges — but notice where this one sits. It isn't a third layer; the docs are explicit that it belongs to the approval layer itself. A model is adjudicating a model's tool calls, inside the mechanism that exists to check the model. That's a defensible trade for attended use, and it's one you should make knowingly — especially since the docs describe no record of what the judge decided or why.

## Staged shell review

Compound commands get the most careful treatment we've seen a harness document. Muse Code parses a compound command into ordered stages and checks each one individually; execution blocks at the first unapprovable stage. In `wc -l report.log && echo cleaning && rm -rf report.log`, the two read-only stages pass and the `rm -rf` holds at stage 3 of 3 — and the command executes as a single unit only once *all* stages pass. **Rejecting the held stage means nothing runs**, including the earlier safe stages. No partial execution, no "the harmless half already happened."

Stages the parser can't reduce to a complete, static command — command substitutions, non-read-only redirects, variable-assignment prefixes — are handled by mode: in `untrusted` they hold for review; in `on-request` only dangerous-set matches hold and the rest passes to the sandbox. That's the honest version of shell parsing: the parser knows what it can't see, and the strict mode treats unparseable as unapprovable.

## Trust scopes and rule precedence

When a stage holds, the prompt offers three resolutions:

- **Allow once** — one-time, nothing saved.
- **Always allow in this workspace** — saves a prefix rule scoped to the workspace root; it doesn't travel to other projects or cover different commands.
- **Reject** — denies the entire command.

Two hardening details worth stealing: **a deny rule always overrides an allow rule, whatever the specificity**, and interpreter prefixes (`python`, `bash`, `node`) can't be saved as broad allows, because what follows them is arbitrary code. That second rule closes the classic allowlist hole — `always allow python` is an allow-everything rule wearing a disguise, and Muse Code refuses to write it.

Workspace trust is the outer gate: on first open, Muse Code asks whether to trust the workspace, and only a trusted workspace loads project-local skills, rules, and hooks (remembered per workspace root). One caveat from the [configuration docs](https://dev.meta.ai/docs/muse-code/configuration.md): committed project memory under `.agents/memory/` is read **even in an untrusted workspace** — Meta's own docs tell you to treat a repo's `MEMORY.md` as a prompt-injection surface.

## The sandbox

The sandbox is OS-enforced — Seatbelt on macOS, a bundled bubblewrap helper on Linux — and its rules are simple:

- **Writable:** the workspace plus a temp directory. Everything else is read-only, and out-of-bounds writes fail at the OS level.
- **Read-only even inside the workspace:** `.git`, `.muse`, and `.agents` — the agent can't rewrite its own history, configuration, or memory. That's a detail no other surveyed harness ships, and it closes a real hole: an agent that can edit its own rules file can approve itself.
- **Fail-closed:** if the sandbox can't be verified — Seatbelt check at session startup, bubblewrap probe on Linux — Muse Code refuses shell execution entirely rather than running unconfined.

Network gets its own dial, `--sandbox-network`: **`proxy-only`** (the default) holds the *first* connection to each new host, port, or protocol for review like a shell command; `restricted` is no network; `enabled` is full egress. Per-destination review as the default is a genuinely strong choice — most harnesses' network story is a binary, and the ones that default open never ask.

## Escape hatches

Three flags, and the two-layer model tells you what each one costs:

- **`--disable-approval`** — prompts off, sandbox stays on.
- **`--disable-sandbox`** — approval stays on, sandbox off. But note the fine print: this also removes workspace confinement from the file tools (`write_file`/`edit_file` can write anywhere) and **forces the network to full egress**, overriding `--sandbox-network`. One flag, three controls gone.
- **`--yolo`** — both layers off, and the workspace is trusted for the run, which loads the checkout's `AGENTS.md`, rules, and skills. Meta's docs recommend it only for disposable CI containers and carry the right warning: on a PR or fork checkout, those files are attacker-controlled instructions. Trusting the workspace is part of what the flag disables — that's the part people miss.

One question the docs don't answer: **the empty chair.** What happens to a held approval when `muse exec` runs headless and nobody's there — deny, timeout, hang — is not documented. Until Meta documents it, don't hand Muse Code an unattended job in any mode where something can hold. (Our test for this is [the empty-chair test](/blog/interactive-vs-autonomous-the-empty-chair-test); [dsh](/controls/dsh) is the harness that answers it natively.)

## Hooks: the coverage is wide, the boundary is elsewhere

The [hooks system](https://dev.meta.ai/docs/muse-code/extending.md) covers the full lifecycle — `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PreLLMCall`, `PostLLMCall`, `PreCompact`, `PostCompact`, `SubagentStart`, `SubagentStop`, `Stop` — and a hook can block an action before it happens. `PreLLMCall`/`PostLLMCall` and the subagent lifecycle events are broader coverage than most harnesses expose, and `PermissionRequest` means a hook can stand inside the approval flow itself.

Registration comes from three sources with three trust levels:

| Source | Location | Trust |
|---|---|---|
| Project | `<project-root>/.muse/hooks.json` | explicit trust required (`muse hooks trust <key>`) |
| User | machine-wide settings file | explicit trust required |
| Managed | file at the `managed_hooks_path` setting | **pre-approved, no trust step** |

Managed hooks are the first fleet-level control primitive we've seen ship in a major harness — centrally administered hooks that apply without per-machine consent. The docs state the flip side plainly: whoever controls the managed hooks file controls what executes. Treat that path like you treat sudoers.

Two honest caveats, one from the docs and one from a real install:

1. **Hooks run outside the sandbox.** A hook's command executes directly through your shell, outside both the sandbox and approval; the only hardening is a cleared environment with a small allowlist. The same is true of MCP tools — approval applies, containment does not. The strongest sandbox in the ecosystem has two documented doors around it, and both are the extension points.
2. **The docs and the binary describe two different front doors.** The documented `muse hooks` management CLI (`list`, `validate`, `trust`, `run --fixture`) is not exposed by the shipped 0.2.1 beta binary. What the binary has instead — we verified this on a real install — is a plugin system behind a `MUSE_EXPERIMENTAL_PLUGINS` flag: hooks register as capabilities of a native plugin (`.muse-plugin/plugin.json`), installed and trusted through `muse plugins install` / `approve`, with a per-hook fixture runner (`muse plugins hook test`). The flag gates only that management CLI; once a plugin's hooks are approved, they fire in ordinary unflagged sessions. The hook payloads and decision outputs follow the same schema Claude Code hooks use, down to the field names. Beta means beta — expect this to converge on the documented CLI.

## Multi-agent

A lead session spawns child agents for bounded tasks. **By default, children share the lead's workspace** — parallel writes can collide. Worktree isolation is opt-in via `muse --subagent-worktree-isolation`, which gives each child its own git worktree checked out from the lead's commit; in a non-git workspace the flag is silently ignored. Children can't spawn their own children, concurrency is capped around core count, and the runtime journals every spawn, status change, and control action. If you've seen coverage saying subagents "each run in their own worktree," that's the flag, not the default — check which one your pipeline runs.

## The event log

Underneath it all, Muse Code keeps an append-only local event log of every model call, tool run, approval, and edit — Meta describes it as replay-exact and restart-safe, and it's one of only two native records in our [comparison](/controls) that isn't just a transcript. The limits are the usual ones: it lives on the machine that ran the agent, it's authored by the process it describes, and nothing in the docs says it records what the approval judge decided or against what. A strong crash-recovery journal is not yet a decision ledger.

## Where the native model ends

Muse Code's native model is the best default posture shipping today: sandbox on, approvals on, fail-closed when the sandbox can't be verified, deny-beats-allow, staged shell review, and self-protection for its own config. What it doesn't have:

- **The judge is unexplained.** The LLM approval judge decides automatically, and no documented record captures what it approved or why.
- **The extension points bypass the boundary.** Hooks and MCP tools run outside the sandbox — the places you'd add controls are the places the containment doesn't reach.
- **The empty chair is undocumented.** Headless approval resolution is unspecified; unattended use is a guess.
- **Everything is per-machine.** Prefix rules are scoped to a workspace root on one machine; the event log stays local; managed hooks are the one org-level primitive, and they're a pointer to a file, not a policy you can query.
- **One flag undoes both layers**, and nothing independent records that it was passed.

Those gaps are structural, and they're the same four we find in [every harness we've surveyed](/controls): self-authored records, per-machine policy, escape hatches without evidence, rules that don't travel to the next harness. That's the job of a layer above — one policy, enforced identically, with an independent off-machine record, across every harness you run. [Our Muse Code integration](/integrations/muse-code) ships that layer for Muse Code today; the [coverage matrix](/coverage) tracks the rest of the harnesses as we cover them.

## Frequently asked questions


