230 lines
8.4 KiB
Markdown
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.
|