This is a follow up to a post on on agent sandboxes.
The last time we reviewed a couple agent sandboxes one commenter succinctly summarized "its firecracker all the way down", however lucky for us just a day later Meta launched Muse to give us a new VM to explore!
As part of the launch one of the bold claims was that Muse is the faster alternative to some of their competition which was covered last time. So lets look under the hood and see what they have been up to. Like last time we will do this by dialing a shell out through ws-term and looking around with the usual tools. One last note before we begin is I'll spend the first half on the sandbox itself and the second half on the agent harness bits we can see from inside it so feel free to skip to the section you're interested in since this post is a tad bit longer than the last!
Not Firecracker
Right of the bat the first thing you notice is that systemd-detect-virt and the DMI table
disagree about what kind of machine this is, and both are right:
$ systemd-detect-virt
systemd-nspawn
$ cat /sys/class/dmi/id/product_name
cloud-hypervisor
$ uname -r
7.0.0-26-generic # a stock Ubuntu kernel, not a custom build
$ hostname; id; nproc; free -h | grep Mem
htch-runtime
uid=0(root) gid=0(root) groups=0(root)
2
Mem: 7.7Gi 2.1Gi ...
So the shell is root inside a systemd-nspawn container, and that container is
running inside a VM whose DMI vendor, product, and BIOS strings all read Cloud
Hypervisor. The container shares the VM's kernel, which is why it's a plain
Ubuntu -generic kernel rather than the -fc- custom build Claude Code boots.
Nothing on the box calls itself Muse in a path. The daemon is hatch, the env vars are
JARVIS_*, the VM is a "hatchling," the bootstrap cert is the "yolk," the domain is
metaaivm.com and its DNS edge is metaclaw. The product name shows up in what the
runtime ships: PROACTIVE_PREFERENCES.md in the home directory says "Muse reads this
whole file before composing the day's edition," the skills tree has muse_db and
muse-feedback, and the image carries models/ultra_muse_1000. Meta's own
security post
confirms it: "Muse (codebase name: Hatch)." I'll come back to that post at the end,
because it turns out to describe this box quite precisely.
$ cat /sys/class/dmi/id/sys_vendor /sys/class/dmi/id/product_name /sys/class/dmi/id/bios_version
Cloud Hypervisor
cloud-hypervisor
0
$ cat /proc/cmdline
console=ttyS0 root=/dev/vda rootfstype=btrfs ip=dhcp ds=nocloud;s=http://169.254.169.254/latest/
lsm=landlock,lockdown,yama,integrity,apparmor,bpf ro
systemd.set_credential=vmm.notify_socket:vsock-stream:2:512
Cloud Hypervisor and Firecracker are siblings: both Rust VMMs on KVM built on the same
rust-vmm crates. Firecracker came out of AWS Lambda and is deliberately tiny;
Cloud Hypervisor came out of Intel, now lives at the Linux Foundation, and is the
"grown-up" one. The differences that matter for a product like this:
| Firecracker | Cloud Hypervisor | |
|---|---|---|
| Origin | AWS (Lambda, Fargate) | Intel, now Linux Foundation |
| Bus | virtio over MMIO only, pci=off |
Full PCI, virtio-pci |
| SMBIOS/DMI | none (empty product_name) |
present, vendor string "Cloud Hypervisor" |
| Hotplug | no | vCPU, memory, disk, NIC hotplug |
| Devices | block, net, vsock, balloon | + vhost-user, virtio-fs, virtio-pmem, TPM, GPU passthrough |
| Balloon | inflate/deflate only | + free-page reporting (guest returns freed memory automatically) |
| Boot | direct kernel only | direct kernel or firmware (UEFI/OVMF) |
| Snapshot/restore | yes | yes |
| Sandbox | its own jailer |
seccomp, expects an external sandbox |
Muse uses the right column: the guest has four virtio-block disks it can swap, a
vsock notify channel back to the VMM, a balloon with free-page reporting, and later a
browser VM that gets leased in per task. Both E2B and Claude Code showed the
Firecracker signature last time (pci=off, empty DMI, virtio_mmio.device=). This one
has a PCI bus and a BIOS string.
$ for d in /sys/bus/virtio/devices/*; do echo "$(basename $d) $(cat $d/device) $(basename $(readlink $d/driver))"; done
virtio0 0x0002 virtio_blk virtio3 0x0001 virtio_net virtio5 0x0013 vmw_vsock_virtio_transport
virtio1 0x0002 virtio_blk virtio4 0x0004 virtio_rng virtio6 0x0005 virtio_balloon
virtio2 0x0002 virtio_blk virtio7 0x0002 virtio_blk
$ cat /sys/bus/virtio/devices/virtio6/features | cut -c1-6
011001 # stats_vq, deflate_on_oom, and bit 5: free page REPORTING
That last bit is how a per-user VM that stays up is affordable: the guest hands freed
pages back to the host as it frees them, so an idle hatchling costs its ~2 GB working
set rather than its 8 GB allocation, and two vCPUs at load average: 0.00 on a
126-core EPYC cost nothing.
The two boxes
$ lsblk -o NAME,SIZE
vda 7.3G # VM root, btrfs, read-only (ro on the cmdline)
vdb 7.5G # overlay upper: the writable scratch layer for /
vdc 587M # the "hatch image": daemon, tools, models, squashfs + verity
vdd 100G # yours: the RV, btrfs, mounted at /home/hatch
$ df -h | grep -vE tmpfs
overlay 7.5G 17M 7.5G 1% /
/dev/mapper/rv 100G 111M 99G 1% /home/hatch
The container has no /dev/mapper, but /sys/block still lists the device-mapper
targets with their UUIDs, and the UUID prefix tells you what each one is:
$ for d in /sys/block/dm-*; do echo "$(cat $d/dm/name) $(cat $d/dm/uuid)"; done
root_overlay CRYPT-PLAIN-root_overlay # vdb: dm-crypt plain mode, a throwaway key
opt_hatch CRYPT-VERITY-0000...-opt_hatch # vdc: dm-verity, integrity-checked, not secret
rv CRYPT-LUKS2-5b3a29f5...-rv # vdd: LUKS2. Your volume is encrypted at rest
So the tools image is measured (verity), the scratch layer is encrypted with a key
nobody keeps, and the RV, which one script expands as "Reliable Volume," is a real
LUKS2 volume holding /home/hatch, /var/lib/hatch, and the Postgres data directory.
The split is the same idea as Claude Code's yours vs theirs disks, but with a
container drawn around it:
flowchart TB
subgraph vm["Cloud Hypervisor microVM (the hatchling)"]
direction TB
hostsvc["VM-side systemd: spawnd, sentinel, authd, hatch-safety, postgres, browser-broker, ingress-rev-proxy"]
subgraph cell["systemd-nspawn runtime cell (htch-runtime)"]
execd["hatch-execd, spawns your shells"]
daemon["hatch daemon, the harness (entered from outside)"]
home[("/home/hatch on vdd (rw, LUKS2), the RV, persists")]
opt[("/opt/hatch squashfs on vdc (ro, verity), tools + skills + models")]
end
hostsvc -->|nsenter into the cell| daemon
hostsvc -->|nsenter into the cell| execd
end
daemon -->|unix socket the cell can't see| inference["inference proxy"]
The VM-side services and the cell share a PID namespace but not a mount namespace.
hatch daemon and hatch-execd show up in ps inside the cell with PPID 0: they
were started on the VM and nsentered in. Even as cell root you cannot read
/proc/<daemon>/ns/* or /proc/<daemon>/root. That's the same trick as Claude Code's
sealed process_api, except the operator process lives next to your tenant
namespace instead of being PID 1 of it.
$ ps -eo pid,ppid,cmd | head -5
1 0 /usr/lib/systemd/systemd
17 1 /usr/lib/systemd/systemd-journald
1347 0 /opt/hatch/bin/hatch-execd --runtime-cell-leader=1957 # PPID 0: entered from the VM
1586 0 /opt/hatch/bin/hatch daemon --runtime-cell-leader=1957
$ readlink /proc/1586/ns/mnt
# (empty: not yours to see)
$ ls /run/hatch/
auth cell-anchors egress-tls egress-tz noded privsep resume runtime-cell sandbox sandbox-api telemetry
# no proxy/, no daemon/: inference socket is not in the cell's view
Root in the cell is uid 131072 on the VM, a user namespace, and the shell you get is deliberately clipped:
$ grep -E 'Seccomp|NoNewPrivs' /proc/self/status
NoNewPrivs: 1
Seccomp: 2
Seccomp_filters: 4
$ python3 -c 'import ctypes,os; l=ctypes.CDLL(None,use_errno=True); l.syscall(425,0,0); print(os.strerror(ctypes.get_errno()))'
Operation not permitted # io_uring_setup is filtered
$ capsh --print | grep -o 'Bounding set.*' | grep -cE 'sys_ptrace|net_admin'
0 # no CAP_SYS_PTRACE, no CAP_NET_ADMIN, no CAP_NET_RAW, no CAP_SYS_MODULE
Even your tool shells are isolated from each other: my bash was in a different mount
namespace from the cell's PID 1. Every exec the agent runs gets its own view.
How it boots
Lucky for us this is well documented in the scripts /opt/hatch/runtime-cell/ and /opt/hatch-image/bin/ which describe a VM that boots with no owner and gets one attached. Lets summarize the key points for performance as I see it.
The VM boots with no identity. hatch-prewarm runs "after hatch-init, before any RV
attach" and describes the cell boot files as "identityless preboot." The env carries
JARVIS_IS_ASSIGNED=0. Attaching you to a hatchling is spawnd rv-graft:
rv-graft Graft RV identity onto the pre-booted runtime: verify the RV binds,
superficially validate the resume handoff marker, ensure the os-intent
ledger dir, and write the boot-id-stamped /hatch/data/resume/rv-identity-ready
Nothing is installed at boot. The VM root is read-only btrfs built with mkosi;
/opt/hatch is squashfs on dm-verity (the prewarm script calls it the "measured
rootfs"); the cell's rootfs is reconciled against a KDL manifest (runtime-cell.kdl:
~110 apt packages, from build-essential to libreoffice-*-nogui, tigervnc, xvfb,
cloudflared, nodejs 24), and the distro's own apt-daily/unattended-upgrades
timers are retired with inert unit overrides so nothing phones home unattributed.
Packages the agent installs are recorded as intent in an append-only ledger on the RV
and replayed, not persisted in the rootfs.
The page cache is pre-warmed. Before the RV attaches, vmtouch -t touches the
Postgres server and its ldd closure, the hatch daemon binary, systemd-nspawn's
closure, and the cell's init/loader/libc, under a 256 MiB budget with a 10 s
self-deadline, "fail-open everywhere." A health gate requires is-system-running to
be exactly running before a VM is eligible, so nothing in this path may fail a unit.
The cell's boot target is nearly empty.
$ systemctl list-units --type=service --state=running
systemd-journald.service # that's it
$ systemd-analyze
Startup finished in 2.212s (userspace)
default.target reached after 213ms in userspace
default.target is a custom unit whose only Wants= is hatch-execd.socket.
Everything else (Postgres, Sentinel, authd, safety, telemetry, the browser broker)
lives on the VM side and is bind-mounted in as Unix sockets.
The harness ships separately from the OS. hatch --version printed a commit built
at 2026-09-11T02:29:54Z, about two hours before I read it, on
JARVIS_CD_CHANNEL=alpha. The daemon is a "live-update bundle" with its own trust root
(the prewarm script refuses to ldd it for that reason), so a new harness does not
mean a new VM image.
Watching a rollout
Three times during the evening my shell dropped, the redial loop reconnected, and the box had a different boot time and a different harness build:
$ grep btime /proc/stat; hatch --version
btime 1789094439 hatch 0.1.0 (17400c3e011) built 02:29:54Z # 19:40, the VM I started on
btime 1789100040 hatch 0.1.0 (17400c3e011) # 21:14
btime 1789102110 hatch 0.1.0 (5d489a6707a) built 03:11:58Z # 21:48
btime 1789105122 hatch 0.1.0 (052113f66d3) built 03:38:10Z # 22:38
$ journalctl | grep "Startup finished"
Sep 10 21:14:13 htch-runtime systemd[1]: Startup finished in 2.307s.
$ cat /run/hatch/resume/rv-identity-ready; cat /run/hatch/resume/execution-ready.marker
6c7da3cc-... 2026-09-11T04:14:21.732Z marker=present
2026-09-11T04:14:39.110151343+00:00
| Stage | Wall clock | Since kernel boot |
|---|---|---|
Kernel boot (btime) |
21:14:00 | 0 s |
Cell default.target |
21:14:13 | 13 s |
| RV grafted, identity known | 21:14:21 | 21 s |
| Second CA refresh (identity-specific anchors published) | 21:14:35 | 35 s |
execution-ready marker |
21:14:39 | 39 s |
That's the deploy mechanism: a new build on the channel means a fresh hatchling with
the new bundle, graft the RV, tear down the old one, ~40 s of outage. My home directory
came back with every file intact and the same timestamps each time, because none of it
lived on the VM. /usr/sbin/reboot is overwritten with a script that says the same
thing in the other direction:
# A hatchling cannot reboot in place. The VMM does not survive a guest reset:
# the VM disappears, the hatchling goes UNHEALTHY, and the workflow recovers by
# REPLACING it with a brand new VM. That was measured, not assumed ...
# So `reboot` already means "replace this VM" ... Powering off reaches the same
# end state with services stopped in order.
The daemon's telemetry fields track it as such (rollout_id, release_id,
previous_build_id, target_build_id, outage_duration_ms, pre_deploy_all_healthy)
and a whole family of hatch_mount_to_ws_ready_* metrics (postgresql_startup,
db_preflight, runtime_cell, daemon_active_to_ws_ready, readiness_bottleneck)
covers the graft-to-ready window in the table above.
Who manages the lifecycle
The binaries contain the interface the VM presents to whatever runs it, and from inside it looks like this:
- Metadata at boot. The kernel cmdline points cloud-init's
noclouddatasource at169.254.169.254, served by the VMM host; that's where per-VM identity, channel, and region (JARVIS_VM_COMPUTE_REGION=scu) arrive. - Readiness over vsock.
vmm.notify_socket:vsock-stream:2:512makes systemd'ssd_notifygo straight to the VMM, and the prewarm comments say the control plane's "health gate requires exactlyrunning." - Health from inside.
hatch-healthdpolls the daemon's/healthon a metrics socket "so host-side healthd can dial it without going through the runtime cell's IPv6 veth." - Rescue.
hatch-rescuehasrestart-component,emit-flare, and, my favourite,emit-codex-session: it spawns acodex app-serverin a "rescue codex home" to diagnose akernel_incident. That's what the 258 MB OpenAI Codex CLI in/opt/hatch-image/bin/is for. The VM's on-call is an agent. - Replacement. The reboot script calls it "the workflow," and the daemon posts to
/v1/runtime-eventsand/v1/leased-vm-resources-eventswithhost_regionandgit_sha.
What is metaaivm.com?
Every hatchling has a public name:
JARVIS_FQDN=70f9aa6e-1767-497b-96fd-6bbac24fe79b.metaaivm.com
$ dig +short 70f9aa6e-....metaaivm.com
edge-metaclaw.c10r.facebook.com.
57.144.221.192
$ host 57.144.221.192
... edge-metaclaw-shv-01-sjc6.facebook.com. # a Meta PoP in San Jose, closest to me
$ whois metaaivm.com | grep -E 'Creation|Name Server' | head -2
Creation Date: 2026-02-26
Name Server: A.NS.FACEBOOK.COM
The per-VM hostname is a CNAME into Meta's edge (c10r is their edge tier; the domain
was registered this February). Behind it, inside the VM, is ingress-rev-proxy, an
"Internet-facing ingress reverse proxy with Noise_XX encryption": :443 for infra
(health, version, journal, rescue, yolk) and :4431 for the public data
plane (ping, v1/noise, spaces). From my laptop, 443 accepts the TCP connection and
then resets the TLS ClientHello, and 4431 isn't exposed at the edge at all. Without the
Noise handshake the edge has nothing to forward.
This is the biggest architectural difference from last time. Claude Code's VM has
no inbound at all; Instinct's E2B box is only reachable by the backend. Muse's VM
is a server with a DNS name. The phone app holds a persistent Noise session to your
VM through Meta's edge, and spaces (a route on the public listener, with a
space.json build format in the daemon's strings and four space-*.sock sandboxes in
/run/hatch/sandbox/) are web apps the agent builds and serves from your VM.
The persistent session is also why the app feels the way it does. The "responding"
bubble appears the instant you hit send because the daemon's admission step is a few
writes to local Postgres and the typing indicator fires there, before prompt assembly
and long before a model token. The daemon instruments the whole path
(ingress_ms, admission_ms, pre_inference_prompt_assembly_*_ms, time_to_llm_ms,
time_to_first_response_token_ms, tokens_per_second), and nothing in it is a VM
waking up.
flowchart LR
phone(["Muse app"]) -->|TLS + Noise_XX| edge["edge-metaclaw.c10r.facebook.com"]
edge -->|:4431 public / :443 infra| irp["ingress-rev-proxy (in the VM)"]
irp --> daemon["hatch daemon"]
irp --> spaces["spaces: agent-built web apps"]
daemon -->|"/v1/messages, avocado, gpt"| inf["inference proxy"]
subgraph cell["runtime cell"]
shell["your exec / ws-term"]
end
shell -->|CONNECT, MITM| sentinel["Sentinel egress proxy :3128"]
sentinel -->|allowlist + HITL approval| internet(["internet"])
Egress
The cell has one veth with a /30 and a gateway that is also the DNS server and the proxy:
$ ip -br a
host0@if3 UP 198.19.0.2/30 fd8b:4f84:7d32:99::2/64
$ grep -v '^#' /etc/hosts | tail -2
198.19.0.1 hatch-egress-proxy
fd8b:4f84:7d32:99::1 hatch-egress-proxy
$ env | grep -i proxy
HTTPS_PROXY=http://hatch-runtime:<rotated-per-boot>@hatch-egress-proxy:3128
NO_PROXY=localhost,127.0.0.1,::1,198.19.0.1,198.19.0.2,...
CURL_CA_BUNDLE=/run/hatch/egress-tls/ca-bundle.pem # also GIT_SSL_CAINFO, NODE_EXTRA_CA_CERTS, AWS_CA_BUNDLE
What happens when you try to get around it:
$ dig +short @8.8.8.8 example.com
198.18.123.158 # not example.com: a fake IP from the 198.18/15 bench range
$ curl https://1.1.1.1/ # direct 443, no proxy
curl: (35) ... SSL connect error # TCP is answered, TLS is not: the gate ate it
$ curl -sv https://example.com/ 2>&1 | grep -E 'issuer|Connection Established'
< HTTP/1.1 200 Connection Established
* issuer: CN=Hatch Sandbox Egress CA; O=Hatch # MITM'd, like Claude Code's egress gateway
$ curl https://example.com:8443/ # non-standard port
< HTTP/1.1 200 Connection Established
curl: (28) Operation timed out # CONNECT accepted, then held
DNS is answered by the gateway with synthetic addresses so that every connection can
be attributed to a name at the proxy, and the pre-start.sh script attaches an
eBPF connect4/connect6 cgroup hook (spawnd attach-cell-gate) as "the floor UNDER
Sentinel: with BPF_F_ALLOW_MULTI-composed cgroup hooks every program must allow, so a
dead/held-down Sentinel no longer means ungoverned link-local/RFC1918/CGNAT reach."
There's a second classifier on the VM side of the veth that drops frames aimed at
host-local destinations.
Sentinel is the policy engine, and the runtime-cell manifest spells out how it decides, in a comment explaining why the distro's own apt timers were disabled:
# ... their egress reaches Sentinel unattributed ... and fleet-wide HITL prompt
# storms whenever they dial a host the managed system-egress allowlist does not
# carry (the 2026-08-15 cli.github.com storm).
So the policy is per host: a managed allowlist (managed-internal-network-policies.yaml
is named as the file), and a human-in-the-loop prompt for anything off it. Unattributed
traffic, meaning a process Sentinel can't tie to a cell cgroup, is what the eBPF gate
is there to stop. The proxy password and the CA bundle both rotate on every VM
replacement, and a hatch-ca-trust.path unit watches for rotation. The one thing the
box never sees is an inference endpoint: api.anthropic.com returned a 404 through the
proxy, reachable but keyless, and the daemon's inference socket isn't mounted in the
cell.
Harness
The harness is one 327 MB Rust binary (similar size to Claude Code's Bun harness was 324 MB), and the Cargo registry paths baked into it are the dependency list:
$ strings hatch | grep -oE 'index\.crates\.io-[a-f0-9]+/[a-zA-Z0-9_-]+-[0-9]' | sed 's/.*\///; s/-[0-9]$//' | sort | uniq -c | sort -rn
tokio hyper axum tokio-tungstenite reqwest rustls h2 # async runtime, HTTP, WebSocket
sqlx-postgres sqlx-sqlite sqlparser # Postgres client, and a SQL parser for muse.db
genai # multi-provider LLM client (rust-genai)
ort fastembed tokenizers hf-hub ndarray # ONNX runtime: the local classifiers + embeddings
boa_engine boa_parser # a JavaScript engine, in Rust
seccompiler # builds the seccomp filters your shell runs under
tree-sitter scraper html5ever lopdf calamine image # parsing code, HTML, PDF, xlsx, images
jsonschema schemars moka prometheus tracing-subscriber
$ strings hatch | grep -ciE 'langgraph|langchain|rmcp|modelcontextprotocol'
0
The one open-source agent-ish component is genai, a provider-agnostic Rust LLM
client, which is how one binary talks to several model families behind a single
interface:
$ strings /opt/hatch/bin/hatch | grep -oE '(claude|avocado|gpt)-[a-z0-9.-]+' | sort | uniq -c | sort -rn | head
3 avocado-5.16-v4 # internal model family, also "avocado-memory-flush-v1"
2 claude-opus-4-6
2 claude-opus-4-8
1 claude-sonnet-4-6
1 gpt-5.6-sol
1 gpt-5.5-codex
$ strings /opt/hatch/bin/hatch | grep -c anthropic # "failed to parse anthropic response", "/v1/messages"
The transport is Meta's internal gateway, which the code calls ipnext
(ipnext/stream.rs, ipnext/avocado-5.16-v4, ipnext/proxy_readiness.rs), and
JARVIS_ANTHROPIC_TIMEOUT_MS / JARVIS_AVOCADO_CONTEXT_WINDOW_TOKENS are separate
knobs, so the providers are first-class rather than hidden behind a router. Inference
leaves the daemon over JARVIS_INFERENCE_PROXY_SOCK, a Unix socket the cell cannot
reach, so which model answers is a server-side decision. Meta's post names the
headline model as Muse Spark 1.3; avocado is presumably its codename.
It is an agentic loop, and the symbol names lay out its stages:
admission → pre_inference (skill_loading, prompt_assembly, context_pressure, compaction)
→ model stream (ipnext / anthropic /v1/messages / openai responses)
→ tool_dispatch_batch (+ approval_intent)
→ post_tool_continuation → post_inference_terminalization
agent_loop(49) message_execution(210) objective_worker(258) subagent_spawns(248) compaction(152)
Compaction is eager and runs in the background (started_eager_background_compaction,
adopted_eager_background_compaction), so the context summary is ready before the
window fills, and it has its own model (avocado-memory-flush-v1). Objective
workers are long-running goals with their own dispatch and "notification steps,"
which is what powers the proactive feed editions. Sub-agents are first-class
(subagent_spawns, subagent_monitor, the SUBAGENTS_MONITORING.md policy file in
your home directory) and each one gets its own transcript under
agents/agent-<uuid>/sessions/. Every stage has a timeout env var
(JARVIS_TOOL_DISPATCH_TIMEOUT_MS, JARVIS_MODEL_STREAM_FIRST_CHUNK_TIMEOUT_MS,
JARVIS_POST_INFERENCE_TERMINALIZATION_TIMEOUT_MS ...) and a restart checkpoint in
Postgres, which is how a session survives the VM being replaced mid-turn.
One super cool feature to see is there are sidecar classifiers, a lot of them:
$ strings hatch | grep -oE '[a-z0-9_]*(classifier|prefilter|gatekeeper|judge|guard)[a-z0-9_]*' | sort | uniq -c | sort -rn | head
198 judge 75 guard 70 classifier 45 gatekeeper
27 safety_classifiers 13 policyguard 12 prefilter 11 visual_browser_screenshot_guard
8 cbrne_returned_text_guard 8 browser_action_guard 7 phone_call_judge
The named models: 9b_safety_classifier, 2b_tool_call_classifier_v0_4,
pi_3b_prefilter (a prompt-injection prefilter; there's also an "ORIGIN GATE" prompt
that classifies which source of text could carry an injection, and
JARVIS_EXTERNAL_CONTENT_BOUNDARIES_ENABLED). Some run on-box through ort, the rest
behind the hatch-safety socket on the VM side. A separate /alignment/system/steps/
pipeline (judge, reflect, repair, distill, generalize) and a
self_improvement schema in Postgres grade and rewrite the agent's own behaviour
offline, and JARVIS_DEFAULT_MAX_TRAINING_TIER reads like a per-user consent level for
what can be used as training data.
Some models are on the box too:
$ ls /opt/hatch-image/models/
asr/ faster-whisper-tiny # local speech-to-text
memory/ models--Qdrant--all-MiniLM-L6-v2-onnx # 91 MB embedding
models--jinaai--jina-reranker-v1-turbo-en # 153 MB reranker
$ ls /opt/hatch-image/bin/
bun codex rtc-sidecar hatch-manifest hatch-prewarm ...
$ /opt/hatch-image/bin/codex --version
codex-cli 0.149.0 # OpenAI's Codex CLI, 258 MB, used by hatch-rescue
Memory
Memory is not a git repo of Markdown like Instinct. It's on-VM Postgres, and the
muse_db skill ships the whole schema as a 4,116-line reference so the agent can
query it through a bounded read-only SELECT surface:
$ grep -c '^#### ' /opt/hatch/skills/muse_db/references/schema.md
194 # tables
$ grep '^### ' schema.md | tr '\n' ' '
activity agent device feed goals health ideas ingest media memory messages podcasts
runtime scheduler self_improvement shell spaces
$ grep '^#### `memory\.' schema.md
memory.claims memory.entries memory.entry_attributes memory.embeddings
memory.embedding_models memory.metadata
The memory itself is Markdown under ~/memory/ (dated logs, bank/{world,experience,
opinions}.md, people and group pages), and Postgres is the index over it: every chunk
lands in memory.entries with a path#L<line> citation, as a 384-dim pgvector row in
memory.embeddings from the on-box MiniLM model, and as a tsvector for keyword
search, and the daemon's memory_search tool and the pre-inference recall step run
hybrid retrieval over both with the jina reranker ordering the hits. That's why the
embedder is local: the corpus is the most personal data on the box, it's re-embedded
every time a memory flush appends to it, and a 91 MB model on two cores does that
without the chunks ever leaving the LUKS volume.
The device.* schema is your phone synced in (contacts, call log, calendar events,
upload sessions), agent.* is transcripts, compactions, and sub-agent progress, and
the schema notes say reasoning columns are served through a redacted projection.
It is a real server per VM, not a shared one somewhere else:
$ grep -i pgsql /proc/net/unix | head -1 # net namespace is shared with the VM
... /run/hatch/postgres/.s.PGSQL.5432
$ ls -ld /var/lib/hatch/postgres
d--------- nobody nogroup /var/lib/hatch/postgres # data dir on the RV, mode 000 to the cell
$ find / -xdev -name .git 2>/dev/null
/home/hatch/workspace/wsterm/.git # the only git repo on the box is mine
The server binary lives at /opt/metasql on the VM root (the prewarm script names it
as a "whole-fat-binary" it refuses to page in), the data directory rides the LUKS2 RV,
and the daemon talks to it over the Unix socket with a connection pool
(postgres/pool.rs, postgres/write_retry.rs). The credential is only handed out to
"trusted Hatch database callers": hatch-doctor run from my shell got a 403 from
authd, which authenticates callers by SO_PEERCRED uid and cgroup. Backups are
btrfs: spawnd and the daemon both carry btrfs snapshot / subvolume snapshot /
backup_path strings, which matches Meta's "your VM data is backed up continuously."
The agent's own files is very familiar to those who have seen other 2026 assistants, an OpenClaw-style workspace:
root@htch-runtime:~# ls /home/hatch/
AGENTS.md USER.md hooks
HEARTBEAT.md agents memory
IDENTITY.md assets prompts
MEMORY.md channels runtime.lock
PROACTIVE_PREFERENCES.md config subscriptions
SOUL.md data user
SUBAGENTS_MONITORING.md docs workspace
TOOLS.md dreams
root@htch-runtime:~#
Here we can also see the design which embraces subagents goals and cron jobs:
PROACTIVE_PREFERENCES.md # "Muse reads this whole file before composing the day's edition"
agents/agent-<uuid>/sessions/<uuid>.jsonl # 18 sub-agents so far, transcripts as JSONL
workspace/cron.d/{secondly,minutely,hourly,daily,weekly,monthly,yearly,runonce}
workspace/goals/<slug>/{GOAL.md,briefs,crons,agent_notes,files,references}
workspace/feed/ workspace/scheduler/
Tools
$ ls /opt/hatch/bin | wc -l
70
$ ls /opt/hatch/bin | head -40 | tr '\n' ' '
authdc browser-broker browser-service calendly device-data duffel facebook-cli ... hatch-vault
hatch-ws-client hatch-zeitgeist healthkit-cli instagram-cli ... outlook-mail peloton philips-hue
places plaid printify ... spotify-api stripe-link tailscale tessie-api threads-cli ticketmaster tts ...
$ ls /run/hatch/privsep | wc -l
60
Like Instinct, tools are CLIs rather than MCP servers. Unlike Instinct, they run on
the box, but each one runs as its own privsep worker: spawnd
ensure-worker-accounts creates a hatch-w-<tool> uid per tool, gives it an id-mapped
view of its state directory, and the cell reaches it only through
/run/hatch/privsep/<tool>.sock. Most binaries in /opt/hatch/bin are symlinks to one
hatch-multicall binary that refuses to run under its own name. Two config files
gate which tools and skills a VM even sees by CD channel:
$ cat /opt/hatch/runtime-cell/skill-scopes.conf | grep -v '^#'
hatch-e2e ads_mcp health nutrition
internal-test ads_mcp audio_notes_read documents end-call health nutrition polymarket price-tracker
prod whatsapp
Gated skills are staged host-only and overlaid into /opt/hatch/skills at cell launch
"fail-closed"; an unknown channel reveals nothing.
Credentials never reach the cell. authdc --help describes hatch-authd as the thing
that "operates on auth-files" and "dynamic credentials," gated by peer credentials, and
Meta's post says what those dynamic credentials are: surrogate tokens, so "the agent
never sees real tokens." Purchases get the same treatment through Stripe Link: there's
a stripe-link-checkout-card privsep socket and a checkout-spend.sock on the VM
side, and the post explains that a single-use card number is issued "tied to that
particular merchant, a particular dollar amount, and only valid for a limited period."
The browser system is very similar to what we saw with Instinct with a broker/scheduler concept to route over sessions:
$ browser-broker --help
Long-running broker daemon that routes browser sessions to leased VMVM browsers
--browser-vm-image <BROWSER_VM_IMAGE>
Immutable leased browser VM image ref
--socket <SOCKET>
... that subdir is DELIBERATELY not bind-mounted into the runtime cell:
the cell has NO path to the broker. The sole client is the host daemon,
admitted by exact cgroup identity plus --allowed-peer-uid; the in-cell→broker
bind remains absent to close the HiTL-consent bypass
--lease-helper-socket <LEASE_HELPER_SOCKET>
UDS of the privileged lease helper the on-demand lease router relays lease
ops through (forwards to the stefi-proxy; performs the viewer bind-mount)
--retain-vmvm-for-debug
Development only: retain an already-active failed VMVM route and its
broker-private Docker API socket until the 15-minute lease TTL or broker
shutdown ... restarts an exited retained browser container without
recreating it. Has no effect on local-cell routes or VMVM admission
"VMVM" is Meta's word, never expanded, and that help text is the whole evidence for
it: a browser session is routed to a leased VM built from an "immutable browser VM
image," inside which the browser is a Docker container reachable over a broker-private
Docker API socket, on a 15-minute lease obtained through stefi-proxy. Only the daemon
can reach the broker, so a tool the agent runs in the cell can't drive a logged-in
browser without going through the consent flow. The sub-agent driving it gets an
accessibility-tree snapshot rather than the DOM, with no script execution and DevTools
disabled (visual_browser_screenshot_guard and browser_action_guard are the
classifiers watching it). /opt/meta-chromium is also shipped in-cell for headless
work that doesn't need your cookies.
The lease itself is brokered by something called Stefi. There's a
JARVIS_STEFI_PROXY_SOCK next to the inference socket, and the daemon's strings show
what goes through it:
$ strings /opt/hatch/bin/hatch | grep -i stefi
hatch-engine/crates/hatch-browser-lease/src/stefi.rs
stefi_create stefi_status stefi_renew stefi_release # browser VM leases
(Stefi `consent_uri`, return_uri-stamped) resolved by the auth-status # connector OAuth
STEFI WhatsApp cursor conflict: # WhatsApp channel pairing
parse Stefi onboarding avatars: # onboarding assets
Opaque monotonic Stefi version. Compare only; do not display as a count.
So it seems like stefi-proxy is the VM's one door into Meta's control plane: anything that needs a
backend decision (lease a VM, link an account, pair a messaging channel, send a push)
goes out through that socket, and the cell has no path to it either.
Inputs and outputs
The cleanest inventory of what goes in and out of the box is the list of Unix socket paths compiled into the daemon. Every one is a bind-mount from the VM side, and the cell sees only the handful it needs:
$ strings hatch | grep -oE '/run/hatch/[a-z0-9_./-]+\.sock' | sort -u
/run/hatch/daemon/http-api.sock realtime-protocol.sock rtc-stats.sock # the app, via ingress-rev-proxy
/run/hatch/proxy/inference.sock # OUT: models, through ipnext
/run/hatch/proxy/stefi.sock # OUT: Meta control plane (leases, consent, channels, push)
/run/hatch/sentinel/egress-approvals-admin.sock daemon-egress-approvals.sock http-api.sock # egress policy + HITL
/run/hatch/safety/security.sock # hatch-safety: the classifier ensemble
/run/hatch/auth/authd.sock vault-encrypt/encrypt.sock whatsapp-keyd/keyd.sock # secrets
/run/hatch/browser-broker/browser-broker.sock research-browserd.sock daemon/browser-control.sock # leased browsers
/run/hatch/rtc/realtime.sock voice-genui-decision.sock present-widget.sock # voice calls, live UI
/run/hatch/checkout-spend/checkout-spend.sock credit-watcher.sock # money: purchases and the credit meter
/run/hatch/cron-store/control.sock noded/control.sock tailscale/control.sock ssh-access.sock
/run/hatch/telemetry/telemetry.sock bugreport.sock daemon/metrics.sock # OUT: to Meta's logging (scuba)
/run/hatch/sandbox/space-{inference,media,web-search,privileged}.sock sandbox-api/api.sock space-share.sock
/run/hatch/exec/execd.sock daemon/enter-tool-environment.sock # IN to the cell: your shell
/run/hatch/privsep/<tool>.sock x60 # the tool workers
Summary
| Claude Code | Instinct | Muse | |
|---|---|---|---|
| Isolation primitive | Firecracker microVM | Firecracker microVM (E2B) | Cloud Hypervisor microVM + nspawn cell |
| Guest inside the VM | Custom Rust PID 1 | Full Ubuntu + XFCE | Ubuntu host services + Ubuntu container |
| Boot (measured) | ~6.4 s to harness | ~1.26 s to desktop | 13 s to cell, ~40 s to ready |
| Lifecycle | Reclaim when idle, wake on message | Timeout, cold boot or snapshot resume | Pre-booted hatchling, RV grafted; replaced per rollout; balloon reclaims idle memory |
| What's durable | vda block volume |
git repo in S3 | vdd, a LUKS2-encrypted 100 GB "Reliable Volume" |
| Memory model | Conversation on disk | Markdown vault, git | On-VM Postgres (194 tables) + local embedding/reranker |
| Inbound | none | backend only | Public FQDN, Noise_XX through Meta's edge |
| Egress | 443-only MITM gateway | open | MITM proxy + eBPF gate + fake-IP DNS + HITL approvals |
| Harness | On the box, Bun | Off the box | On the box, 327 MB Rust, sealed, entered from the VM side |
| Model | Claude via SSE | never from the box | Server-side routing: avocado-*, claude-*, gpt-* via genai |
It was a great time seeing how similar products can diverge in implementation strategies. Either way all of them clearly have had a lot of thoughtful engineering behind them to make a complex collection of moving parts become a very smooth user experience. I'm looking forward to see how the space evolves and how these different architectural decisions eventually converge :).
PS
If you are interested in sandboxes one other massive piece of news that I'd be remiss not to mention is the launch of the Open AI agents sdk using the e2b sandboxs. You may remember E2B from its mention in the last post where the CEO even popped by to answer questions. I'd reccomend checking out their announcement if you're curious.