What Argus supports
Argus Server — the headless host
Run Argus with no window on a Mac mini so agents keep working while your laptop comes and goes.
Argus Server.app is Argus with no window. It runs the whole service layer —
agents, worktrees, terminals, simulators, the remote gateway — on a machine
nobody is sitting at, typically a Mac mini, so agents keep working while your
laptop comes and goes.
It is built from the same codebase as the desktop app, as a second
electron-builder target. It is not a flag on Argus.app, and that is
deliberate: app.requestSingleInstanceLock() quits the second instance, and the
lock is keyed on the userData path derived from the app id. Two distinct app ids
means two distinct locks, which is the only reason a server and a desktop can
run on the same Mac at all — local development of remote access depends on it.
| Argus | Argus Server | |
|---|---|---|
| App id | dev.houwert.argus |
dev.houwert.argus.server |
State (ARGUS_HOME) |
~/.argus |
~/.argus-server |
| Gateway port | 47615 | 47617 |
| Window | yes | none — tray item only |
| Dock tile | yes | no (LSUIElement) |
argus:// links |
claims the scheme | never claims it |
| Distribution | .dmg |
.app only |
The server ships the same payload as the desktop: the simulator bridge,
scrcpy-server.jar, the agent prompts, the conductor tree, and the
clients/argus-web bundle — the server is what serves that bundle to phones and
browsers.
Fully standalone state
The server never shares anything with a co-located desktop. Its ARGUS_HOME
defaults to ~/.argus-server, so it gets its own worktrees, tools.json,
paired-device keys, Claude accounts, chat history and app settings. Two
processes running git in one worktree is not a thing to design for.
ARGUS_HOME still overrides the default, on both apps.
Its open projects live in repo-roots.json under that same directory and are
re-registered at launch. The server has no renderer to replay a project list, so
without this it came back from every restart with nothing for clients to attach
to.
The gateway is not a setting on the server — it is the only way in, so it is
forced on at startup regardless of remote_enabled.
Building
pnpm build:app # Argus.app + .dmg → release/
pnpm build:app:server # Argus Server.app → release-server/
The server config lives in scripts/electron-builder-server.config.cjs, derived
from electron-builder.yml so the two can't drift on bundled resources. It
overrides the app id, product name and output directory, points the packaged
main at dist-electron/server.js, drops the DMG and the Dock tile, and
removes the usage strings only a window can trigger. Both targets run
scripts/electron-after-pack.cjs.
dist-electron/server.js is the same bundle as main.js with an esbuild banner
that sets ARGUS_SERVER=1 (and the default gateway port) before the first
module loads — early enough that ARGUS_HOME is already correct when services
read it at import time.
Running one next to your desktop, for development
pnpm dev # desktop, ~/.argus-dev, gateway 47616
pnpm dev:server # headless, ~/.argus-server-dev, gateway 47617
pnpm dev:server needs no Vite — there is no renderer. It builds the main
process and launches Electron with ARGUS_SERVER=1, a separate userData
(argus-server-dev, hence a separate single-instance lock), and
ARGUS_DEV_TRUST=1.
Dev-loopback pairing
Pairing normally shows a 6-digit SAS on both ends and waits for a human to confirm they match. That assumes two devices and two screens; a headless server has neither, and pairing a desktop to a server on the same Mac has nothing to compare against.
With ARGUS_DEV_TRUST=1 and an unpackaged build, the server arms a pairing
session against ws://127.0.0.1:<port>, auto-confirms the SAS, and writes the
pairing code to $ARGUS_HOME/dev-pairing.json:
{
"created_at": 1770000000000,
"pairing_code": "eyJ2IjoxLC…",
"payload": {
"v": 1,
"url": "ws://127.0.0.1:47617",
"ds_pub": "…",
"pairing_secret": "…"
}
}
Paste pairing_code into the client. The file is rewritten whenever the server
re-arms, so it always holds the current code, and it is deleted on quit.
This bypasses the anti-MITM confirmation, so it is gated twice: on the env var
and on app.isPackaged being false. It cannot happen in a release build.
When the files are on the other Mac
A desktop attached to a host draws the same UI, but the repo is on the host's disk. Everything that touches a filesystem resolves on the host; the handful of actions that only make sense where the window is stay local.
| action | attached to a host |
|---|---|
| Add project | browses the host's folders, not yours |
| Reveal in Finder, Open, Open in editor | refused with a notice — Argus never syncs a worktree back |
| OAuth and other external links | open in your browser |
| Dropping a file into the chat, importing a patch | the bytes are uploaded to the host, and the mention points at where they landed |
| Images pasted or dropped into the chat | already travel as bytes; unchanged |
Uploaded attachments
A dropped file's path belongs to the machine you dropped it on, which is why
the bytes travel instead. They are staged under $ARGUS_HOME/uploads/<session>/
on the host — never inside the worktree, so nothing shows up in the session's
diff — and staged files are swept seven days after their last use.
The bytes move as a chunked binary transfer over the same encrypted connection that carries device video, not as one giant message, so a screen recording or a large patch goes through the way a small text file does. The chat shows a progress bar while a drop is uploading, and the transfer is only accepted once its length and checksum both match — a connection that drops mid-upload leaves nothing half-written behind.
One upload is capped at 512 MB, a limit on what the host is willing to stage on its own disk. It is enforced on the host as the bytes arrive, and an oversize file is refused by name rather than truncated; put a bigger one on the host yourself.
What a client may read and write
read_file and write_file are reachable by an admin-paired desktop. Both
resolve their target, follow symlinks, and refuse anything that lands outside
the session's worktree or a registered project root — a symlink inside the
worktree pointing at ~/.ssh is not a way out. Chunked transfers go through the
same check, once, when the transfer opens: a chunk carries a transfer id and
nothing else, so there is no later opportunity to redirect where it lands.
Git errors keep their detail
A failed git command carries a kind, the raw stderr and a suggested recovery. That structure survives the connection, so a push rejected on the host offers the same "pull then push" action it would locally.
Unattended operation on a Mac mini
A mini that stays awake is the machine to run automations on: the scheduler starts with the app, needs no window, and keeps triaging overnight while your laptop is shut. A paired desktop granted admin manages the server's automations as if they were its own.
Auto-login is required
CoreSimulator needs a logged-in Aqua session. simctl boot fails under a
launchd daemon, so the server must run as a launchd agent inside a real
user login — which means the mini has to be set to log in automatically:
System Settings → Users & Groups → Automatic log in → pick the Argus user.
FileVault blocks auto-login at first boot. Either leave FileVault off on a physically secured machine, or accept that a cold boot needs one manual unlock.
Install the launchd agent
pnpm build:app:server
cp -R "release-server/mac-arm64/Argus Server.app" /Applications/
pnpm server:install-agent # or: … /path/to/Argus Server.app
That renders build/dev.houwert.argus.server.plist into
~/Library/LaunchAgents/ and bootstraps it into the GUI domain. It starts at
login and restarts on crash. Logs land in ~/.argus-server/launchd.{out,err}.log
alongside Argus's own rotating log.
launchctl print gui/$(id -u)/dev.houwert.argus.server # status
pnpm server:install-agent --uninstall # remove
Staying awake
The server holds a caffeinate -i -m -s power assertion for as long as it runs
— a sleeping mini stops agents mid-run and drops every attached client. The
assertion is tied to the server's pid (-w), so a crash can't leave the machine
pinned awake. The display is free to sleep.
For a mini on mains power, also set System Settings → Energy → "Prevent automatic sleeping when the display is off".
Pairing your first device
The server has no window, so pairing happens in the tray:
Pair a device… arms a pairing session, copies the pairing code to the
clipboard, and writes it to $ARGUS_HOME/pairing-code.txt (mode 0600) for a
machine you only reach over SSH — the code is far too long to read off a menu
item. Paste it into the desktop's Add host field.
When the desktop connects, the menu shows a six-digit number. Compare it against the one on the desktop's screen — that comparison is what makes a man-in-the-middle impossible, and it is the reason pairing can never happen over the wire. If they match, approve:
The server decides what the device may do. The device never asks for a tier, and nothing it sends afterwards can widen one — pairing and grant changes are host-tier commands that never cross the wire. Pick the narrowest that works:
| choice | grants | what it can do |
|---|---|---|
| Approve — watch only | monitor |
Read agents, sessions and device screens. Cannot answer a tool-use prompt, which is a code-execution act. |
| Approve — run agents | control |
The above, plus send messages, spawn agents, create sessions, answer prompts, commit/push/merge a session's branch, open and merge pull requests, and author automations. The right tier for a phone. |
| Approve — full control of this Mac | admin |
Drives the host as if you were at its keyboard — every command except the host-only set. Only for a Mac you own. |
Least privilege is listed first deliberately: admin inverts the allowlist to a
denylist, so it should be a deliberate reach rather than the obvious click. You
can change a device's grants later, or revoke it, from Settings → Remote access
on the host.
Reject if the numbers differ. The code file is deleted the moment pairing settles either way, and on quit.
If the menu says no reachable address, the gateway has no LAN address or tunnel yet — check the network, or turn on a tunnel.
This is the same flow the desktop runs in a dialog, on a different surface. It
is not the ARGUS_DEV_TRUST bypass, which skips the comparison entirely and
cannot run in a packaged build.
Updating the server
A mini nobody sits at still needs new builds, and the install command is deliberately host-tier — a client must never restart a host out from under someone else's running agents. So updating happens in the tray, next to pairing.
The menu shows where the update stands:
| menu says | meaning |
|---|---|
| Check for Updates… | nothing known yet, or the last check came back clean |
| Checking for updates… | a feed check is in flight |
| Up to date | the running version is the newest on the channel |
| Update available: 1.2.3 | pick Download Update — downloads never start on their own |
| Downloading update — 42% | progress; the menu updates live |
| Update ready: 1.2.3 | pick Restart and Install |
| Update failed: … | the feed error, shortened; Check for Updates… retries |
The running version is the first line of the menu, so only the new version is named here.
Checks also run in the background once an hour, the same as the desktop. That is already far tighter than a machine that runs for weeks needs, so the server adds no second timer.
Restarting is refused while agents are running
Installing an update quits the process, which kills every agent on the machine.
So Restart and Install is replaced by a disabled 3 agents running — cannot restart whenever anything is working — including an agent whose prompt is still
in flight between tool calls. Stop or wait out the agents and the item comes
back. It never restarts silently, and the count tells you what it is waiting on.
Install automatically when idle
Install Automatically When Idle is a checkbox in the same section, off by default. With it on, a downloaded update installs itself the moment the last agent finishes — the same guard, just without a human to click it. The server re-checks for idleness every minute, so an unattended restart lands within about a minute of the fleet going quiet.
Off by default because a mini rebooting unprompted is a surprise; turn it on for a machine you want to stay current without visiting.
The setting is stored in $ARGUS_HOME/app-settings.json as
server_auto_install_updates.
The tray is the whole UI
The server's only surface is the menu-bar item: the same live agent tree the desktop shows, plus its version, gateway URL and state directory, the pairing and update flows above, and Quit. Menu entries don't navigate anywhere — there is no window to navigate.
Quitting the server does not prompt about running agents the way the desktop does; there is no renderer to ask. It tears down PTYs and agent subprocesses and exits.