Skip to content

Remote access

Bálint Horváth edited this page Oct 26, 2026 · 2 revisions

Remote access

Run the agents on a server and drive them from a laptop, a phone or another browser. Devices pair once and get a token of their own.

Run the agents where the code and the compute are, and drive them from wherever you are. The daemon on the server holds the workspaces, tabs, agents and jobs as always. Every other machine is a client that pairs once and then connects like a local one: the full-screen terminal UI, the line mode, one-off prompts, a browser, or a phone.

# on the server
malac daemon start --remote
malac remote pair                    # a one-time code, and a QR code for a phone

# on your laptop
malac remote add devbox http://devbox.local:7700 --pair ABCDE-FGHJK
malac -C devbox                      # the full-screen UI, on the server's daemon
malac -C devbox --plain              # the line mode
malac -C devbox "fix the flaky test" # a prompt

On a phone, scan the QR code: it opens the web UI at the server's address with the code filled in. The tutorial walks through a Linux server driven from a Mac.

Turning it on

Remote access is off by default. The daemon then serves only local requests: from a loopback address, naming a loopback host (127.0.0.1, localhost, [::1]), with no proxy headers. Everything else gets 403 before it reaches the API, the web page included.

web.remote = true, or --remote on malac daemon start, restart or run, lets paired devices in. The daemon then listens on every interface, unless web.host or --host names an address. --host 127.0.0.1 with --remote keeps the daemon off the network and allows only tunnels and SSH.

A restart keeps all of this. malac daemon restart, and the terminal UI's restart after an update, bring the daemon back with the same address, remote access and tunnel, so paired phones carry on. Flags on restart change them; malac daemon stop and then start apply your configuration afresh.

malac remote (or malac remote status) shows whether remote access is on, where the daemon listens and how to reach it:

$ malac remote
remote access allowed · listening on 0.0.0.0:7700 · 1 paired devices
  network        http://devbox.local:7700
  SSH tunnel     http://127.0.0.1:7700

How to connect
  same network   malac remote add <name> http://devbox.local:7700 --pair <code>
  SSH tunnel     ssh -N -L 7700:127.0.0.1:7700 <this host>, then malac remote add <name> http://127.0.0.1:7700 --pair <code>
  Cloudflare     malac tunnel cloudflare
  ngrok          malac tunnel ngrok
  Tailscale      malac tunnel tailscale, or tailscale-funnel to go public

Pairing

The daemon's master token, in ~/.malac/daemon.json, belongs to the daemon's machine: it works on local requests only, never through the network or a tunnel. Every other device pairs for a token of its own:

  1. On the daemon's machine, create a code: malac remote pair, Connection → Pair a device in the terminal UI, or Settings → Remote access → Show a pairing code in the web UI. A code looks like ABCDE-FGHJK and is valid for ten minutes, for one device. It comes with a QR code of the best address (a running tunnel, remote.public_url, the network address, then the loopback one) and a malac remote add line for each address.
  2. On the device, trade the code for a token: malac remote add <name> <url> --pair <code> on a computer, the pairing form in a browser (the QR code's link fills it in), or the pairing screen of an app.
  3. The device uses that token from then on.

Typing a code is forgiving about case, dashes, and O/0 or I/L/1. After ten wrong codes, every open code is withdrawn until a new one is made, so guessing is out of reach even on a public address.

Pairing works with remote access off too, for SSH tunnels and other local clients.

Devices

malac remote devices lists the paired devices: name, kind, when it paired, when and from where it was last seen. Connection → Remote access in the terminal UI and Settings → Remote access in the web UI show the same list.

malac remote revoke <id>, or Revoke in either UI, removes a device: its token stops working at once, and its open connections close. A revoked browser returns to the pairing form. Tokens are stored as SHA-256 hashes in ~/.malac/devices.json (mode 0600).

Saved connections

malac remote add <name> <url> --pair <code> saves the daemon's address and the device token in ~/.malac/connections.json (mode 0600):

$ malac remote add devbox http://devbox.local:7700 --pair ABCDE-FGHJK
paired as macbook (3f2a9c1e) with malac 1.1.1 at http://devbox.local:7700
saved `devbox`: malac 1.1.1 with 2 workspaces · `malac -C devbox` connects

The URL may be a whole pairing link (…/#pair=ABCDE-FGHJK), which carries the code. --token saves a token you already have, and --device-name sets what the other daemon calls this machine (its host name by default). add with an existing name and neither --pair nor --token changes only the address, for example after a quick tunnel restarted with a new one.

Command What it does
malac remote list [--json] Saved connections, and whether each answers
malac remote remove <name> Forget one, revoking its token when the daemon answers
malac -C <name>, --connect <name> Use that daemon for the full-screen UI, --plain, -p and malac <prompt>; local is this machine

On a saved connection, paths name the daemon's machine. --cwd is a folder there, and without it a new tab opens in the server's home folder; /cd ~/src/app in the terminal UI works the same way. Models, keys, MCP servers and plugins are the server's configuration, so the client needs none. Pasted images travel with the prompt; files do not yet: @ and dropped files work only with this machine's daemon, and the folder browser points to /cd <folder> instead.

In the terminal UI

The Connection menu lists this computer and every saved connection. Choosing one switches the whole interface to that daemon, in the workspace it used last. The choice is remembered: a plain malac reopens it next time, or falls back to this computer with a note when it does not answer. -C local goes back to this machine.

On another daemon, the header shows devbox › workspace, the status line starts with @devbox, and the workspace switcher (Session → Switch workspace) lists the workspaces of every daemon under its name.

Connection → Remote access shows the daemon you are on: whether remote access is allowed, its addresses, the tunnel, keep-awake, the paired devices (↑ ↓ choose, x revokes) and how to connect. p shows a pairing code with its QR code.

In a browser

Open the daemon's address. Without a token, the web UI shows a pairing form; the QR code's link fills in the code. The browser keeps its device token in local storage and stays paired until you revoke it. On the daemon's own machine, malac web still prints the local address with the master token.

Settings → Remote access in the web UI holds the switch to allow other devices, the bind address, port and public address, the tunnel to start with the daemon, pairing codes, the paired devices, keep-awake, and how to connect.

Ways to connect

Way How Notes
Same network malac daemon start --remote, then http://<host>:7700 Plain HTTP: fine at home, not on a café network
SSH tunnel ssh -N -L 7700:127.0.0.1:7700 devbox, then http://127.0.0.1:7700 Encrypted and nothing exposed. Works with remote access off, since the request looks local to the daemon.
Cloudflare Tunnel malac tunnel cloudflare A public https://….trycloudflare.com address, no account; a new address on every start
ngrok malac tunnel ngrok Needs a free account and ngrok config add-authtoken; free addresses show a warning page in browsers first
Tailscale malac tunnel tailscale, or tailscale-funnel tailscale reaches only your tailnet, over WireGuard; tailscale-funnel is public
Your own proxy A TLS reverse proxy in front, and remote.public_url set to its address The address is offered first in pairing links

Tunnels

malac tunnel <provider> asks the daemon to run the tool as its child, waits for the public address and prints it with what to know about that kind of tunnel. The daemon stops the tunnel when it stops, or on malac tunnel stop, and starts it again after a restart; malac tunnel status shows it.

Some tools wait for you before they give an address. tailscale serve on a tailnet where Serve is off prints a link to turn it on, then waits: malac shows that message and link (in the terminal, the Connection dialog and Settings → Remote access), and the tunnel comes up as soon as you are done. [remote] tunnel = "cloudflare" starts one with the daemon every time. If the daemon is not running, malac tunnel starts it for tunnels only: remote access on, listening on loopback.

Provider Runs
cloudflare cloudflared tunnel --no-autoupdate --url http://127.0.0.1:<port>
ngrok ngrok http <port>
tailscale tailscale serve http://127.0.0.1:<port>
tailscale-funnel tailscale funnel http://127.0.0.1:<port>

malac never installs these tools. When one is missing, it says how: brew install cloudflared on macOS, or the static cloudflared binary in ~/bin on Linux, and the ngrok and Tailscale installers. It looks on PATH and in ~/bin, ~/.local/bin, /opt/homebrew/bin, /usr/local/bin and the Tailscale app.

A tunnel needs remote access. What comes through one carries a public Host and proxy headers, so the daemon treats it as remote and refuses it otherwise, and only pairing and device tokens work through it.

Cloudflare WARP. If you already use Cloudflare Zero Trust, devices running the WARP client can reach the server's private address through a cloudflared tunnel of your own, with no public address at all. malac does not set this up. To the daemon it is a request from the network, so turn remote access on and pair as on the same network.

Security

  • Local or remote. A request is local only when it comes from a loopback address, names a loopback host and carries no proxy header (Forwarded, X-Forwarded-For, X-Forwarded-Host, X-Real-IP, CF-Connecting-IP). Other machines, tunnels, proxies and pages that resolve their own name to this machine (DNS rebinding) are remote, and refused outright while remote access is off.
  • Tokens. The master token works on local requests only. Device tokens work wherever remote access allows, can be revoked one by one, are stored hashed and are compared in constant time. Only GET /api/hello and POST /api/pair answer without a token.
  • Browsers. A WebSocket from a browser must come from the page's own origin, or from a VS Code webview. REST calls need the token in a header, which pages on other origins cannot send; the daemon never grants CORS.
  • Transport. The daemon speaks plain HTTP. On a network you don't trust, anyone who can watch the traffic can take a device token, so use SSH, Tailscale or a TLS proxy there. Cloudflare and ngrok terminate TLS at their edge and can see the traffic; SSH and Tailscale are end to end.
  • A device can do what you can. It runs prompts, answers approvals, changes settings, pairs and revokes devices, and starts tunnels. Pair only devices you own, and revoke the ones you lose.
  • Repositories cannot open it. A project's .malac/config.toml cannot set web, daemon or remote, so cloning a repository never opens remote access or starts a tunnel.

Keeping the server awake

While agents or jobs run, the daemon keeps the machine from sleeping, and lets it sleep a minute after the last one ends. That matters most for a laptop you leave running at home. See Keeping the machine awake.

Settings

# ~/.malac/config.toml on the server
[web]
remote = true
# host = "127.0.0.1"   # tunnels and SSH only

[remote]
tunnel = "cloudflare"                       # start a tunnel with the daemon
# public_url = "https://malac.example.com"  # an address in front of the daemon, offered in pairing links
Key Default Meaning
web.remote false Let paired devices on other machines, and through tunnels, connect
web.host 127.0.0.1, or 0.0.0.0 with web.remote Bind address
web.port 7700 Port; any free port when it is taken
remote.tunnel none cloudflare, ngrok, tailscale or tailscale-funnel, started and stopped with the daemon
remote.public_url none An address in front of the daemon, such as a TLS reverse proxy; offered first in pairing links after a running tunnel

The daemon reads these from your user configuration when it starts.

Files

Path What it is
~/.malac/devices.json Devices paired with this machine's daemon, their tokens hashed (mode 0600)
~/.malac/connections.json Saved connections to daemons on other machines, with their device tokens (mode 0600)

Clone this wiki locally