mola.

Intro

The default image boots Omarchy, the Arch Linux desktop, under your platform's hypervisor. You get root, systemd, pacman, Hyprland, and a browser. Linux x86_64 KVM hosts can also install Ubuntu 24.04 with XFCE and Chrome; see the Ubuntu image guide for the catalog and resource defaults. Install what you need. If you break the machine, delete it and create another.

The engine clones every machine from one base image using copy-on-write. A new machine therefore costs about a second and a few megabytes rather than a 16 GB copy. Machines share nothing after creation.

The engine listens on loopback only. You do not create an account, and no hosted service sits in the middle. Your data stays on your computer unless you send it elsewhere.

Installation

Two commands on macOS. One on Linux, if you already installed QEMU.

Use Node.js 22.15 or later in the 22.x series, or Node.js 24 or later, plus Python 3.10+ and an SSH client. Linux requires x86_64 with usable KVM; macOS requires Apple Silicon.

Install QEMU

brew install qemu        # macOS, Apple Silicon
sudo pacman -S qemu-full # Linux, and check /dev/kvm exists

Any QEMU build works. QEMU applies the macOS hypervisor entitlement to itself when it builds, so you need no Developer ID and no separate runtime.

Start the engine

npx mola-core

The first run downloads a 1.1 GB guest image and verifies its checksums as the bytes arrive. Expect a few minutes. The engine then prints an address and a token, and stays in the foreground until you stop it.

To check your machine first, run npx mola-core doctor. It tests each requirement and names whatever is missing.

Create a machine

export API=http://127.0.0.1:4141/v1
export TOKEN=$(cat ~/.mola/token)

curl -s -X POST $API/machines \
  -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name": "scratch"}'

A machine gets 4 vCPU, 4 GB of memory, and a 40 GB disk by default. Set vcpus, memory_mb, or disk_gb to change that. The response returns booting. The machine reaches ready about seven seconds later.

Keeping the guest image current

The guest image is downloaded once and then reused, so a machine you create next month is built from the image you fetched today. When a newer one is published the engine says so at startup and leaves it at that: the image is about 1.5 GB, and starting the engine is not consent to fetch it.

npx mola-core start --refresh-image fetches the new one. It downloads to a staging directory and swaps it in only once the checksum matches, so an interrupted refresh leaves the image you already had. Machines that already exist keep their own disks and are not affected either way.

Taking it off again

npx mola-core uninstall deletes every machine and its disk, the guest image, the keys and the token, and tells you how much it freed. It shows you the total and asks first. --keep-image clears the machines but keeps the image, so the next one does not re-download it.

It removes only what Mola created. A virtual machine it did not start is left running.

Where the engine stores things

The engine writes its token, keys, guest image, and machine disks to ~/.mola. Set MOLA_HOME to move them. That directory sits outside the installed package on purpose: npx clears its cache without warning, and a machine's disk must outlive the tool that created it.

Hosted storage and recovery

Mola Core 1.9.2 gives an authenticated control plane durable storage receipts. Snapshot, restore, import, seal, delete and fence operations keep their operation ID and generation across lost responses and restarts. Long operations return HTTP 202 while pending; clients reconcile the same receipt before sending another mutation.

Restore verifies checksums and prepares a temporary disk before atomic replacement. Pending work keeps the disk fenced. A timeout does not prove the guest is stopped or make its capacity available.

Stopped resize preserves the persistent disk and grows its ext4 filesystem. Restoring an older, smaller snapshot retains the computer's larger allocation: Mola verifies and grows unpublished restore staging before replacing the disk. Disks cannot shrink.

Running-disk checkpoints require a qualified image and explicit host and control-plane enablement. They quiesce disk writes and retain durable operation receipts; they do not capture process memory. Check capabilities before requesting a checkpoint.

These are private host APIs for operators. See the host storage contract and guest-agent update requirements. Live native Ubuntu restore and disk-growth tests cover 20 GB, 40 GB and 80 GB disks, including an older snapshot restored onto a larger allocation. Off-host archive and broader image qualification have separate acceptance gates.

For hosted computers, customer metadata, fixed expiry, restricted agent credentials and browser-approved MCP connections, use the Mola Cloud docs.

Build your own guest image

Most people use the downloaded image. Build your own to change the package set, or to avoid the download.

git clone https://github.com/obaid/mola-core
cd mola-core && npm install
python3 bin/native-prepare --output ~/.mola

The script writes ~/.mola/image, where the engine looks for it. If you pass a different --output, set MOLA_HOME to match. The script never overwrites an existing image, so move the old directory aside to rebuild.

Control

Send one action per request to POST /v1/machines/{id}/actions. The engine accepts nine. High-frequency clients can keep a persistent action session open instead.

VerbBodyWhat you get
exec{"command": "ls ~"}exit code, stdout, stderr
read_file{"path": "~/notes.md"}base64 bytes and size
write_file{"path": "~/a", "content": "hi\n"}bytes written
screenshot{}a PNG, base64
click{"x": 640, "y": 400}ok
move{"x": 640, "y": 400}ok
scroll{"direction": "down"}ok
type{"text": "hello"}ok
key{"key": "super-return"}ok

Open a terminal and run a command

curl -X POST $API/machines/$ID/actions -H "Authorization: Bearer $TOKEN" \
  -d '{"action": "key", "key": "super-return"}'

curl -X POST $API/machines/$ID/actions -H "Authorization: Bearer $TOKEN" \
  -d '{"action": "type", "text": "fastfetch"}'

curl -X POST $API/machines/$ID/actions -H "Authorization: Bearer $TOKEN" \
  -d '{"action": "key", "key": "enter"}'

Those keys are Omarchy's own bindings. super-return opens a terminal, super-space opens the launcher, and super-k lists every binding.

Watch the screen

curl -X POST $API/machines/$ID/desktop -H "Authorization: Bearer $TOKEN"

{"data": {"desktop_url": "http://127.0.0.1:4141/desktop#t=...", "expires_in": 60}}

Open that URL in a browser to see the live desktop. The mouse and keyboard work. The ticket travels in the URL fragment, which browsers never send to a server, so it stays out of access logs and referrer headers. Each ticket works once and expires after 60 seconds.

Machine states

A machine reports one of four states. Two of them need explanation.

StatusMeans
bootingQEMU is running, the guest has not reported in for this boot
readythe guest reported in and its shell works
stoppedoff, disk intact
unknownthe runtime cannot see it

ready does not mean "the process started". QEMU can run while the guest still starts its network or its compositor. The engine waits for a heartbeat from the current boot before it reports ready. Your first command therefore reaches a machine that can answer it.

unknown reports that the runtime cannot see the machine. The engine returns it instead of guessing. A runtime that cannot see a machine has not reported that the machine stopped, and a platform that treats those two answers as the same loses track of machines it still runs.

The engine does not trust the guest

Whoever uses a machine has root inside it. The engine therefore treats everything a guest reports as a claim about itself rather than a fact about the platform. It records the capabilities a guest reports, then decides readiness on its own.

A failed delete keeps the record

A delete that cannot tear a machine down returns 502 and keeps the machine registered, with the reason attached, so you can retry. Dropping the record instead would leave a virtual machine consuming memory that you could no longer name or stop.

Fast data plane

Drive a ready computer through one ordered WebSocket connection instead of making an HTTP request for every action.

curl -s -X POST $API/machines/$ID/session \
  -H "Authorization: Bearer $TOKEN"

{"data":{"session_url":"ws://127.0.0.1:4141/actions/socket?t=...",
         "expires_in":60}}

Connect to session_url within 60 seconds. The ticket works once and the connection is bound to this computer and this boot. Stop, restart, restore, or delete the computer and the socket closes immediately. Obtain a new ticket after every boot.

Every request has a unique ID. Mola executes requests in arrival order and returns the same ID with the result:

→ {"id":"1","op":"action","action":{"action":"exec","command":"uname -a"}}
← {"id":"1","ok":true,"result":{"exit_code":0,"stdout":"Linux ..."}}

Up to 16 actions may be queued. The connection has a 60-second idle timeout and a 30-minute maximum lifetime. Send {"id":"heartbeat-1","op":"ping"} while an otherwise idle session is still needed. Never replay a side effect whose outcome was reported as unknown.

Compatibility stays simple

The REST action endpoint and all MCP tools remain supported. Use a session when latency or action volume matters; use REST or MCP when one call at a time is clearer.

Binary files and screen frames

Set "binary":true on read_file or screenshot. Mola sends JSON metadata containing the exact byte length, MIME type, and SHA-256 digest, followed by one binary frame. File reads and incoming binary writes are limited to 1 MiB. Screenshots may be larger, so allocate from the reported length.

{"id":"screen-1","op":"screen.subscribe","max_fps":5}

A screen subscription sends one full frame and then only changed frames, up to 10 FPS. Slow clients receive coalesced updates instead of an unbounded queue. Use screen.resync for a fresh full frame and screen.unsubscribe when finished. Frames are transport only: Mola does not inspect or interpret the screen.

Machine-scoped port tunnels

curl -s -X POST $API/machines/$ID/tunnels \
  -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"port":9222}'

The response contains a single-use WebSocket URL that carries binary data to that port on the selected computer's loopback interface. This lets an external tool reach a native protocol such as Chromium CDP without exposing the port publicly. The caller chooses a permitted port, never a host; privileged and Mola-managed ports are denied.

Tickets expire after 60 seconds and tunnels close on lifecycle changes, after 60 seconds idle, after 30 minutes, or at 32 MiB transferred. A computer may have four tunnels open. See the complete protocol contract and runnable examples.

MCP

The engine ships an MCP server, so Claude Code, Claude Desktop, Cursor and anything else that speaks MCP can drive a machine. You write config, not code.

claude mcp add mola -- npx -y mola-core mcp

Or, for a client that wants JSON:

{
  "mcpServers": {
    "mola": {
      "command": "npx",
      "args": ["-y", "mola-core", "mcp"]
    }
  }
}

Then ask for something that needs a computer.

claude
> make me a linux machine, install neovim, and show me the desktop

  create_machine   ready in 7s
  start_task       pacman -S neovim
  check_task       downloading... installed
  open_desktop     http://127.0.0.1:4141/desktop#t=...

  Done. neovim 0.11.4 is installed. The link opens the live
  desktop, and it expires in a minute.

Seventeen tools: machines, the shell, files and the screen. screenshot hands the agent a picture of the desktop, which is what lets a model that has never heard of Mola look at a window and click the right thing.

Two things the tools get right for you

run_command stops at 120 seconds, so installs and builds go through start_task, which detaches the process and returns a handle to poll. That is also how the agent reports progress instead of going quiet for four minutes.

The engine has to be running. If it is not, the tools say so and name the command, rather than failing in a way that looks like a broken server.

Full MCP setup, including Claude Desktop, Cursor, and what to do when something goes wrong.

Cua Driver in Omarchy

Cua-enabled Omarchy images include the driver and an agent skill inside each computer. An agent running in the guest can inspect the desktop, windows, accessibility tree, and browser through the local driver. Check the image you are using from a guest terminal:

cua-driver --version
cua-driver status
ls ~/.agents/skills/cua-driver

Availability follows the guest image. Updating the mola-core npm package alone does not replace an existing self-hosted computer's disk. Mola Cloud installs Cua on older retained Omarchy disks when they next become ready and exposes authenticated sessions through its Cua API and agent tools.

The current image leaves the experimental Hyprland input plugin disabled. Desktop-scoped input works when the tool arguments include scope: "desktop"; window-scoped input may return isError. Inspect each result and take a fresh desktop state after input. Read the full Cua guide for image and skill details.

Hosted browser & application tools

The Linux host API adds managed browser automation, typed installers, session manifests, display geometry and scoped capture for Mola Cloud. Cloud-managed computers use the private host token and current computer generation under /internal/v1/machines/{id}. Locally managed computers expose the equivalent typed routes under /v1/machines/{id} with operator authentication. Keep the host listener on loopback behind the authenticated control plane.

Managed Chrome supports tab inspection, snapshots with expiring element references, navigation, fill, click, keyboard, evaluation and screenshots. Query browser capabilities before selecting a mode. Preparing the browser requires stopping unmanaged Chrome first; profile migration happens offline and preserves its source copy.

Typed installers support apt, pacman, checksum-verified deb and portable artifacts, Flathub packages and bounded scripts. Supply a stable operation identity before submission and poll its durable receipt. A queued receipt is not success. An uncertain worker outcome does not replay automatically.

Private durable exec jobs support 1–900 second deadlines through POST /internal/v1/machines/{id}/job-exec. Commands, JSON payloads and redaction values travel through anonymous memory and stdin. Poll /job-exec/status using the original operation, generation and boot identity. /job-exec/cancel requests cancellation of that scope; only confirmed termination releases its reservation. Lost replies never cause command replay. Legacy synchronous actions and pool exec setup retain their 120-second limit.

Capture chooses a desktop, exact window or browser tab and crops source pixels before returning them. Unsupported targets fail closed. Display geometry is confirmed against the guest display before it is saved. A changed runtime generation rejects stale capture and input.

Cloud vault injection uses a private guest channel and returns sanitized success only. It checks the saved exact HTTPS login URL or expected focused app. Vault storage, expiring viewer grants, schedules, pools, payment accounting and commercial provider credentials belong to the cloud control plane.

Read the guest-tools contract for operation fields and image capabilities, and the cloud browser guide, vault guide, live-view guide and jobs guide for customer APIs. Paid providers expose deployment availability and require separate qualification.

API reference

Send Authorization: Bearer <token> with every request under /v1. GET /v1 returns the endpoint list, so an agent can discover the API itself.

EndpointDoes
POST /v1/machinesmake one and start it
GET /v1/machineslist them
GET /v1/machines/{id}describe one
POST /v1/machines/{id}/startstart a stopped one
POST /v1/machines/{id}/stopshut down, {"force": true} cuts power
POST /v1/machines/{id}/actionsdo something inside it
POST /v1/machines/{id}/sessionone ordered WebSocket for many actions
POST /v1/machines/{id}/tunnelsreach one allowed guest loopback port
POST /v1/machines/{id}/desktopa browser URL for the screen
DELETE /v1/machines/{id}destroy it and its disk

Environment

VariableDefaultDoes
MOLA_HOME~/.molatoken, keys, images, machine disks
MOLA_PORT4141listen port
MOLA_QEMUfound on PATHuse a particular QEMU
MOLA_IMAGE_URLGitHub releasewhere the guest image comes from
MOLA_MAX_RUNNING2how many may run at once
MOLA_MAX_MEMORY_MB8192memory they may reserve between them

Troubleshooting

doctor reports that the host is not ready

Read the line it names. On macOS, run brew install qemu. On Linux, check that /dev/kvm exists and that your user can open it. Inside a cloud VM, your provider must expose nested virtualisation, and many do not.

A machine never reaches ready

tail -40 ~/.mola/runtime/machines/<id>/console.log

HVF does not support GICv2 emulation means your QEMU needs a different interrupt controller. Set MOLA_GIC=3 and create the machine again. QEMU writes its own errors to runtime.log, beside the console log.

Screenshots return scrambled frames

The capture reached the compositor mid-draw. Send an input event first, such as {"action": "key", "key": "super"}, then capture again.

The desktop URL stays on "connecting"

Request a new ticket. Each one works once and lasts 60 seconds. Note that changing only the #fragment of a URL does not reload the page, so pasting a fresh ticket over a spent one in the address bar does nothing. Open the URL in a new tab.

Creating or starting a machine hits a limit

The engine allows 2 running machines and 8192 MB between them. Raise either limit with MOLA_MAX_RUNNING or MOLA_MAX_MEMORY_MB, or stop a machine you are not using. Stopped machines consume disk only and count against neither limit.

A machine appears that you did not create

At startup the engine adopts any machine the runtime holds but the registry does not list, and names it adopted-<id>. A previous engine died before it wrote the record. The machine runs normally, so delete it the usual way.