Remote Access & Clients
A machine (a workspace, in the settings editor) lives on one Mac, but the person who wants to drive it is not always sitting at that Mac. Bromure Agentic Coding can be reached remotely, and every remote path rides a single, optional, disabled-by-default SSH front door. Turn it on and you get three ways in: a person can ssh in from any terminal and land in a menu; you can manage the front door itself from the command line; and a rich client — another Mac running the app, or Bromure Remote on iPhone, iPad, or Apple Vision Pro — can connect and mirror the whole instance — its sessions, rooms and Switchboard, machines, terminals and boards — in a live window.
The rich client reaches a server two ways. On the same network you add it directly by address. From anywhere else you sign in to a bromure.io account and pick the server from a directory: the two devices are introduced by bromure.io and then talk peer-to-peer, with no port-forwarding, no VPN, and no address to remember — and once you're signed in, your own devices authorize each other so the connection is passwordless with no manual key setup.
This chapter covers enabling remote access on the host, each of the entry points, how the rich client mirrors a remote (by address and via bromure.io), editing a remote's Preferences, the iPhone, iPad and Vision Pro clients, reaching services inside remote VMs, exposing a dev server to the internet, and the security model that keeps all of it safe. Fleet-scale enrollment and cloud telemetry are a different system entirely — see Enterprise. The complete command reference lives in CLI, API & MCP.
The three ways in
Everything below depends on the SSH front door being enabled (it is off by default). Once it is running, the same server serves all three audiences:
| Way in | Who uses it | What you get |
|---|---|---|
| Plain SSH | A person at any terminal (ssh -p 2222 you@mac) | The remote menu — a curses-style TUI. A raw shell is never reachable. |
The bromure-cli remote CLI | You, on the host itself | Manage the front door: enable/disable, keys, status. See Enabling remote access on the host. |
| The rich client | Another Mac running the app, or Bromure Remote on iOS / iPadOS / visionOS | A live mirror of the remote instance, with bidirectional edits — reached by address on a LAN or through a bromure.io account from anywhere. |
The server is embedded — it is Apple's swift-nio-ssh library built into the app, not macOS's own sshd. That distinction matters: because the app runs its own server, it can verify your macOS account password through OpenDirectory without you ever enabling Remote Login in System Settings and without administrator rights. It listens only for the exact macOS user who runs the app, and it forces every interactive login into the remote menu. The only machine-facing exceptions are four whitelisted commands the rich client uses (bromure-fatclient/1 control, forward <ip> <port>, forward-udp <ip>, and browser-mcp <vm>); anything else a human types behaves like a login and drops into the menu.
Note: The Remote Access window describes the server as "an app-owned instance of the system SSH server." It is more precisely an embedded swift-nio-ssh server, deliberately not macOS's
sshd— that is exactly what lets it work without Remote Login or admin rights.
Enabling remote access on the host
Remote access is off until you turn it on, and it can be managed two equivalent ways: the Remote Access window or the bromure-cli remote CLI. Both write to the same place, so a change made in one is reflected in the other and applies live.
The Remote Access window
Open Bromure Agentic Coding (the app-name menu) → Remote Access…. The window title is Remote Access. A green or gray dot reads sshd running or sshd stopped; when the server is up, a copyable connect string of the form ssh -p <port> <user>@<host> appears under the header.
| Section | Field | Purpose | Default |
|---|---|---|---|
| Header | Enable remote access over SSH | Starts and stops the server. | Off |
| Listener | Bind IP | Which interface to listen on. 0.0.0.0 = every interface; 127.0.0.1 = this Mac only. | 0.0.0.0 |
| Listener | Port | Listener TCP port; must be ≥ 1024 (a non-root process cannot bind privileged ports). | 2222 |
| Authentication | macOS account password | Accept the macOS login password of the user running the app (verified via OpenDirectory). | On |
| Authentication | Public key | Accept keys from the authorized-keys list below. | On |
| Server host key | fingerprint | The server's own ed25519 host-key fingerprint, for out-of-band verification. Reads (generated on first enable) until first use. | — |
| Authorized public keys | list | Enrolled ssh-ed25519 / ecdsa public keys. Add by pasting a key line; remove with the trash icon. Reads No keys enrolled. when empty. | empty |
Edit Bind IP or Port and press Return to apply the change live. The Authentication section notes Verified against <user> — the user running bromure-ac. so it is clear whose password unlocks the door.
Warning: At least one authentication method must stay enabled — you cannot switch off both password and public-key auth. Pasting an invalid key is rejected with Not a valid public key. Adding or removing an authorized key restarts the listener, which drops any live connections.
The bromure-cli remote CLI
The CLI talks to the running app over its owner-only control socket, so the app remains the single source of truth. It is the fastest way to enable access on a headless Mac you reached over an existing shell.
| Command | What it does |
|---|---|
bromure-cli remote (or remote status) | Prints enabled/running state, bind and port, auth methods, host-key fingerprint, login user, a ready-made connect string, and the numbered authorized-key list. |
bromure-cli remote enable [--port N] [--bind IP] [--password|--no-password] [--pubkey|--no-pubkey] | Starts the server (auto-launching the app agent if needed). Defaults: port 2222, bind 0.0.0.0, both auth methods on. |
bromure-cli remote disable | Stops the server and persists the off state. |
bromure-cli remote key add <path-or-key-text> | Enrolls a public key from a file path or a pasted key line; prints its fingerprint. |
bromure-cli remote key ls | Lists authorized keys with a 1-based index, fingerprint, and comment. |
bromure-cli remote key rm <index|fingerprint> | Removes a key by index or by a fingerprint substring. |
remote enable refuses if you pass both --no-password and --no-pubkey, and it errors while a base image is missing or installing. When the app is not running, status, disable, and key ls print No bromure-ac agent running.
Note: The server will not start while the base image is absent or still installing — enabling then returns an actionable error rather than a silent no-op. This is true both at launch and when you flip the toggle. Install or finish installing the base image first.
Host keys, fingerprints, and trust
On the first enable, the server generates an ed25519 host key. Its fingerprint — shown in the Server host key row and by bromure-cli remote status, in the form 256 SHA256:… bromure-ac-remote (ED25519) — is what a connecting client verifies to be sure it reached the right Mac. Give this fingerprint to whoever connects (or check it yourself in the rich client's first-contact sheet) so a man-in-the-middle cannot impersonate the host. See trust on first use for how the rich client pins it.
Rate limiting
Password attempts are throttled per source IP by a token bucket: five attempts at full speed, then a refill of about one attempt every two seconds, with the imposed delay capped at eight seconds. Attempts are delayed, never refused, and nobody is ever locked out; one noisy IP cannot slow another. Public-key authentication is never throttled.
Signing in over plain SSH
Any terminal can reach the host with ssh -p 2222 <user>@<mac>. Instead of a shell, the connection lands in the remote menu — a curses-style TUI (internally bromure-ac __remote-menu) forced onto every human login. A raw shell is never reachable through it.
The menu predates the sessions-first redesign and is organized around machines: a Workspaces list with live state glyphs (● running, ◌ booting, ⏸ suspended, ○ off), plus Models (the installed models and the on-device catalog) and Trace (summaries, host names, leaks) sections. From it you can:
- Create a workspace with a single-screen form, with a Full settings… door into per-pane settings forms.
- Start, suspend, kill, reboot, or delete any workspace.
- Edit settings. Secrets show as
•••• (set); leaving a secret blank keeps the stored value. - Manage worktrees — create, merge, attach a terminal, or discard.
- Generate SSH keys, toggle Fusion and model routing, and browse trace summaries.
- Attach to any tmux tab or a running Docker container.
Note: The menu's settings forms can't edit the Models board. Their Models form is read-only: it lists the server's providers (API key or subscription) and says whether the workspace overrides the global settings. Choose providers and models from a Mac, in Preferences or in a mirror's remote Preferences.
Navigate with the arrow keys, Enter to select, and q or Esc to go back; Exit (disconnect) ends the session. Selecting Attach hands the terminal to the guest's tmux; Ctrl-b d detaches back to the menu. While you are attached, an overlay keychord (default Ctrl-]) pops a host-side controller — switch tabs, open a new tab, manage worktrees, toggle Fusion or routing, edit settings, reboot, suspend, or disconnect — without the keystroke ever reaching the guest. Attaching to a container prompts for the shell (Enter = sh).
Note: The menu needs a real terminal. Connecting without a TTY prints a hint to reconnect with
ssh -t. Starting a cold workspace waits up to about 20 seconds for the guest's tmux session before attaching.
The overlay chord is configurable on the host Mac (the machine being connected to) via the file ~/Library/Application Support/BromureAC/remote/overlay-key — accepted forms are C-], ^], 0x1d, or 29 — or the BROMURE_OVERLAY_KEY environment variable.
Mirroring a remote with the rich client
The rich client turns this Mac's copy of Bromure Agentic Coding — or Bromure Remote on iPhone, iPad or Apple Vision Pro — into a live mirror of a remote one. On a Mac you get a full window titled Remote — <name> that looks and works like the local main window: the sessions home, rooms and the Switchboard, the machines, the task and automation boards. Everything you do runs on the remote and echoes back.
Connecting
Open Workspaces › Connect to Remote Bromure… (⇧⌘K). The window sums up the choice: "Mirror another Mac running Bromure — its grid, workspaces, tabs and automations. Sign in to reach your servers from anywhere, or add one directly by address on your LAN." Both paths land in the same mirror window; they differ only in how the two devices find each other and authorize.
Note: The SSH handshake adapts to the link: connect and handshake budgets scale with the measured round-trip time, so a high-latency link (hotel Wi-Fi, cellular, a continent away) connects slowly instead of timing out.
By address (same network)
Use this on a LAN where you know the Mac's address and don't need a bromure.io account.
- On the remote Mac, enable remote access (the Remote Access window or
bromure-cli remote enable). - On this Mac, click +, enter a Name (for example,
rack mini), Address, Port (default2222), and Remote user, then click Connect. Saved rows showuser@address:portand the last-connected time. - On first contact, a sheet shows the remote's SSH host-key fingerprint. Verify it against the remote's Remote Access window, then click Trust & Continue. The pin is remembered for this endpoint; later connections are silent unless the key changes, in which case the button turns red and reads Trust New Key & Continue — a possible man-in-the-middle.
- The client tries public-key auth with this device's dedicated key. If it isn't authorized yet, it falls back to the remote user's macOS login password (button: Sign In); a successful password login enrolls this device's key on the remote over the tunnel, so every later connection is passwordless. To pre-authorize without ever typing a password, click Show this Mac's key and run
bromure-cli remote key add '<this Mac's key>'on the remote.
After the first connect, double-click a saved server for one-click reconnects; each row's context menu offers Connect, Edit…, and Remove. Bromure Remote on iPhone and iPad offers the same flow under Add a server by address.
Through a bromure.io account (from anywhere)
Signing in to bromure.io lets a client reach your servers across networks — a coffee-shop Wi-Fi to a Mac at home — with no port-forwarding, no VPN, and no address to remember. bromure.io only introduces the two devices and relays bytes when a direct path is impossible; the SSH session itself stays end-to-end encrypted between them.
- Sign in. On a Mac, choose the app-name menu's Log in to bromure.io… and sign in through your browser (or expand the code section and enter a six-word enrollment code plus a Device Name). In Bromure Remote, sign in from its start screen. Every device you enroll shares the same account.
- On the remote Mac, enable Remote Access. A server appears in the directory only while its front door is on and it is heartbeating bromure.io.
- On the client, open Connect to Remote Bromure…. Your servers are listed under My servers · bromure.io with a live online dot and last-seen time. Double-click one (or select it and click Connect). The status reads "Connecting to
<name>via bromure.io…" while a peer-to-peer path is established — direct, router-port-mapped, or relayed — then the mirror opens.
There is no address to type, no host-key sheet, and no password in this path: your own devices trust each other through your account (below), and the host key is pinned automatically.
Passwordless across your own devices
Once you are signed in, every device you enroll shares its SSH identity through your account, so any of your devices reaches any of your servers with no password and no manual bromure-cli remote key add:
- Each device publishes its SSH public key to bromure.io on launch. Each of your servers (Remote Access on) pulls your devices' keys and installs them into its
authorized_keyswithin seconds, so a device you enrolled a moment ago is authorized almost immediately. - A server also publishes the macOS login it runs as, so the client dials it as the correct user automatically (the very field the by-address path asks you to fill in).
- Keys are scoped to your own devices — never another user's, even inside the same organization.
- Signing out on a device retires it: its key is dropped from your servers and it disappears from the directory, so signing back in never leaves a stale duplicate. A device unused for 90 days also stops being authorized until it next checks in. And from the bromure.io web console you can log out any of your devices on demand.
Note: The rich client generates its own dedicated ed25519 keypair (comment
bromure-ac-fatclient) the first time you use it — this is the key published to your account. For a by-address connect the host-key pin belongs to the endpoint (address:port), not the user, so editing a saved server's address or port drops the pin and re-triggers the fingerprint sheet; a bromure.io connect pins the peer's key through the account instead.
The mirror window
The mirror window opens automatically after a successful connect. It is the sessions home of the remote Mac: the same sidebar (sessions, rooms, the Switchboard, tasks, automations, Virtual Machines with any Kubernetes clusters and registries), the same stage and the same header. Start a session with New session (⌘N), chat with it, answer its questions, open a room with its grid of chats, or talk to the remote's Switchboard — it all happens on the remote, and the ⌘K palette works too.
The header has the same controls as the local window: the Linux pill for a session's terminal, the machine menu named after the machine (Settings…, Machine Details, Copy IP Address, Inspect Trace, Pop Out to Its Own Window, Reboot), the Fusion bolt, Show or hide the agentic browser (⌃⌘B) and Show or hide the Files pane (⌃⌘E). A mirror window adds one control of its own, the network-tunnel toggle (see The system-wide network tunnel). Terminals render with the machine's own appearance.
A few things behave specially over the mirror:
- Settings… opens the same settings editor as local, backed by a secrets-blanked read and a secret-preserving save (see Secrets stay on the host). Its Models pane edits that remote machine's override of the remote Mac's model settings. Generate new SSH key works and shows you the new public key.
- The Files pane (⌃⌘E) browses the machine's files over the tunnel. It replaces the separate file-browser window of earlier releases. See Machines.
- Reboot works as on the server's own window: a clean reboot after Reboot “…”?, or a forced one when you hold ⌥ while choosing it (see Rebooting a machine); the verb is then sent to the server.
- Pop Out to Its Own Window opens an extra live terminal window that closes itself if the machine stops.
If the remote runs an older Bromure without the sessions API, the mirror falls back to the older machine-first layout.
Menus act on the mirror in front
When a mirror window is the key window, these menu items act on the remote Mac instead of this one:
| Item | Effect on the mirrored Mac |
|---|---|
| New Session… (⌘N) | Opens the mirror's new-session screen. |
| Workspaces › New Room… | Creates the room on the remote. |
| Next Room (⌃⌘R) | Cycles through the remote's rooms (archived rooms are skipped). |
| Workspaces › Infrastructure › Create Kubernetes Cluster… / Create Registry… | Opens the creation sheet in the mirror, so the cluster or registry is built on the remote. |
| Preferences… (⌘,) | Opens Preferences targeting the remote (below). |
Remote Preferences
Press ⌘, with a mirror window in front and Preferences opens on that Mac's defaults, titled Preferences — <host>. Whenever a mirror window is open, a Settings for: picker at the top of Preferences switches between This Mac and each connected remote; each target gets its own editor, so drafts never cross machines. While the remote's settings load the window reads "Loading preferences from <host>…"; if that fails it shows Couldn't load the remote preferences with Retry.
Everything is round-tripped through the remote: secrets arrive blanked and are kept unless you type a new one. The Models pane edits the remote's global model settings — API keys are shown redacted — and changes are saved when you click Save, unlike local Preferences, where the Models board saves as you edit (see Models & Providers). This Mac's own app-wide settings — the Automation pane and the Workspace subnet and Remote transport sections of Resources — are not shown for a remote, nor in a remote machine's editor.
Signing in to providers remotely
Subscription sign-ins work end to end from the client. Sign in with provider runs the capture on the remote, but the provider's sign-in page (or device-code URL) opens in your browser; for a provider that never shows a URL, the client shows the sign-in VM's terminal so you can finish interactively. A sign-in started from a remote machine's settings asks the scope question — Every workspace or Just this workspace — on your screen; if nobody answers, it stays with Just this workspace. A sign-in started from remote Preferences is always shared by every machine on the remote. Log out… works remotely too.
Branches and review remotely
New Branch…, Review Changes and Merge into branch work from the mirror for a session with a folder of its own, exactly as on the remote's own screen (see Branches & Review). In Bromure Remote, a session's menu offers New worktree….
Asking a session on the mirrored Mac
While this Mac mirrors a remote, a session here can ask a session on the remote by its @nickname: remote sessions appear in the composer's @ list and to the agents with their host's name, and files attached to a request travel through the tunnel into the peer's inbox. Answers, steering and closing all route back the same way. The reverse direction — the remote's agents asking a session on this Mac — is not available, because the remote has no channel back to the client.
What mirrors
The mirror shows every machine with its live run state, sessions and rooms, tabs and branches, the Grid, automations, the coding-task board, machine details and vitals, the Docker dashboard, Kubernetes clusters and registries, live interactive terminals, and the remote's security events, which appear in the Security Timeline next to this Mac's — its machine filter offers All machines, This Mac and each mirrored host. On-device models and their downloads run on the remote, never on the client. Edits converge on every connected client: starting, stopping and rebooting machines, session and room operations, tab operations, Grid edits (whole layout, last writer wins), automation edits, the Fusion toggle, Docker actions, settings saves and file transfers.
The mirror follows the remote through a push channel (GET /state/subscribe) over an SSH-tunnelled bridge to the remote's control socket; while the push channel is up the client only re-reads the full state every 5 seconds as a heartbeat, and falls back to reading it every 0.75 seconds if the channel drops. Short control calls multiplex over one SSH connection per host; interactive terminal attaches use their own dedicated SSH connections, because a multiplexed channel would buffer the immediate output a terminal needs. Over a bromure.io connection the same bridge and terminal streams ride the peer-to-peer path, and the mirror behaves identically.
Chats are sparing with the link too. The first time you open a remote session's chat, it loads only the latest ~1.5 MB of the conversation; scrolling up to Load earlier conversation fetches older parts. A chat you open again — clicking back to a session, or a room shown again — starts from the history already downloaded and fetches only the new messages.
A few operations are not available from the rich client:
- Grid-layout edits need a GUI window open on the remote. A fully headless remote (app running, no main window) mirrors everything else but cannot accept a layout change.
- Importing an SSH key from a file is hidden in remote settings editors.
Bromure Remote on iPhone, iPad and Apple Vision Pro
Bromure Remote is a client-only mirror, reached exactly the same way (sign in and pick a server, or add one by address). It is sessions-first like the Mac:
- iPhone — a Sessions list with a count of sessions that need you, a new-session screen ("What can I help you with?"), and a screen per session: its chat, a Linux button for its terminal, and a menu with Resume, Rename…, New worktree…, the machine's settings, Archive (or End & Archive), End session and Delete session. A room opens as a tab per session, swiping between them, with the room's Switchboard.
- iPad — a persistent sidebar with sessions, rooms and machines; a room shows its grid of chats like the Mac.
On both, the list is ordered as on the Mac's sidebar: New session first, Coding Tasks and Automations right under it, then the sessions. Machines are split into Virtual Machines (with the Grid) and Native Machines — the Sidecar Macs the remote has attached, shown only when there is one; a native machine offers New Terminal. On iPad, New Session stays visible above the foldable Sessions section.
When a remote refuses to start a session (on a Sidecar Mac without that agent, for example), the client shows the remote's reason instead of doing nothing.
- Apple Vision Pro — the iPad layout, plus spatial windows: pin any terminal as its own window anywhere in the room, open the Grid as a dedicated stage window, or turn the whole grid into floating windows with Pin All as Windows.
In a room, one composer at the bottom has a To picker — the room's Switchboard or any session in it — and an attach menu (Photo Library, Choose Files, Paste Image); attachments are uploaded into the target's machine when you send. On iPhone and iPad, a session that needs your input can reach you as a push notification while the app is in the background; tapping it opens that session. The Models board isn't available on these devices; change models from a Mac.
Pending decision prompts
Some actions raise a decision on the remote — a home-storage upgrade, a base-image drift reset, a compromised-VM wipe, a close-action question, a provider sign-in's scope question, or a credential-exfiltration alert (the compromise detector pausing a VM that tried to leak a credential). These appear as alerts in your mirror window, and your answer rides back over the tunnel. If a prompt goes unanswered it times out to the safe choice. A pending prompt never blocks the remote's control plane: the rest of the mirror stays live while the alert is up. The consent prompts a running agent can trigger (credential use, supply-chain pass-through, guardrail writes, prompt injection) are handled by a separate, guest-unforgeable path documented under Remote consent handling.
Secrets stay on the host
Secrets never cross the link. When the rich client reads a remote machine's settings or the remote's Preferences, every secret field arrives blanked (model-provider API keys arrive as a redacted stand-in). On save, a blank or redacted field means "keep the stored value" and a typed one replaces it. API keys, tokens, subscription sign-ins and MCP environment secrets stay on the remote Mac. The rich client authenticates by SSH key alone once paired — there are no password prompts during mirroring.
Reaching services on the remote's VMs
A remote workspace's dev server lives at a private address like http://192.168.64.5:3000 on the remote's network. Bromure gives you two tiers of access to it — one automatic and browser-scoped, one opt-in and system-wide.
The browser pane (automatic, PAC-scoped)
The mirror window's collapsible browser pane (the globe button in the header, or ⌃⌘B) is a local Chromium VM whose page traffic to the remote workspace subnet is tunnelled over SSH. A generated PAC (proxy auto-config) script routes only the remote subnet through a per-host SOCKS forwarder and everything else DIRECT, and destinations are resolved on the remote side — so navigating to a remote dev server's literal http://192.168.64.x:port just works. The remote agent's own browser-control MCP stream is relayed to this pane, so the remote coding agent drives the browser you are looking at.
Select a session or machine on a running remote machine and press ⌃⌘B. The pane does nothing until the first state update has delivered the remote's subnet and the SOCKS forwarder is up (a log line notes "tunnel not ready" until then). Its local browser VM is pinned to subnet 192.168.127.0/24 (gateway 192.168.127.1) so it can never collide with a workspace subnet. Persistent browser profiles live on this Mac, keyed by the remote machine's ID.
Note: The browser pane requires this Mac to be Apple Silicon with the browser base image installed locally. The SOCKS forwarder binds
0.0.0.0(so the browser VM can reach it at the vmnet gateway) but accepts connections only from loopback and the pinned192.168.127.xbrowser subnet — it is never an open relay.
SOCKS forwarding and fixed port forwards
Every connected remote host automatically gets a local SOCKS5 forwarder on an ephemeral port bound to 0.0.0.0 (peer-filtered to loopback and the 192.168.127.x browser subnet). Each CONNECT opens a forward <ip> <port> SSH channel that the remote bridges — via the guest's always-on loopback relay on vsock 5010 — to that guest's own 127.0.0.1:<port>. So from this Mac, curl --socks5 127.0.0.1:<port> http://192.168.64.5:3000 reaches the remote guest byte-for-byte.
For fixed forwards there is hidden CLI plumbing: bromure-cli __forward <hostID> <localPort> <remoteIP> <remotePort> for an ssh -L-style forward, and bromure-cli __forward-socks <hostID> <localPort> for a standalone SOCKS proxy. Both bind loopback by default and accept --bind-all.
Warning: The remote refuses any forward whose target is not a guest on its own vmnet subnet. You cannot use a forward to reach the remote Mac itself, its gateway, or the internet — only the workspace VMs.
The system-wide network tunnel
When you want any local app — not just the browser pane — to reach remote VMs at their literal addresses, enable the per-host tunnel with the mirror window's network toggle (the connected-dots icon, first among the header's controls). Its tooltip reads Enable direct network access to the remote's VMs (192.168.x.x from any local app).
A one-time privileged helper — a launchd daemon installed via SMAppService, no sudo — creates a utun interface and routes the remote subnet to it, then hands the interface's file descriptor back to the app. A userspace TCP/UDP stack in the app terminates each flow and splices it over the same SSH forward channels, using your SSH identity. There are no pf rules and no kernel forwarding.
The first time you enable it, an alert titled Allow Bromure's network helper explains the one-time macOS approval:
- Click the toolbar toggle.
- In the alert, open System Settings → General → Login Items and allow Bromure Agentic Coding (macOS asks for administrator credentials once).
- The app polls for the approval (every 2 seconds, up to 5 minutes) and connects automatically the moment it lands. While it waits, the toggle tooltip reads Waiting for approval in System Settings › Login Items….
If the remote hasn't reported its machine subnet yet (no machine has started), the tooltip reads Waiting for the remote to report its workspace subnet (start a workspace) — click to cancel. When the tunnel is up the tooltip reads Direct network access to the remote's VMs is ON — click to turn off; if it fails, Network tunnel failed — click to retry. Toggling off stops the tunnel, and when the last host disables it the daemon is unregistered. The setting is remembered per host.
Note: The tunnel needs an administrator account for the one-time approval. Without one, Bromure keeps working over the built-in SOCKS path — the helper is only needed to reach remote VMs from other apps. If macOS refuses the registration outright, an alert titled Couldn't install the network helper explains, and the toggle retries on the next click. A
pingto a remote guest is answered locally by the tunnel: it confirms that routing is up, not that the guest is alive.
Multiple remotes (per-install subnets + fleet routing)
You can mirror several remotes at once; each opens its own window, SOCKS forwarder, and (optionally) tunnel. The trap this used to hit: every install historically used the same 192.168.64.0/24 network for its workspace VMs, so two mirrored remotes advertised identical subnets and the system-wide VPN could not tell them apart.
Per-install subnets fix this at the source. Each Mac now picks a random /24 inside 172.16.0.0/12 — skipping Docker's 172.17–172.20 range and any /24 the host is already on — and uses it for all of its workspace VMs, so two independent installs almost never collide (172.16/12 is also the private range least likely to clash with a corporate LAN or VPN). Fresh installs start on such a subnet; you can see, change or re-randomise it any time under Preferences › Resources › Workspace subnet. Guest addressing is pure DHCP from Bromure's own switch, so this is entirely host-side (no image rebuild), and the remote advertises its chosen subnet in its state so the rich client and the tunnel follow it automatically.
For the rare case where two remotes still share a subnet, the first is routed literally and the second is given a 100.64.<n>.0/24 CGNAT-space alias, with a 1:1 host-octet-preserving remap so both can be reached at once.
Warning: Multi-host alias routing is implemented and unit-tested, but the runtime wiring that stitches an alias-aware PAC and browser-MCP remap across hosts is still pending. A single remote host is the fully supported configuration today. A known limitation of aliasing, once wired, is that a dev page emitting an absolute literal
http://192.168.64.xlink inside an aliased remote can misroute; literal (non-aliased) remotes are unaffected.
Exposing a dev server to the internet
Separately from all of the above, an individual HTTP service running inside a workspace can be exposed to the public internet with a per-port Cloudflare quick tunnel. Open a running machine's details (the machine menu › Machine Details), find the service under the ports card, and click the globe (tooltip: Expose to the internet (Cloudflare quick tunnel)). Bromure runs one cloudflared process for that origin and gives you a random https://<name>.trycloudflare.com URL — no Cloudflare account needed. One tunnel and one URL per service; exposing or unexposing one never disturbs the others.
The globe's color tracks state: gray = off, orange = installing or starting, green = running, red = failed. A status line under the port shows progress (Downloading & verifying cloudflared (~18 MB, one-time)…, then Starting tunnel…) or the clickable public hostname with a copy button.
The first time you expose anything, a one-time consent dialog titled Expose services to the internet via Cloudflare Tunnel? covers the download, verification, the tunnel processes (recorded in the Security Timeline), Cloudflare's Terms of Service, and the fact that anyone with the random URL can reach the service. Click the globe again to stop.
The cloudflared binary is a pinned release, verified twice before it ever runs: the SHA-256 of the downloaded archive is checked against a hash baked into the app, and a Developer ID code-signature check confirms Cloudflare's team ID. It runs with --no-autoupdate.
Warning: Quick-tunnel URLs are public to anyone who has them, and the hostname rotates whenever the tunnel restarts (a reboot, an unexpose/re-expose, or a crash). Only HTTP(S) origins get a globe — SSH and well-known raw-TCP or database ports (22, 1433, 3306, 3389, 5432, 5672, 5900, 6379, 9092, 11211, 27017) and UDP never do, because a browser could not reach them through Cloudflare's edge. All of a workspace's tunnels are torn down when its VM suspends or shuts down. A hash mismatch on download refuses installation until the pin is updated in an app release.
OAuth callback forwarding
A CLI running inside a workspace VM — gh, gcloud, a subscription sign-in — often completes an OAuth flow by opening a browser on your host Mac and waiting for a redirect to http://127.0.0.1:<port>/callback. Bromure bridges that automatically: the host binds the loopback port on both IPv4 and IPv6, and on the first real request splices it over vsock 5010 to the guest's own 127.0.0.1:<port>. The redirect_uri is never rewritten, so PKCE and redirect validation still pass. There is nothing to configure — start the login inside the workspace and the redirect is delivered into the guest.
The forwarder tears itself down after 300 seconds so the loopback port is never held indefinitely. If the host port is already in use, the flow proceeds natively without the bridge, and speculative browser preconnects (sockets that open but never send) are ignored so they cannot wedge a single-shot callback server.
Security model
Remote access is powerful, so it is worth being precise about its boundaries.
- Off by default, one door. No remote path exists until you enable the SSH server. Every path — human menu, CLI, rich client, port forwards, browser MCP — rides that one server and no other.
- Encryption and transport. All traffic is SSH. The rich client's state stream and control calls, terminal attaches, file transfers, port forwards, and browser-MCP relay all travel inside the SSH connection. Because swift-nio-ssh has no post-quantum key exchange, a modern OpenSSH client will print a "not using a post-quantum key exchange" warning; this is expected and does not indicate a downgrade attack.
- bromure.io is a broker, not a middleman. In the account path, bromure.io only introduces your two devices and — when no direct peer-to-peer path can be built — relays opaque bytes through a TURN relay (over TLS). It never terminates the SSH session: the encrypted tunnel runs end-to-end between your two Macs. SSH keys are shared only among the devices of your own account (never another user's), each server pulls its owner's device keys itself, and enrolling / signing out is how a device's trust is granted and retired. A signed-out or 90-day-idle device stops being authorized.
- Who can log in. Only the exact macOS user who launched the app. Password auth is verified against OpenDirectory (no Remote Login, no PAM changes, no admin rights), and public-key auth accepts only keys you have enrolled. Password attempts are rate-limited per source IP; keys are not.
- What a session can do. A human login can only reach the remote menu — never a shell — so it can create, configure, and drive workspaces, but it cannot run arbitrary host commands. The rich client's four machine verbs are similarly narrow: a control-socket bridge, a forward to a guest on the vmnet subnet, and a browser-MCP relay. Forwards to the host itself, the gateway, or the internet are refused.
- Secrets never leave the host. Remote settings reads are secrets-blanked (model-provider keys redacted) and saves are secret-preserving; API keys, tokens, subscription sign-ins and MCP environment secrets stay on the remote Mac.
- Trust on first use. The rich client pins the remote's host key per endpoint on first contact and warns loudly on any later change.
- Guest-unforgeable consent. MITM consent prompts raised by a running agent are rendered on the host side of the terminal pump, where a compromised guest can neither see nor forge them; any timeout or detach resolves to deny. See Remote consent handling.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Enabling returns an error | Base image is missing or still installing | The server cannot start until the base image is present. Finish the install, then enable again. |
ssh connects but says to reconnect with ssh -t | No TTY was requested | The remote menu needs a terminal; add -t (or use ssh -t -p 2222 <user>@<mac>). |
| OpenSSH prints "not using a post-quantum key exchange" | swift-nio-ssh has no PQ key exchange | Expected and harmless — the connection is still encrypted. |
| A by-address connect keeps asking for a password | This Mac's key isn't enrolled on the remote | Let one password login enroll it automatically, or run bromure-cli remote key add '<this Mac's key>' on the remote (Show this Mac's key copies it). |
| A bromure.io connect still asks for a password | The key registry hasn't authorized this device on that server yet | Passwordless is automatic here — check you're signed in to bromure.io on both Macs, the server has Remote Access on, and both are running a current build (the server publishes its login and pulls your keys). Give it a few seconds after enabling Remote Access. |
| My servers · bromure.io is empty | Not signed in, or the server isn't advertising | Sign in (app-name menu → Log in to bromure.io…); on the remote, enable Remote Access so it heartbeats and appears. A greyed row means it stopped heartbeating. |
| "Couldn't establish a connection … via bromure.io" | No direct or relayed network path between the two Macs | Confirm both can reach bromure.io; the relay needs turns:5349 reachable. Try again — a path is retried on the next attempt. |
| "Reached … over bromure.io, but its SSH front door didn't answer" | Remote Access was turned off on the remote | Re-enable Remote Access on the remote Mac. |
| "This Mac's bromure.io device was revoked. Sign in again." | This device was logged out / revoked (by you, the web console, or 90-day idle) | Re-enroll: app-name menu → Log in to bromure.io…. |
| Red Trust New Key & Continue sheet | The remote's host key changed since it was pinned | Verify out of band; only continue if you know why the key changed (reinstall, new Mac) — otherwise it may be a man-in-the-middle. |
| Live connections drop after editing keys | Adding or removing an authorized key restarts the listener | Expected; reconnect. |
| Browser pane stays blank | The SOCKS forwarder or subnet is not ready yet | Wait for the mirror's first state update; ensure the machine is running and this Mac has the browser image installed. |
| A forward is refused | The target is not a guest on the remote's vmnet subnet | Forwards can only reach workspace VMs, never the host, gateway, or internet. |
| Tunnel toggle stuck at Waiting for approval… | The privileged helper is not approved yet | Approve Bromure Agentic Coding in System Settings → General → Login Items (needs admin). Without it, SOCKS still works. |
| Grid-layout edits don't take on the remote | The remote is headless | Grid edits need a GUI window open on the remote; everything else still mirrors. |
Files, ports, and environment variables
Server-side state lives under ~/Library/Application Support/BromureAC/remote/: hostkey_ed25519 (0600) and its .pub, authorized_keys (0600), and the optional overlay-key override. The rich client's identity and state live under ~/Library/Application Support/BromureAC/remote-client/: id_ed25519(.pub) (comment bromure-ac-fatclient), known_hosts (TOFU pins), hosts.json (saved servers), and per-host mirror persistence under hosts/<host-UUID>/. The whole support directory relocates under CFFIXED_USER_HOME.
| Port / interface | Role |
|---|---|
TCP 2222 (configurable, ≥ 1024) | The embedded SSH server, bind 0.0.0.0 by default. |
Outbound TCP 443 + turns:5349 | bromure.io account path only. Reaching your account (device directory, rendezvous) and, when no direct peer-to-peer path can be built, the TURN relay over TLS. Both must be reachable from each device for the account path; unrelated to the by-address path. |
| Guest vsock 5010 | Always-on loopback relay in every VM — the path for port forwards and OAuth callback delivery. |
| Guest vsock 5830 | The workspace agent's browser-MCP stream, relayed to the rich client's browser pane. |
Ephemeral port, bound 0.0.0.0 | Per-host SOCKS5 forwarder (peer-filtered to loopback and the 192.168.127.x browser subnet). |
192.168.127.0/24 | The rich client's pinned local browser-VM subnet. |
100.64.<n>.0/24 | CGNAT alias subnets assigned on a fleet subnet collision. |
The relevant UserDefaults keys (domain io.bromure.agentic-coding) are remoteAccess.enabled, remoteAccess.port, remoteAccess.bindAddress, remoteAccess.passwordAuth, remoteAccess.pubkeyAuth, fatclient.tunnel.<host-UUID>, and cloudflareTunnel.consented. Diagnostic environment variables include BROMURE_OVERLAY_KEY (remote-menu chord), BROMURE_FATCLIENT_LOG (verbose rich-client logging), and BROMURE_FATCLIENT_UTUN (force-enable the system-wide tunnel opt-in — the toolbar toggle is the product path). The full CLI reference, including the hidden __forward, __forward-socks, __dial, and __attach-window plumbing, is in CLI, API & MCP.