Here's a full step-by-step guide on how to turn your spare Mac into an always-on machine OpenAI's Codex CLI can fully control, with computer use enabled. You'll be able to talk to it from your phone through the ChatGPT app (ChatGPT Remote), or from your main Mac over SSH.
This is a port of ykdojo/claude-controls-mac to Codex - same idea, mapped onto Codex CLI's own commands, config, and features.
In case you're reading this on GitHub Pages, here's the repo version.
I wanted a separate environment Codex can control on its own, so I can delegate tasks I don't necessarily want to run on my own machine - certain kinds of research and development work.
Codex CLI, especially with --dangerously-bypass-approvals-and-sandbox (aka
--yolo), carries inherent risk when run on your main machine. You can
mitigate that by creating a separate environment on a spare Mac with everything
it needs and nothing you care about.
It has an added bonus: you can talk to Codex anytime, anywhere from your phone via ChatGPT Remote, while the work actually runs on the box.
The guide assumes you have a main Mac plus a spare Mac to set up, but you can adapt it to any two machines.
Containers still run on your main machine (network requests go out through it), and can't reach Mac-only apps - which matters if you want Codex to drive GUI apps through computer use (clicking, dragging, and so on).
Running an agent with broad permissions is safer on a machine that has nothing to lose - but you keep a full Mac instead of a container. The approach here:
- Use an old/spare Mac, not your main one.
- Create a fresh local account with no personal data and no Apple ID signed in, so the agent has nothing sensitive to reach.
- Drive it over SSH from your main Mac on your local network, and control it from your phone.
- A spare Mac (the target / "the box").
- Your everyday Mac (the source), on the same Wi-Fi.
- A ChatGPT plan that includes Codex (for login on the box), and for the phone and browser steps, the ChatGPT desktop + mobile apps.
You'll be giving the agent full access to this machine, so it can reach anything stored on it. If there's existing data you don't want it to have, erase the machine first:
- Macs that support it: System Settings -> General -> Transfer or Reset -> Erase All Content and Settings.
- Older Intel Macs: restart into Recovery (hold Cmd-R at boot), use Disk Utility to erase the internal drive, then reinstall macOS.
Optionally update to the latest macOS afterward (System Settings -> General -> Software Update).
- Create a new local user account (System Settings -> Users & Groups).
- I recommend not signing into an Apple ID. Skip it during setup.
The account needs admin rights or sudo will refuse to run.
- System Settings -> Users & Groups -> set the account to Allow this user to administer this computer.
- To repair it from another admin account:
sudo dseditgroup -o edit -a <user> -t user admin
On the target, turn on SSH so the source Mac can connect:
sudo systemsetup -setremotelogin onIf it fails with Turning Remote Login on or off requires Full Disk Access privileges, give your terminal app Full Disk Access first:
- System Settings -> Privacy & Security -> Full Disk Access.
- Click +, then add Applications -> Utilities -> Terminal.
- Quit and reopen the terminal, then rerun the command.
So the agent (and your SSH commands) can run admin tasks without a password prompt each time. Run this once on the target. It asks for the login password this one time:
echo "<user> ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/<user>-nopasswd >/dev/null
sudo chmod 440 /etc/sudoers.d/<user>-nopasswd
sudo visudo -cf /etc/sudoers.d/<user>-nopasswd # validate - must print 'parsed OK'- line 1 writes the rule into
/etc/sudoers.d/. - line 2 makes it read-only - sudo ignores the file otherwise.
- line 3 validates the syntax; a typo in a sudoers file can lock you out of
sudoentirely, so it must printparsed OK.
Test with sudo -n true, which succeeds silently if passwordless sudo works.
You can reach the target by either a hostname or an IP. I recommend the hostname: it stays the same, while the IP can change.
Hostname (recommended). Run on the target:
scutil --get LocalHostName # prints the hostname, e.g. MacBook-ProAdd .local to form the address: <target-host>.local.
Give the target a unique name. Each Mac needs a
.localname unique on your network. Rename if needed:sudo scutil --set LocalHostName codexbox # -> codexbox.local
IP address (not recommended). Run on the target:
ipconfig getifaddr en0 # e.g. 192.168.1.80Throughout the rest of the guide, replace <user> with the target account name
and <target-host> with the hostname, so the address is
<user>@<target-host>.local.
On the source Mac, create an SSH key (skip if you already have one):
ssh-keygen -t ed25519Install your public key on the target (asks for the target's login password
once). macOS doesn't ship ssh-copy-id, so append the key over plain ssh:
cat ~/.ssh/id_ed25519.pub | ssh <user>@<target-host>.local \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"Test it - this should print the target username with no password prompt:
ssh <user>@<target-host>.local whoamiTip: add the box to
~/.ssh/confignow - it makes step 13 (phone control via Codex Remote SSH) a one-click add later:Host codexbox HostName <target-host>.local User <user> IdentityFile ~/.ssh/id_ed25519
macOS sleeps after ~10 minutes idle, even when plugged in, which takes it off the network. To make it never sleep, run this on the target (or over SSH):
sudo pmset -c sleep 0 # never system-sleep while plugged in (-c = on charger)
sudo pmset -c disablesleep 1 # also prevents sleep with the lid closed (clamshell)
sudo pmset -c displaysleep 0 # keep the display on tooVerify:
pmset -g | grep -iE 'sleep'sleep 0, SleepDisabled 1, and displaysleep 0 confirm it worked.
Stop the screen saver from ever starting so it never locks on its own:
defaults -currentHost write com.apple.screensaver idleTime 0macOS ships pbcopy (write clipboard) and pbpaste (read clipboard). Piped
over SSH, they move the clipboard between machines - encrypted, peer-to-peer, no
Apple ID or third-party service.
clip.sh wraps this into one command with two subcommands, and adds
image support on top of pbcopy/pbpaste (which are text-only). Install it on
your PATH on the source Mac, and point it at the target with IC_BOX ("ic" =
"isolated codex"):
mkdir -p ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/hasansezertasan/codex-controls-mac/main/clip.sh -o ~/.local/bin/clip
chmod +x ~/.local/bin/clip
export IC_BOX="<user>@<target-host>.local" # add to ~/.zshrcUsage:
clip send- this Mac's clipboard → the target (text or image). For an image you can paste it straight into a Codex session on the target with Ctrl-V.clip get- the target's clipboard → this Mac (text or image).
Install Codex on the box. Homebrew is the easiest to keep updated; npm and the official install script also work:
# Homebrew (recommended)
ssh <user>@<target-host>.local 'brew install --cask codex'
# or npm
ssh <user>@<target-host>.local 'npm install -g @openai/codex'
# or the official install script
ssh <user>@<target-host>.local 'curl -fsSL https://chatgpt.com/codex/install.sh | sh'Make sure codex is on PATH for non-interactive shells - add its directory to
~/.zshenv (read by every zsh, unlike ~/.zshrc). For a Homebrew install on
Apple Silicon:
ssh <user>@<target-host>.local 'echo '\''export PATH="/opt/homebrew/bin:$PATH"'\'' >> ~/.zshenv'(The setup-computer-use.sh script in step 11 also adds the right directories
to ~/.zshenv for you.)
This optional step applies opinionated defaults via
setup-codex-env.sh - shell aliases, config.toml model
and reasoning defaults, AGENTS.md guidance, the GitHub CLI, and (opt-in)
Playwright MCP and yt-dlp. Every item is toggleable; see the full list in
codex-env-components.md.
Whatever you select, it also ensures jq on the box (installing it if missing) -
a hard dependency of the ic helper, which parses Codex's rollout JSONL with it
for ic history and ic ls. Run this step, or brew install jq yourself, so
those previews work.
Interactively on the target - shows a checklist (core pre-checked, opt-ins unchecked):
ssh -t <user>@<target-host>.local \
'curl -fsSL https://raw.githubusercontent.com/hasansezertasan/codex-controls-mac/main/setup-codex-env.sh -o setup-codex-env.sh && bash setup-codex-env.sh'Non-interactively - installs core only by default, or pick with flags
(--yt-dlp, --playwright, --all, --core):
ssh <user>@<target-host>.local \
'curl -fsSL https://raw.githubusercontent.com/hasansezertasan/codex-controls-mac/main/setup-codex-env.sh -o setup-codex-env.sh && bash setup-codex-env.sh --all'The script is idempotent (OK to re-run).
Both logins are interactive, so SSH in:
ssh <user>@<target-host>.localThen run codex on the target and choose Sign in with ChatGPT (or use an
API key). Follow the prompts - a browser/device flow you can finish from a
browser on your main Mac. You can also start it explicitly with codex login.
GitHub - optional, but highly recommended so the agent can work with repos:
gh auth loginIf you didn't install the GitHub CLI in step 9, do so first. I recommend a separate GitHub account, not your main one.
This lets an interactive codex session on the target see (screenshots) and
control (mouse/keyboard) its own desktop, driven over SSH.
Codex CLI has no built-in computer-use tool, so we give it one as an MCP server:
computer-use-mcp exposes
screenshot / click / type / window tools over MCP and works with Codex CLI.
This doesn't work out of the box - SSH and macOS's permission model get in the way, so the setup below routes around that.
Why it needs a workaround: macOS gates screen capture and input behind
Screen Recording and Accessibility permissions tied to the GUI login session, so
an SSH process can't reach the display. Fix: a LaunchAgent keeps a tmux server
alive inside the GUI session on a fixed socket; every codex session created
there lands on that server and inherits the GUI session, so it (and the MCP
server it spawns) can reach the display. You attach over SSH.
Run setup-computer-use.sh on the target:
ssh -t <user>@<target-host>.local \
'curl -fsSL https://raw.githubusercontent.com/hasansezertasan/codex-controls-mac/main/setup-computer-use.sh -o setup-computer-use.sh && bash setup-computer-use.sh'This installs the LaunchAgent (persistent tmux server with anchor session
cc) and registers computer-use-mcp in ~/.codex/config.toml. Requires tmux
(brew install tmux) and Node/npx (for the MCP; brew install node or step 9
--all). Re-runnable; --uninstall to remove.
Install ic.sh (ic = "isolated codex") on the source Mac:
mkdir -p ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/hasansezertasan/codex-controls-mac/main/ic.sh -o ~/.local/bin/ic
chmod +x ~/.local/bin/ic
echo 'export IC_BOX="<user>@<target-host>.local"' >> ~/.zshrc # or edit the default in the scriptEach ic spawns its own codex session on the box (run several at once) and
attaches:
ic # new codex session
ic -c # continue the most recent conversation (codex resume --last)
ic -r # resume picker (codex resume)
ic sh # a plain shell on the box, no codex (alias: ic shell)
ic vnc # open Screen Sharing (VNC) to the box (see step 15)
ic rc # how to drive the box from your phone (ChatGPT Remote; see step 13)
ic history # stored conversations: count, location, recent (alias: hist)
ic ls # list live sessions (state, age, proc)
ic attach <id> # attach a running session (alias: ic a)
ic kill <id> # kill a session (alias: ic k)
ic kill-all # kill all sessions
ic kill-except <id> <id> ... # kill all sessions except the listed ones
ic -h # helpAll ic sessions run with --dangerously-bypass-approvals-and-sandbox (the box
is an isolated sandbox, so approvals are auto-granted).
Copying text out: sessions run in tmux, and Terminal.app can't reliably receive clipboard escape sequences, so mouse-selecting won't always reach your Mac clipboard. Quick workaround: Cmd-A then Cmd-C copies the whole visible screen.
Screen Recording and Accessibility can only be granted in the GUI, and a human has to do it at the machine (in person or via Screen Sharing) - macOS blocks synthetic clicks on these prompts.
The grants go on tmux, not codex. macOS attributes capture/control to
the responsible process in the chain, which here is the tmux server (codex,
then node/npx, then the MCP tools all run as its descendants). So:
- Grant
tmux(/opt/homebrew/bin/tmuxon Apple Silicon, or/usr/local/bin/tmuxon Intel) under both Screen Recording and Accessibility. - Restart the tmux server after granting - a running process caches its
permission state at launch:
tmux -S /tmp/cc-tmux.sock kill-server(the LaunchAgent respawns the anchor within seconds). Codex sessions started after the restart pick up the grant.
To make the entries appear in System Settings, trigger a computer-use action
(ic, then ask Codex to take a screenshot) - macOS adds tmux to the list
(toggled off) so you can switch it on. On newer macOS, granting tmux Full
Disk Access as well suppresses the per-app "tmux would like to access data
from other apps" prompts. Restart the tmux server after any grant.
Because every ic session lives on the box's tmux server at a fixed socket, a
Codex session on your source Mac can prompt one directly over SSH - useful for
delegating work to the box and checking on it, agent to agent:
# find the session (ic ls prints the ids)
ic ls
# type a prompt into it, then confirm with a second Enter
ssh <user>@<target-host>.local "tmux -S /tmp/cc-tmux.sock send-keys -t <session> 'Switch to main and pull; PR #4 is merged.' Enter"
sleep 2
ssh <user>@<target-host>.local "tmux -S /tmp/cc-tmux.sock send-keys -t <session> Enter"
# read the reply (re-run / poll until it's done)
ssh <user>@<target-host>.local "tmux -S /tmp/cc-tmux.sock capture-pane -t <session> -p" | tail -30I like to run a VPN on the box so its traffic goes out separately from my local
IP. You can just ask the box's Codex to do it once you've finished
step 11: ic in and say "install Proton
VPN". It'll download and install the app. The parts it can't do alone:
- Credentials. Signing in is yours to do. Send the password securely with
clip sendfrom step 7. - macOS permission prompts. The first connect pops a system prompt to allow a VPN / network configuration - approve it at the machine.
- Computer use for the GUI. The app is GUI-only, so driving it relies on step 11. Once signed in, the agent can connect and switch servers itself.
This flow works for pretty much any Mac app, not just a VPN.
ChatGPT Remote is Codex's official way to drive a session from your phone while the work runs on the host machine (the box), not in the cloud. Your files, credentials, and local setup stay on the box; your phone sends prompts, approvals, and follow-ups, and gets back diffs, terminal output, and results.
Because the box is an SSH host (steps 2-5), the clean way to wire this up is Remote SSH from the ChatGPT desktop app on your source Mac (the one with a GUI and the app):
- On the source Mac, make sure the box is in
~/.ssh/config(see the tip in step 5) andssh codexboxworks. Codex must be installed and on PATH on the box (step 8). - In the ChatGPT desktop app -> Settings > Connections > SSH -> Add
-> pick your host (e.g.
codexbox) -> choose the remote project folder. - Optionally enable "Keep this Mac awake" and Computer Use under the same Connections pane.
- On your phone's ChatGPT app -> Remote tab -> pick the host -> start or continue a Codex thread. It runs on the box.
You can also pair directly (desktop app -> Set up Remote -> scan the QR code with your phone) if you'd rather drive the desktop app's own session.
Note: unlike Claude Code's
claude remote-control, Codex's phone pairing is not started from the CLI - it's set up in the ChatGPT desktop app.ic rcjust prints these steps for reference. Community tools (e.g. app-server + a LAN web-terminal bridge) exist if you want a fully self-hosted alternative, but ChatGPT Remote is the official path.
Computer use from step 11 can see the screen but is a coarse way to drive a browser. Codex for Chrome (OpenAI's official extension) gives proper browser control - navigating, clicking, filling forms, reading console logs and network requests - and drives your regular Chrome profile, so the agent can use any logged-in state you set up on the box.
It requires Chrome on the target and a Codex-enabled ChatGPT plan. (At launch it was unavailable in the EU/UK - check current availability.)
Ask the box's Codex to install Chrome and open the extension. The parts it can't do alone (do these at the machine, in person or via Screen Sharing):
- Add the extension to Chrome and approve its permissions.
- Log into your account in the extension. Send login info with
clip sendfrom step 7.
Enable it in Codex: Codex -> Plugins -> add Chrome -> install extension ->
approve permissions, then invoke browser tasks with @Chrome in a session.
The environment setup from
step 9 adds a
"Codex for Chrome" guidance section to ~/.codex/AGENTS.md so browser use is
efficient (prefer the accessibility/DOM tree over screenshots). If you skipped
it, re-run the script - it's idempotent.
This lets you see the target's screen live from the source Mac - and take over its mouse and keyboard - using macOS's built-in Screen Sharing.
This has to be enabled in the GUI at the machine - since macOS 12.1 it can't be enabled from the command line.
On the target: System Settings -> General -> Sharing -> Screen Sharing on. If Remote Management is on, the Screen Sharing toggle may be hidden - turn it off first.
Connect from the source Mac (or use ic vnc):
open vnc://<user>@<target-host>.localLog in with the target account's password and tick Remember this password in my keychain.
.local names only resolve on your LAN, so everything so far is local-network
only. Tailscale fixes that: it connects your machines
with peer-to-peer, end-to-end encrypted
WireGuard tunnels, so SSH, ic, clip, and Screen
Sharing work from any network with nothing exposed to the public internet. On
your home network it takes a direct LAN path, so local use stays fast.
Install on the target (the Homebrew formula works headless over SSH; sign in at the URL the last command prints):
ssh <user>@<target-host>.local 'brew install tailscale'
ssh <user>@<target-host>.local 'sudo brew services start tailscale'
ssh <user>@<target-host>.local 'sudo tailscale up --operator=<user>'Install on the source, then open the Tailscale app and log in with the same account:
brew install --cask tailscale-appWith MagicDNS (on by default) the target is reachable by bare hostname from
anywhere - the same address, minus .local. Switch IC_BOX in ~/.zshrc:
export IC_BOX="<user>@<target-host>" # Tailscale - works remotely too
# export IC_BOX="<user>@<target-host>.local" # original - local network onlyScreen Sharing works the same way: open vnc://<user>@<target-host>.
Recommended, in the admin console: device approval (Settings -> Device management) and disable key expiry on the target (Machines -> ...), so the box doesn't drop off the network when its key expires.
To confirm remote access works, put the source Mac on a different network (e.g. a
phone hotspot) and run ic ls.
Credit: this guide is a Codex port of ykdojo/claude-controls-mac.