Coding agents on Linux boxes, the Mac for Apple builds
My coding agents run on always-on Ubuntu boxes that bring them back after a reboot. A separate Mac only does what Linux can't, Apple builds and simulators.
In 30 seconds
- Agents live on always-on Ubuntu 24.04 boxes; a separate build Mac runs no agent and only does Apple builds, simulators and UI tests.
- Each agent runs in its own tmux server under a per-user service that resumes its session at boot, or refuses and waits for me.
- Each project gets its own tree and a memory slice sized to the box, soft limit before hard, so work is throttled before anything is killed.
- The move isn't finished; some agents still run on my Mac laptop.
Problem
A post going round this week said every thread on a Mac feels slower than on Linux. My own reply on X on October 24 was shorter: “Move dev work to Linux boxes. Use Mac only for Apple items Linux boxes cannot handle.” Here’s the setup, for anyone starting out.
My agents started on my Mac laptop, my daily machine, which sleeps and restarts under them.
What I tried
| Attempt | What happened |
|---|---|
| Agents on my Mac laptop | Load 91 on 16 cores after 53 days up, with the file-events daemon pinned at 100%. Another day, load near 980, that daemon at 21.8 GB and simulators at 23 GB. |
| A laptop as an agent box | It hard-hung 45 s after automation switched it to its “performance” profile, and again later under CI. |
| Tight memory budgets per project | One project sat throttled at 13G with about 95G free, and that pressure set off the out-of-memory killer. |
| Starting the Codex engine (its background app server) before its login | It stayed “logged out” until someone restarted it by hand. |
| The default file-watch limit | It ran out while an agent session watched its finished worktrees. |
| Relaunching an agent by typing into its pane | It dropped an MCP config. |
| Restarting agents to load new rules | Panes got stuck in Claude Code’s “Background work is running” dialog. |
What worked
Linux for agents, a Mac for Apple. Most agents moved to dedicated, always-on boxes. A separate build Mac runs no coding agent at all.
The box. Ubuntu 24.04 LTS desktop, booting UEFI. Xorg, not Wayland, for computer use. Autologin on; lock screen, blanking and sleep off. The login keyring is unencrypted, because autologin never unlocks a password one. Disk encryption only if it unlocks with nobody there. The firmware powers back on after a power cut. No unattended upgrades: I update on purpose.
Power. Desktops run the performance governor and swappiness 10. On laptops, automation never touches the firmware’s power profile. After its hangs, my laptop runs CI only under a hard power cap.
Tools. git, gh, tmux and OpenSSH from Ubuntu’s packages. Node 24 from the official tarball, checked against its published SHA-256. Codex from npm, Claude Code from its own installer, its auto-updater off where a version must stay fixed.
One tree per project. Each project gets its own tree, its own Codex engine home and its own memory slice, under one shared login user. That’s a convention, not a security boundary. An agent that holds its own credentials gets its own Unix user, and CI jobs run as separate users.
Budgets sized to the box. A soft memory limit before the hard one, so a busy project slows before anything dies; the out-of-memory killer’s pressure kill is off for the user’s services. The file-watch limit sits at about half a million and is never lowered. After a login, a watcher restarts the Codex engine, only when no Codex task is running.
Agents that come back on their own. Lingering is on, so the user’s services start at boot. Each agent has its own tmux server and a per-user service, after its Codex engine, that waits up to 120 s for the display, the engine and my model proxy on another box, then resumes the agent’s last session by its id. No record, no start. tmux lives in the project’s slice, so it outlives the service. Nothing auto-restarts: a failed start, or any dialog at start, waits for me.
Restarts through one wrapper. It relaunches from the agent’s recorded spec, in a new tmux server every time. To load new rules, I have the agent re-read them instead of restarting it.
Reaching them. ssh with keys only, over Tailscale; I approve each new box. No box-to-box keys by default: each exception is a forced-command key tied to the caller’s address. Logins are device-code flows I finish. Agents reach my phone through a private relay, one mailbox each, and an iOS app; secrets move only as owner-only files.
The Mac’s job. Agents call one build wrapper over ssh with a repo URL, or a git bundle for unpushed work. It runs CI on a fresh simulator, deleted afterwards; runs share one simulator slot and wait up to an hour. The result is one line: PASS or FAIL, sha, exit code, seconds. Lint, formatting and project generation can run on Linux.
Numbers
| Mac laptop load on 16 cores, 53 days up | 91measured |
|---|---|
| Mac file-events daemon on a worse day | 21.8 GB, simulators 23 GB moremeasured |
| One CI job, power-capped laptop vs two desktops | about 2,066 s vs 584-835 s mediansmeasured |
| Memory budget that throttled with about 95G free | 13Gmeasured |
| File-watch (inotify) limit, at least | 524,288configured |
| Boot wait for display, engine and proxy | up to 120 sconfigured |
| Simulator runs at once on the build Mac | 1configured |
| Agents still on my Mac laptop (2026-10-28) | 4measured |
No head-to-head Mac vs Linux benchmark was run. The Mac numbers are two readings of my laptop choking as an agent host; the CI times compare Linux boxes with each other: the box matters too.
When it stops being true
This is Ubuntu 24.04 with Claude Code and Codex as of October 2026. The move isn’t done: four agents still run on my Mac laptop. A reboot may cure the file-events load; its cause is likely, not proven. The laptop hang’s cause is unknown. If Wayland gets good enough for computer use, the Xorg rule goes.
Agent brief
Set up a Linux box for my coding agents; keep my Mac for Apple work only. "(general)" marks advice not from my setup.
Words: "Codex engine" is Codex's background app server, which Claude-Code-only setups skip (general); a "Codex task" runs in its own git worktree; "proxy" is a model proxy you run, if any.
First ask me:
- Which spare machine, its RAM and cores (a desktop: my laptop hung).
- Which agents (Claude Code, Codex)? Computer use? Apple builds? One Mac or two? My Tailscale account?
Before ssh works (general), I do these by hand as you guide me: make the Ubuntu USB installer; set the firmware to power on after a power cut; install; create an SSH key on my Mac and copy it to the box before password login goes off.
Constraints:
- I finish every login (device-code flows, Tailscale approval); never type my passwords.
- Secrets: owner-only files, never chat.
- No box-to-box keys; each exception is a forced-command key tied to the caller's address, with my yes.
- One shared Unix user is fine for my own agents (general): a convention, not a boundary. An agent holding its own outside credentials gets its own Unix user.
Steps:
- Ubuntu 24.04 LTS desktop, UEFI, Xorg not Wayland. Autologin; lock, blanking and sleep off. Unencrypted login keyring, since autologin never unlocks a password one.
- A stolen box then hands over the agents' tokens: keep it safe (general), or encrypt only with unattended unlock.
- ssh keys only (password and keyboard-interactive off); join Tailscale; plain OpenSSH over it. Firewall ssh to the Tailscale interface (binding to its address can lock you out at boot; general).
- I turn unattended upgrades off to pin versions; without pinning, keep security updates on (general).
- git, gh, tmux, OpenSSH from apt; Node 24 from the official tarball, checked against its published SHA-256; Codex from npm; Claude Code from its installer, updater off where a version must stay fixed (an update applies only after a restart).
- Desktop: performance governor, swappiness 10. Laptop: never switch its platform profile by automation.
- File-watch (inotify) limit at least 524288, never lowered.
- Per project: a tree, its own Codex engine home, a memory slice with a soft limit before the hard one; soft limits together under about three quarters of the RAM (general). Turn off the user manager's out-of-memory pressure kill.
- Per agent, a recorded spec: a note written at setup with its folder, session and start-up tools, which the restart wrapper reads.
- Enable lingering. Per agent: its own detached tmux server and a per-user oneshot service, after any Codex engine, waiting up to 120 s for display, engine and proxy, then resumes its last session (Claude Code: by session id, the newest interactive one; keep scripted test runs out of its folder, general; Codex resumes too, general). None: refuse. tmux in the slice. No auto-restart. If the session doesn't reach its prompt, the service fails and I look.
- Restart only through the wrapper, never a launch typed into a pane, always in a new tmux server.
- After a Codex login, restart its engine, only when no Codex task runs.
- Phone: the Tailscale app plus an SSH app attach to the tmux sessions (general).
- Apple work: a build Mac runs no agent; with one Mac, that's it, and the agents move off (general).
- Mac side (general): Remote Login on, Xcode, a separate macOS user for builds so agent-run builds never touch my account, a deploy key for private repos. The Linux box's key allows only the build wrapper (my release-signing key is forced-command like this).
- The wrapper takes a repo URL or git bundle, fetches the ref, builds and tests on a fresh simulator it deletes after, and returns one line: PASS or FAIL, sha, exit code, seconds. A run waits up to an hour for the one simulator slot, then exits with its own error. Lint, formatting and project generation can run on Linux.
Pitfalls:
- Load new rules by having the agent re-read them. A restart kills session-only tasks, monitors and shells and can stick at "Background work is running": check first, clear it, re-arm.
Done when:
- Reboot untouched: every agent resumes its session, no prompt.
- An ssh password login is refused.
- My phone attaches to an agent's session.
- A Mac build returns its one line.