230 lines
8.4 KiB
Markdown

---
name: mac-server-setup
description: >
Set up and harden a remote Mac as an always-on headless server for
running openclaw agents. Use when provisioning a new Mac (Mac Mini,
Mac Studio, etc.) for server duty via SSH. Covers dev environment
(Homebrew, nvim, tmux, node, bun, starship, gh), nvim config, server
hardening (power mgmt, firewall, consumer service cleanup, Spotlight,
SMB, hostname), SSH key auth, git repos, and local SSH config.
Generates an idempotent setup script on the remote machine. Triggers:
"set up mac server", "harden mac", "provision remote mac", "new client
server setup", "mac server hardening", "openclaw server setup".
---
# Mac Server Setup
Provision a remote Mac as a reliable headless server with dev tools and
security hardening. Outputs an idempotent bash script on the target machine.
## Workflow
### 0. SSH MCP Server Setup
Before anything else, configure an SSH MCP server so Claude Code can
execute commands on the remote Mac. Add to `~/.mcp.json` on the
operator's local machine:
```json
{
"mcpServers": {
"ssh-<name>": {
"command": "npm",
"args": [
"exec", "ssh-mcp", "--",
"--host=<tailscale-ip-or-hostname>",
"--port=22",
"--user=<username>",
"--password=<password>"
]
}
}
}
```
For key-based auth (after SSH hardening), replace `--password` with:
```json
"--privateKeyPath", "/home/<user>/.ssh/id_ed25519"
```
Then enable in `~/.claude/settings.local.json`:
```json
{
"enableAllProjectMcpServers": true
}
```
The `ssh-mcp` package is from npm (`npm exec ssh-mcp`). It provides
`exec` and `sudo-exec` tools. Note: `sudo-exec` requires either
passwordless sudo on the remote machine or won't work.
To enable passwordless sudo on the Mac (needed for hardening):
```
sudo visudo -f /etc/sudoers.d/<username>
```
Add: `<username> ALL=(ALL) NOPASSWD: ALL`
**Important**: the MCP server has a ~1000 char command length limit.
Write long scripts in chunks using `cat >>` with heredocs.
### 1. Recon
Gather remote machine state before writing anything. Run all commands in
[references/recon-commands.md](references/recon-commands.md) and report
findings to inform decisions.
### 2. Elicit Configuration
Ask the user:
- **Hostname** — what to name the machine
- **Wi-Fi** — keep or disable?
- **File sharing** — remove SMB or keep with auth only?
- **SSH** — password-only? Set up key auth?
- **Nvim config** — clone from Gitea? Custom repo URL?
- **Git repos** — GitHub org/account for ~/.agents, ~/.<client>, ~/.config/nvim?
- **Extra packages** — beyond standard set?
### 3. Generate Setup Script
A reference script is bundled at
[scripts/setup-and-harden.sh](scripts/setup-and-harden.sh). Copy it to
the remote machine and customize hostname, paths, and SMB share name
before running. The script is idempotent — safe to re-run. Structure:
**Part 1 — Dev environment** (details: [references/dev-setup.md](references/dev-setup.md))
- Homebrew PATH in `.zprofile` (idempotent)
- Packages: neovim, tmux, git, starship, gh, node (brew), bun (curl)
- Git identity (`git config --global`) + gh credential helper
- Nvim config clone + config.json + dotfile symlinks
- Nvim plugin sync via `nvim --headless "+Lazy! sync" +qa`
- Shell aliases + starship init in .zshrc (idempotent)
**Part 2 — Server hardening** (details: [references/hardening.md](references/hardening.md))
- Power: no sleep, auto-restart on power loss
- App firewall: on, allow signed, stealth mode
- SMB: disable guest access
- Consumer services: disable 18+ via `launchctl disable gui/$UID/<label>`
(Siri, Photos, Games, News, Weather, Tips, Maps, Find My, Home, iTunes)
- Hostname via `scutil`
- Spotlight indexing off
- Software auto-install deferred
- Screen Sharing (VNC) via ARD kickstart
- Visual effects disabled (Liquid Glass, transparency, animations)
**Part 3 — Git repos**
- Initialize and push `~/.<client>` (server config/scripts)
- Push `~/.agents` (signet identity) and `~/.config/nvim` if upstream set
- All repos use `upstream` as remote name
### 4. SSH Key Auth
Set up key-based SSH early to enable rsync file transfers.
Can be done via MCP (no interactive step needed):
1. Read operator's pubkey (`~/.ssh/id_ed25519.pub`)
2. Via MCP exec: `mkdir -p ~/.ssh && chmod 700 ~/.ssh`
3. Append pubkey to `~/.ssh/authorized_keys` (chmod 600)
4. Verify: `ssh -o BatchMode=yes <host> echo ok` from local
Password auth can stay enabled — key auth just needs to work
so rsync is available for file transfers.
To optionally harden SSH later (disable password auth):
1. Verify key login works first
2. Edit `/etc/ssh/sshd_config`: `PasswordAuthentication no`,
`PermitRootLogin no`, `AcceptEnv TERM`
3. `sudo launchctl kickstart -k system/com.openssh.sshd`
### 5. Local SSH Config
On the operator's machine, add to `~/.ssh/config`:
```
Host <alias>
HostName <ip>
User <user>
SetEnv TERM=xterm-256color
```
The `SetEnv TERM` fixes kitty terminal + tmux over SSH.
### 6. Client Documentation
Write a README.md and CHANGELOG.md in the client directory
(`~/.<client>/`). These are for the nontechnical client — keep
language plain, explain the *why* not the *how*, and avoid
exposing implementation details. Write locally, rsync over:
```
rsync -av /tmp/readme.md <host>:~/.<client>/README.md
```
README covers: what the server is, current state, what's next,
who to contact. CHANGELOG is a dated record in plain language
of each setup session.
### 7. Verify
See [references/verification.md](references/verification.md) for the
full checklist.
## Tips
- **tmux visibility**: If the user has a tmux session open on the remote
Mac, send commands to it via `tmux send-keys` so they can watch
progress in real time. Prefix with brew shellenv since MCP runs a
non-login shell:
```
eval "$(/opt/homebrew/bin/brew shellenv)" && tmux send-keys -t 0 '<command>' Enter
```
This gives the user live visibility into what you're doing on their
machine. Use it for key moments (script execution, service restarts,
verification commands) rather than every single command.
## Key Constraints
- SSH MCP servers typically can't sudo — generate script, user runs it
- `launchctl disable gui/$UID/<label>` is SIP-safe and persists reboots
- Never disable SIP or FileVault
- Keep software update auto-check, just defer auto-install
- Add `set -ga terminal-overrides ",*:Tc,*:kbs=\177"` to tmux.conf for
backspace fix over SSH
- MCP command length limit (~1000 chars) — for short content, write
in chunks using `cat >>` with heredocs. For larger files (README,
docs, configs), write locally and rsync over SSH instead.
- **SSH key auth first**: Set up SSH key auth early (before disabling
password auth) so rsync works from the operator's machine. Add the
operator's pubkey to `~/.ssh/authorized_keys` via MCP exec, then
verify with `ssh -o BatchMode=yes <host> echo ok`. This unlocks
rsync for file transfer, which is vastly better than chunked
heredocs through MCP.
## Gotchas (learned the hard way)
- **Non-login shell PATH**: SSH MCP runs a non-login shell, so
`/opt/homebrew/bin` is not on PATH. Always prefix commands with
`eval "$(/opt/homebrew/bin/brew shellenv)"` when running brew-installed
tools (tmux, gh, starship, etc.) via MCP.
- **Unicode curly quotes in share names**: macOS uses `'` (U+2019)
not `'` in default share names like "Mac's Public Folder". Never
hardcode share names — parse dynamically from `sharing -l` output.
- **`sharing -r` quoting**: Even with correct quotes, MCP command
piping can mangle special characters. Safest approach:
`sharing -l | grep "^name:" | sed 's/name:[[:space:]]*//' | while read -r name; do sudo sharing -r "$name"; done`
- **VNC `-specifiedUsers` breaks naprivs**: Always use
`-allowAccessFor -allUsers` with ARD kickstart. `-specifiedUsers`
sets naprivs to -2147483648, causing auth failures that are hard
to debug. Must also set VNC legacy mode with explicit password.
- **`launchctl print-disabled` output**: Services show as
`"label" => disabled`, NOT `true`. Use `grep -c disabled` to count,
not `grep -c true`.
- **tmux send-keys quoting**: When piping commands through
`tmux send-keys`, apostrophes and special chars in arguments need
careful escaping. Prefer simple commands or use MCP exec directly
for complex operations.
- **HTTPS git push on headless Mac**: Fails with "could not read
Username: Device not configured". Fix: `gh auth login` then
`gh auth setup-git` to install the credential helper.