Muse Code Hooks Reference — Plugin-Only, Claude's Wire Schema, Outside the Sandbox
Running Muse Code? See and control every tool call it makes — allow/ask/deny before each call, the reason on every row, one installer:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Full Muse Code guide → · see your first checked call → · free up to 5 agents
Just want the answer?
On the current 0.2.1 beta, a Muse Code hook isn't a file you hand-edit — it's a plugin capability, gated behind an experimental flag until it's approved once:
MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins install @agenticcontrolplane/muse-code
MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins approve @agenticcontrolplane/muse-code
Full walkthrough: Muse Code install guide. Native control model: Muse Code permissions, explained.
Muse Code, Meta’s terminal coding agent, shipped its public beta with the strongest default posture of any harness in this reference series — an OS sandbox and tool approvals both on from the first run. Its hook system inherits none of that hardening. A hook command runs directly through your shell, outside both the sandbox and the approval layer, and the only way to register one on the shipped 0.2.1 binary is a flag Meta’s own extending-Muse-Code docs don’t mention. This is the reference for what’s actually wired today, the wire format it speaks, and what running outside the sandbox means for a hook meant to be your control point.
The docs and the binary describe two different front doors
Meta’s documentation describes a muse hooks management CLI and a project-level .muse/hooks.json file. Neither is wired in the 0.2.1 beta: the CLI subcommand isn’t exposed, and a hand-written project hooks.json is silently ignored — no error, no warning, it just never fires.
What’s actually wired is the native plugin system, gated behind MUSE_EXPERIMENTAL_PLUGINS=1. A plugin (.muse-plugin/plugin.json) declares hooks as one of its capabilities; muse plugins install and muse plugins approve register and trust it. The flag only gates the management CLI — once a plugin is approved, its hooks fire in ordinary sessions with no flag required.
Trust levels: project, user, managed
| Level | Where | Trust step |
|---|---|---|
| Project | <project-root>/.muse/hooks.json |
muse hooks trust <key> per project |
| User | machine-wide settings file | explicit trust on first use |
| Managed | file at managed_hooks_path |
pre-approved — no trust step |
Managed hooks are the first fleet-level control primitive documented in any of the harnesses in this series: a hook installed through the managed path runs with no per-user approval at all. Whoever controls that file controls what executes on every machine pointed at it — treat it with the same care as a sudoers file, not as a convenience setting.
Installing the ACP plugin — the only path that works today
MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins install @agenticcontrolplane/muse-code
MUSE_EXPERIMENTAL_PLUGINS=1 muse plugins approve @agenticcontrolplane/muse-code
Or via the installer, which detects Muse Code and does the same:
curl -sf https://agenticcontrolplane.com/install.sh | bash
The wire schema: Claude Code’s shape, verified
Every field name matches Claude Code’s hook contract exactly. Input is snake_case — hook_event_name, tool_name, tool_input, session_id, permission_mode. Output is camelCase — a decision under hookSpecificOutput.permissionDecision. Exit code 2 plus stderr is a fallback deny channel, and an advisory systemMessage string is shown to the model without altering the decision.
| Event | Gateway call | Effect |
|---|---|---|
PreToolUse |
POST /govern/tool-use |
deny blocks before the call runs |
PermissionRequest |
POST /govern/tool-use |
settles Muse Code’s own native prompt |
PostToolUse |
POST /govern/tool-output |
audit + content scan after execution |
Stop / SubagentStop |
— | receipt only, no decision |
The event surface is broader than most harnesses here: SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, PreLLMCall, PostLLMCall, PreCompact, PostCompact, SubagentStart, SubagentStop, Stop. The PreLLMCall/PostLLMCall pair and full subagent lifecycle aren’t exposed by every harness in this series.
Outside the sandbox, cleared environment — what that implies
Muse Code’s own sandbox (Seatbelt on macOS, bubblewrap on Linux) is fail-closed and confines writes to the workspace plus temp; .git, .muse, and .agents stay read-only even from inside it. None of that reaches a hook. Hook commands execute directly through the shell, outside both the sandbox and the approval layer — the only hardening applied is a cleared environment with a small allowlist of variables passed through. MCP tools get the identical treatment: approved, not contained.
Two direct implications for anything running there:
- Credentials don’t arrive via env. An
ACP_BEARER_TOKENexported in your shell does not reach the hook process, because the environment is cleared before it runs. The plugin instead reads a credential file (~/.acp/credentials) written once by the installer, with optional config overrides in~/.acp/config.jsonfor the same reason —govern_base,agent_tier,check_timeout_ms, and ashadowflag all live in that file rather than as env vars a hook could read. - Every dependency is a supply-chain target. A hook is a genuine extension point outside Muse Code’s own strongest containment — which is why the ACP integration for it ships as a single file of plain ESM with zero dependencies and no build step, small enough to read start to finish before you trust it.
Failure posture
Interactive sessions fail open, loudly — a stderr warning and a durable line in ~/.acp/lapse.log. Unattended sessions (CI, MUSE_HEADLESS, or an explicit non-interactive agent_tier/permission_mode) fail closed instead, because nobody is watching the scrollback to notice a gap. An explicit deny holds in either posture.
Beta churn — what to expect to change
The 0.2.1 release notes flag breaking changes ahead. The documented muse hooks CLI the binary doesn’t yet expose is the clearest sign of where this is heading — expect the flag-gated plugin path used today to converge toward that documented surface in a later beta. Re-verify after every Muse Code update rather than assuming today’s install command survives unchanged.
Known limitations
- Hooks see tool calls, not tokens: model spend isn’t metered for Muse Code today. No proxy or transcript path ships for it, so tool-call rows are what land in the audit trail, not per-call token cost.
- The hook fires exactly where containment stops — it’s the control point and the edge of the sandbox at the same time. Design accordingly rather than assuming the sandbox’s guarantees extend one step further.
- Beta docs currently describe a management surface the beta binary doesn’t expose; re-check after upgrades.
Comparing to Claude Code hooks
The wire schema is close to identical — this is, byte for byte, the same JSON shape Claude Code’s hooks use. What differs is the install path (a flagged plugin system versus a settings file) and the sandbox relationship: Claude Code has no native OS sandbox to compare against, so “a hook runs outside the sandbox” is a Muse Code-specific caveat with no Claude Code analog. See the Claude Code hooks reference for the full contract on that side.
Frequently asked questions
How do I install a hook in Muse Code today?
Through the plugin system, behind the MUSE_EXPERIMENTAL_PLUGINS=1 flag: muse plugins install and muse plugins approve register a plugin whose capabilities include hooks. The documented muse hooks CLI and project .muse/hooks.json file aren’t wired in the 0.2.1 beta binary.
Is Muse Code's hook payload the same shape as Claude Code's?
Yes, verified against the beta binary. snake_case fields in (hook_event_name, tool_name, tool_input, session_id, permission_mode), camelCase hookSpecificOutput decisions back out, exit code 2 plus stderr as a fallback deny channel, and an advisory systemMessage line.
Do Muse Code hooks run inside the sandbox?
No. Hook commands execute directly through your shell, outside both the OS sandbox and the approval layer, with a cleared environment as the only hardening. MCP tools get the same treatment — approved, not contained.
What happens if a Muse Code hook's policy backend is unreachable?
In interactive sessions it fails open loud, with a warning and a durable lapse-log line. In unattended sessions (CI, MUSE_HEADLESS, or an explicit non-interactive agent_tier) it fails closed. An explicit deny holds either way.
Where to read more
- Muse Code permissions, explained
- Muse Code install guide
- Claude Code hooks reference
- Codex hooks reference
- muse-code-acp-plugin source