369 lines
15 KiB
Markdown
369 lines
15 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, local SSH config, Signet
|
|
agent platform (install, launchd, tailnet binding), and OpenClaw
|
|
agent runtime (auth profiles, gateway 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. Signet Agent Platform
|
|
|
|
Signet is a portable agent identity system — persistent memory,
|
|
secrets vault, installable skills, and a web dashboard. The npm
|
|
package is `signetai`. Full install guide: https://signetai.sh/skill.md
|
|
|
|
**What Signet provides:**
|
|
- Background daemon (port 3850) with memory database, context injection,
|
|
and session extraction pipeline
|
|
- Web dashboard for browsing memories, config, secrets, and status
|
|
- Encrypted secrets vault at `~/.agents/.secrets/`
|
|
- Built-in skills: `/remember`, `/recall`, `/memory-debug`
|
|
- Platform connectors for Claude Code, OpenClaw, and OpenCode
|
|
|
|
**All agent data lives at `~/.agents/`:**
|
|
- `agent.yaml` — config manifest
|
|
- `AGENTS.md` — operational instructions
|
|
- `SOUL.md`, `IDENTITY.md`, `USER.md` — identity files
|
|
- `MEMORY.md` — auto-generated working memory summary
|
|
- `memory/memories.db` — SQLite database (source of truth)
|
|
- `skills/` — installed skills
|
|
- `.secrets/` — encrypted secret store
|
|
|
|
**Install steps:**
|
|
|
|
1. **Prerequisites**: Node.js >= 18 or Bun. If neither exists, install bun:
|
|
`curl -fsSL https://bun.sh/install | bash`
|
|
2. **Install signetai globally** (only two supported methods):
|
|
- Bun (preferred): `bun add -g signetai`
|
|
- npm: `npm install -g signetai`
|
|
- Never use sudo. Never clone the repo. Never use `npx signet init`.
|
|
3. **Install node runtime**: The bin shim uses `#!/usr/bin/env node`,
|
|
so node must be available even if bun is the primary runtime. If brew
|
|
isn't installed, download the node binary directly:
|
|
```
|
|
curl -fsSL https://nodejs.org/dist/v22.14.0/node-v22.14.0-darwin-arm64.tar.xz -o /tmp/node.tar.xz
|
|
tar xf /tmp/node.tar.xz -C /tmp/
|
|
cp /tmp/node-v22.14.0-darwin-arm64/bin/node ~/.local/bin/node
|
|
```
|
|
4. **Verify install**: `signet --version` must succeed before proceeding.
|
|
5. **Run setup wizard**: `signet` (no args) on first run launches the
|
|
interactive wizard. It handles connectors, hooks, file generation,
|
|
and skill deployment. Do NOT try to set these up manually.
|
|
6. **Bind to tailnet**: By default the daemon binds to `localhost`. Set
|
|
`SIGNET_HOST=0.0.0.0` in `.zshrc` so the dashboard is accessible
|
|
across the tailnet.
|
|
7. **Create launchd plist** at `~/Library/LaunchAgents/ai.signet.daemon.plist`:
|
|
- ProgramArguments: `~/.bun/bin/bun` + `~/.bun/install/global/node_modules/signetai/dist/daemon.js`
|
|
- EnvironmentVariables: `SIGNET_PORT=3850`, `SIGNET_HOST=0.0.0.0`,
|
|
`SIGNET_PATH=~/.agents`, PATH including `~/.bun/bin` and `~/.local/bin`
|
|
- RunAtLoad: true, KeepAlive: true
|
|
- Logs to `~/.agents/logs/daemon.{out,err}.log`
|
|
8. **Load**: `launchctl load ~/Library/LaunchAgents/ai.signet.daemon.plist`
|
|
9. **Verify**: `lsof -i :3850 -P` should show `TCP *:3850 (LISTEN)`,
|
|
and `curl http://<tailscale-ip>:3850/api/status` from operator machine.
|
|
|
|
**Important — what the daemon does automatically (do NOT replicate):**
|
|
- Extracts memories from session transcripts via LLM pipeline
|
|
- Injects relevant context into every prompt via semantic search
|
|
- Watches `~/.agents/` for changes and syncs to harness configs
|
|
- Do NOT manually write to `~/.agents/memory/`, call recall before
|
|
every response, or manually summarize conversations
|
|
|
|
**Troubleshooting:**
|
|
- Daemon won't start: `signet daemon logs`, `lsof -i :3850`
|
|
- No memories: daemon may still be processing — extraction is async
|
|
- Embeddings: Ollama is optional, falls back to keyword search (FTS5)
|
|
- Skills not found: `signet sync` reinstalls built-in templates
|
|
|
|
Note: with KeepAlive enabled, `signet stop` won't work — launchd respawns
|
|
the process. Use `launchctl unload` to fully stop.
|
|
|
|
### 8. OpenClaw Agent Runtime
|
|
|
|
Install and configure OpenClaw for agent operation:
|
|
|
|
1. **Install**: OpenClaw is typically installed via Homebrew
|
|
(`/opt/homebrew/bin/openclaw`).
|
|
2. **Configure**: Run `openclaw configure` for interactive setup, or
|
|
edit `~/.openclaw/openclaw.json` directly.
|
|
3. **Auth setup (non-interactive)**: The `openclaw models auth paste-token`
|
|
command uses interactive prompts that don't work through MCP. Instead,
|
|
write files directly:
|
|
- Write `~/.openclaw/agents/main/agent/auth-profiles.json`:
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"profiles": {
|
|
"anthropic:manual": {
|
|
"type": "token",
|
|
"provider": "anthropic",
|
|
"token": "<oauth-token>"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
- Add auth profile to `~/.openclaw/openclaw.json` under `auth.profiles`:
|
|
```json
|
|
"auth": {
|
|
"profiles": {
|
|
"anthropic:manual": {
|
|
"provider": "anthropic",
|
|
"mode": "token"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
- Verify: `openclaw models status` should show the profile.
|
|
4. **Gateway**: OpenClaw manages its own LaunchAgent
|
|
(`ai.openclaw.gateway`). Restart with `openclaw gateway restart`.
|
|
Check health with `openclaw health`.
|
|
5. **Talk to it**: `openclaw agent --agent main --session-id <name> --message "hello"`
|
|
|
|
### 9. 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.
|
|
- **Heredoc descriptions leak into content**: When using `cat >> file << 'DELIM'`
|
|
through SSH MCP, the tool's `description` parameter text can get appended
|
|
to the delimiter line (e.g. `DELIM # Write part 1`), corrupting the file.
|
|
Use `python3 -c` with string concatenation instead for multi-part file
|
|
writes — it's immune to this issue.
|
|
- **bun global binaries need PATH in non-login shells**: SSH MCP doesn't
|
|
source `.zshrc`, so `~/.bun/bin` isn't on PATH. Always export it:
|
|
`export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$HOME/.local/bin:$PATH"`
|
|
- **signet bin shim needs node**: Even though `signetai` runs on bun,
|
|
the npm bin shim (`bin/signet.js`) has `#!/usr/bin/env node`. Install
|
|
node alongside bun or the CLI won't start. A bare node binary in
|
|
`~/.local/bin` is sufficient.
|
|
- **Tailscale CLI vs app**: On macOS, Tailscale.app installs but the
|
|
`tailscale` CLI may not be in PATH. The binary lives at
|
|
`/Applications/Tailscale.app/Contents/MacOS/Tailscale`. `tailscale status`
|
|
works from there without needing brew or PATH changes.
|
|
- **OpenClaw interactive commands through MCP**: Commands like
|
|
`openclaw models auth paste-token` and `openclaw configure` use
|
|
`@clack/prompts` which require a TTY. Piping stdin doesn't bypass
|
|
the prompts cleanly. Write config files directly instead.
|
|
- **signet start hangs MCP**: `signet start` blocks until the daemon
|
|
is fully running, which can exceed the MCP SSH timeout. Background it
|
|
with `& disown` or just let it timeout — check `signet status` after
|
|
to confirm it started.
|