Machine record
Fill this in as you go — it is what you will want six months from now. Saved in this browser only; nothing leaves the page.
0Preflight
Ten minutes of checks that stop you discovering a problem two hours in.
-
Run all four.
hw.memsizemust print 24; if it prints 16 or 32, the tuning numbers in phase 3 are wrong for this box and need rescaling.Terminal sw_vers sysctl -n hw.memsize | awk '{printf "RAM: %.0f GB\n", $1/1073741824}' system_profiler SPHardwareDataType | awk -F': ' '/Model Name|Chip|Total Number of Cores|Serial Number/ {print $1": "$2}' df -h / | awk 'NR==2 {print "Free disk: "$4" of "$2}'GateKeep at least 60 GB free. Homebrew, Postgres, Node and the repo want ~8 GB; the rest is headroom for logs, Time Machine local snapshots, and — if you go that way — local model weights at 5–10 GB each. Under 60 GB, plan on a cloud boss model only.
-
All three names, or Bonjour and the shell prompt disagree with each other forever.
Terminal sudo scutil --set ComputerName hudson-mini sudo scutil --set HostName hudson-mini sudo scutil --set LocalHostName hudson-mini # reachable afterwards at hudson-mini.local -
Use one dedicated macOS account for the whole stack — call it
hudson— not your daily-driver login. Everything in this sheet installs into that account's home directory:~/.hermes,~/repos/project-hudson, the Homebrew services, the Claude Code credentials.It needs administrator rights during install (Homebrew,
pmset,scutil). It is the account that will be auto-logged-in in step 1.2, so treat it as a service account: no personal iCloud, no browser sessions, no other secrets. -
System Settings → General → Sharing → enable Remote Login (SSH) and Screen Sharing. The GUI toggle is the reliable path; the CLI equivalent needs Full Disk Access granted to Terminal on current macOS:
Terminal sudo systemsetup -setremotelogin on sudo systemsetup -getremoteloginThen prove it from your laptop before the mini goes headless:
Laptop ssh hudson@hudson-mini.local -
Ethernet if the port is anywhere near it — a Wi-Fi drop is the most common cause of a bot that "just stopped answering". Add a DHCP reservation on the router so the address survives reboots, and record it in the machine record above.
1Make macOS behave like a server
macOS defaults assume someone is sitting in front of it. Three settings and one honest trade-off fix that.
-
Terminal sudo pmset -a sleep 0 disksleep 0 displaysleep 15 sudo pmset -a womp 1 autorestart 1 pmset -g # verify: sleep 0, disksleep 0, womp 1, autorestart 1Display sleep is fine to leave on — it does not suspend the daemon, and a headless mini has no display to speak of.
autorestart 1is the one that matters after a building power blip. -
Both halves of this stack start as LaunchAgents:
hermes gateway installregisters one, andbrew services startregisters one for Postgres. A LaunchAgent only runs inside a logged-in user session. With FileVault on and no auto-login, a reboot leaves the disk locked at the login window — and the bot is silent until a human types a password.Option Boots back unattended? What you accept FileVault off + automatic login
recommended for a physically secure locationYes Disk is readable to anyone who walks off with the mini — and it holds a bot token, API keys, and a Claude session. FileVault on, no auto-login No Every reboot and power cut needs a person at a screen. Use sudo fdesetup authrestartfor planned reboots — it unlocks once on next boot — but power loss still parks it.FileVault on + a UPS Mostly Removes the power-cut case, not the crash case. The pragmatic middle if the mini lives somewhere shared. Terminal fdesetup status # auto-login lives in System Settings → Users & Groups → Automatic login # (the menu is greyed out while FileVault is enabled) sudo fdesetup authrestart # planned reboot that unlocks itself onceWrite the choice into the machine record. Every "the bot went quiet overnight" report traces back to this line.
-
System Settings → General → Software Update → Automatic Updates:
- Keep on: "Install Security Responses and system files" — no reboot, real protection.
- Turn off: "Install macOS updates" and "Install application updates from the App Store".
An unattended OS upgrade reboots the machine, and — with FileVault on — leaves it at the login window. Do updates on your schedule, then re-run the reboot test in phase 6.
Terminal softwareupdate --list # see what's pending, on your terms -
An external SSD or a NAS share. It is not a substitute for the repo-native bundle in step 6.4 — Time Machine gives you the machine back,
pack-for-move.shgives you the agent back — but on an always-on box that will accumulate state, you want both.
2Base tooling
Xcode command line tools, Homebrew, the runtimes, and the Claude Code login.
-
Terminal xcode-select --install # GUI installer; skip if already present xcode-select -p # expect /Library/Developer/CommandLineTools -
Apple silicon installs to
/opt/homebrew, which is not on the default PATH. Theshellenvline is what makes it stick for future sessions — including the ones launchd will start.Terminal /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile eval "$(/opt/homebrew/bin/brew shellenv)" brew --version -
Terminal brew install node python ripgrep ffmpeg git uvnoderuns the Claude Code CLI ·uvruns the Postgres MCP server viauvx·ripgrepandffmpegare Hermes dependencies.Skip Docker on this boxThe native Postgres path in phase 3 is the default for the mini precisely so you do not pay for Docker Desktop's Linux VM — it reserves multiple gigabytes of your 24 up front and adds a second thing that must survive reboot. Install Docker only if you have another reason to.
-
This login is what the 17 specialists run on, no matter which model you later give the Hermes boss layer.
Terminal npm install -g @anthropic-ai/claude-code claude # log in, then /exit claude doctor # expect a clean install report
3The repo and the memory database
Cloning is the install. The only real work here is tuning Postgres for 24 GB — the Homebrew defaults are sized for a laptop that also runs a browser.
-
Terminal mkdir -p ~/repos git clone https://github.com/lciamp/project-hudson.git ~/repos/project-hudson cd ~/repos/project-hudsonThe 17 subagents in
.claude/agents/and the 60 skills in.claude/skills/register with Claude Code automatically — there is nothing else to install for the team itself. -
~/repos/project-hudson scripts/setup-native-postgres.shIt installs
postgresql@18+pgvector, starts the service, rewritespg_hba.confso TCP connections needscram-sha-256, creates theproject_hudsonrole and database, appliesdb/init/01-schema.sql, and then proves the hardening by checking that a passwordless TCP connection is refused on both127.0.0.1and::1. It is idempotent, and it refuses to run if anything else already holds port 5432. -
Homebrew ships
shared_buffers = 128MBandmaintenance_work_mem = 64MB— enough to make HNSW index builds crawl. Back up the config, append the block, restart.Terminal · locate and back up PG=/opt/homebrew/opt/postgresql@18/bin CONF=$("$PG/psql" -d postgres -tAc "SHOW config_file") echo "$CONF" cp "$CONF" "$CONF.bak-$(date +%F)"Terminal · append the tuning cat >> "$CONF" <<'EOF' # ── project-hudson · Mac mini, 24 GB unified memory ────────── shared_buffers = 2GB effective_cache_size = 6GB work_mem = 32MB maintenance_work_mem = 1GB max_connections = 40 max_wal_size = 2GB min_wal_size = 512MB checkpoint_completion_target = 0.9 random_page_cost = 1.1 effective_io_concurrency = 200 max_worker_processes = 8 max_parallel_workers = 4 max_parallel_workers_per_gather = 2 max_parallel_maintenance_workers = 2 jit = off hnsw.ef_search = 100 log_min_duration_statement = 500ms EOF brew services restart postgresql@18Terminal · verify it took "$PG/psql" -d project_hudson \ -c "SHOW shared_buffers" -c "SHOW work_mem" \ -c "SHOW maintenance_work_mem" -c "SHOW jit"Every number and the reasoning behind it is in the settings reference below. Short version: this box is not a database server — it is a database plus a gateway plus however many Node processes Claude Code spawns, so Postgres deliberately takes about 8% of RAM rather than the textbook 25%.
If Postgres refuses to startRestore the backup and restart:
cp "$CONF.bak-$(date +%F)" "$CONF" && brew services restart postgresql@18. Then re-add lines a few at a time. The log is at/opt/homebrew/var/log/postgresql@18.log. -
Run
claudeonce inside the repo and approve thepostgresserver from.mcp.json. This also caches the workspace-trust prompt for the directory, which is what lets Hermes-spawned headless sessions run without stalling on a dialog nobody is there to answer.~/repos/project-hudson claude # approve the postgres MCP server when prompted, then: # /mcp → postgres should read "connected" # /exit -
Terminal PGPASSWORD=project_hudson_dev /opt/homebrew/opt/postgresql@18/bin/psql \ -h localhost -U project_hudson -d project_hudson \ -c "SELECT extversion FROM pg_extension WHERE extname='vector'" \ -c "SELECT count(*) FROM memories"A pgvector version and a row count (
0on a fresh install) means the whole path works: TCP, password auth, extension, schema. -
Only if this mini is replacing an existing setup. Unzip the bundle from
scripts/pack-for-move.shand follow itsRESTORE.md: drop the repo in place, restore the Claude auto-memory folder, fix the absolute paths inside.claude/settings.local.json, then load the rows.Bundle directory /opt/homebrew/opt/postgresql@18/bin/psql \ 'postgresql://project_hudson:project_hudson_dev@localhost:5432/project_hudson' \ < memories-dump.sql
4Hermes, the always-on boss
The Telegram-facing daemon. Everything up to here works without it — claude in the repo is already the boss. This is what puts it on your phone.
-
The installer is curl-pipe-bash. Download it, read it, then run it — on a machine that is about to hold your bot token and API keys, that is not paranoia.
Terminal curl -fsSL https://hermes-agent.nousresearch.com/install.sh -o /tmp/hermes-install.sh less /tmp/hermes-install.sh # read before running bash /tmp/hermes-install.sh source ~/.zshrc hermes --version # record this — pin it, don't auto-updateConfig lands in
~/.hermes/config.yaml, secrets in~/.hermes/.env. -
Terminal hermes model # interactive picker # Claude Max subscription (no per-token billing; Pro is not eligible): hermes auth add anthropic --type oauthOnly the boss layer runs on this model — the 17 specialists always run on Claude via the
claudelogin from phase 2. Qwen, OpenAI, DeepSeek, OpenRouter and Bedrock all work here.On 24 GB, prefer a hosted boss modelA local model is the one thing that can genuinely starve this box. If you want one anyway, the sizing table gives the ceiling: roughly 14 B at Q4, and not while a heavy Claude Code session is running.
-
- Message @BotFather →
/newbot→ copy the token. - Message @userinfobot → copy your numeric user ID.
- Write both into
~/.hermes/.env, then lock the file down.
Terminal cat >> ~/.hermes/.env <<'EOF' TELEGRAM_BOT_TOKEN=<token from BotFather> TELEGRAM_ALLOWED_USERS=<your numeric user ID> EOF chmod 600 ~/.hermes/.envNever run allow-allAlways set
TELEGRAM_ALLOWED_USERS, and neverGATEWAY_ALLOW_ALL_USERS=true— a documented fail-open left gateways accepting DMs from any Telegram account. Admit anyone else with Hermes pairing codes, which expire after an hour. The full Telegram walkthrough is in the Telegram setup guide. - Message @BotFather →
-
Terminal hermes gateway setup # wizard hermes gateway start hermes gateway status # logs: ~/.hermes/logs/gateway.log hermes gateway install # LaunchAgent — starts at loginlaunchd quirks worth knowing now- Never use a bare
restart— there is a race. Usehermes gateway stop && sleep 5 && hermes gateway start. - Exit-78 config errors can wedge launchd.
- On macOS 26+ it can silently fall back to a detached process with no auto-start and no crash restart. Check
hermes gateway statusafter every reboot. - One bot token per gateway — Telegram rejects concurrent polling, so stop the laptop's instance if you have one.
- Never use a bare
-
Send this as your first message to the bot, so it lands in Hermes's own memory. Substitute the real account name.
Send to your bot For any project-hudson work, use the claude-code skill with workdir /Users/hudson/repos/project-hudson, print mode, and resume sessions rather than starting new ones for follow-ups. Don't answer project-hudson questions from your own tools.Without this you get two competing orchestrators, and the gates that live in
CLAUDE.md— security review after auth or infra changes, chaos validation for failover claims, the approval boundaries — get quietly skipped.
5Permissions and hardening
This machine is an internet-reachable agent with shell access. Ten minutes here is cheap.
-
A headless session cannot answer a permission prompt — it just stops. Commit the allow rules so every Hermes-spawned session inherits them.
.claude/settings.json { "permissions": { "allow": [ "Read", "Edit", "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)" ] } }Scope Bash to exact subcommandsNever write
Bash(git *).gitruns arbitrary programs through-c core.pager=…,-c core.sshCommand=…and aliases, so a "git-only" wildcard is full shell access to anyone who can prompt-inject the bot. -
Always
--max-turnsand--max-budget-usd. Never--dangerously-skip-permissionsorbypassPermissionson this box — the approval boundaries inAGENTS.md(no liveterraform apply, no destructive SQL, no paging real humans) only bind while enforcement is on. -
Terminal lsof -nP -iTCP:5432 -sTCP:LISTENExpect
127.0.0.1:5432and[::1]:5432— never*:5432. Theproject_hudson:project_hudson_devcredential is local-development-only and stays acceptable exactly as long as that holds. If this database ever needs to be reachable off-box, the credential comes from Vault or AWS Secrets Manager instead, never from a file in the repo. -
chmod 600 ~/.hermes/.env— and keep the bot token out of the repo. If it leaks, revoke it in BotFather immediately.- Turn the macOS firewall on (System Settings → Network → Firewall) and allow only Remote Login and Screen Sharing.
- Do not add the bot to public groups. Every message it can read is prompt-injection surface for an agent that can run shell commands.
- Ignore third-party "auth bypass" add-ons — that whole category is a credential-theft vector. Official Max OAuth covers it.
6Prove it, then walk away
An always-on box is only as good as its last reboot test. Do these four before you trust it unattended.
-
On the mini, then from your laptop sudo shutdown -r now # wait ~90s, then from the laptop: ssh hudson@hudson-mini.local 'hermes gateway status; brew services list | grep postgres'Expect the gateway running and
postgresql@18started. If both are down, you are looking at the phase 1.2 trade-off: LaunchAgents need a logged-in session, and without auto-login there isn't one. -
- Ask the bot: "in the project-hudson repo, list your agents" → expect the 17 specialists.
- Send a routed task: "find 5 product ideas in the dev-tools niche" → it should dispatch into Claude Code and come back through
product-researcherand themarket-niche-scanskill.
If the answer looks like Hermes improvising rather than the team, the thin-relay instruction from step 4.5 did not stick — send it again.
-
While that routed task is actually running, on the mini:
Terminal memory_pressure | head -5 sysctl vm.swapusage top -l 1 -o mem -n 12 -stats pid,command,memHealthy on 24 GB: memory pressure "normal", swap used in the low hundreds of MB or less. Sustained swap in the gigabytes means something is oversized — usually a local model, occasionally
shared_buffersif you raised it past 2 GB. Compare against the budget. -
~/repos/project-hudson scripts/pack-for-move.sh ~/DesktopRepo + Claude auto-memory + a dump of the memories table + a generated
RESTORE.md, in one zip. Copy it off the machine. This is the artifact that makes the mini replaceable. -
Optional, and worth it once the memory store has real rows in it. Write the plist, then load it — nothing installs itself.
~/Library/LaunchAgents/com.hudson.pgdump.plist <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key><string>com.hudson.pgdump</string> <key>ProgramArguments</key> <array> <string>/bin/sh</string> <string>-c</string> <string>PGPASSWORD=project_hudson_dev /opt/homebrew/opt/postgresql@18/bin/pg_dump -h localhost -U project_hudson --clean --if-exists project_hudson > "$HOME/backups/memories-$(date +%F).sql" && find "$HOME/backups" -name 'memories-*.sql' -mtime +14 -delete</string> </array> <key>StartCalendarInterval</key> <dict><key>Hour</key><integer>3</integer><key>Minute</key><integer>30</integer></dict> <key>StandardErrorPath</key><string>/tmp/hudson-pgdump.err</string> </dict> </plist>Terminal · install and test mkdir -p ~/backups launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.hudson.pgdump.plist launchctl kickstart -k gui/$(id -u)/com.hudson.pgdump # run it once, now ls -lh ~/backupsSame LaunchAgent caveat as everything else here: it runs at login, keeps 14 days, and writes errors to
/tmp/hudson-pgdump.err.
24 GB memory budget
What the box actually spends, so you can tell "busy" from "oversubscribed" at a glance. Peaks are what you see while a routed task is mid-flight.
| Component | Steady | Peak | Notes |
|---|---|---|---|
| macOS + system services | ~4 GB | ~5 GB | Spotlight indexing after a big clone is a temporary spike. |
Postgres 18 (shared_buffers 2 GB + backends) | ~2.5 GB | ~3.5 GB | Peak is an HNSW index rebuild pulling on maintenance_work_mem. |
| Hermes gateway | ~0.6 GB | ~1 GB | Idle polling is cheap. |
| Claude Code headless session — each | ~0.8 GB | ~1.5 GB | Budget for two concurrent. This is the number that surprises people. |
| File cache and burst headroom | — | ~4 GB | macOS will use whatever is free; that is correct, not a leak. |
| Left for a local model | — | ~10 GB | And only while nothing else peaks at once. |
Postgres settings, and why each one
The workload is small and read-heavy: an agent-memory table with 1536-dimension embeddings, an HNSW index, and hybrid keyword search — a few thousand to a few hundred thousand rows, queried a handful of times per task. That shape, not the RAM total, is what these values follow.
| Setting | Value | Why this, here |
|---|---|---|
| shared_buffers | 2GB | ~8% of RAM, not the textbook 25%. The entire memory store fits comfortably, and the other 22 GB has claimants. |
| effective_cache_size | 6GB | A planner hint, not an allocation — tells it macOS's unified page cache is holding the rest. |
| work_mem | 32MB | Per sort/hash node, so it multiplies by concurrency. Safe because connections are few. |
| maintenance_work_mem | 1GB | The one that matters for pgvector: HNSW builds are dramatically faster with room to work. |
| max_connections | 40 | Agent sessions number in the handful. Each idle backend still costs memory. |
| random_page_cost | 1.1 | NVMe. The default 4.0 assumes a spinning disk and pushes the planner off indexes. |
| effective_io_concurrency | 200 | Same reason. |
| jit | off | JIT compilation costs more than it saves on short vector lookups. |
| hnsw.ef_search | 100 | pgvector's recall/latency dial (default 40). Raise toward 200 if retrieval misses things; it costs latency, not memory. |
| log_min_duration_statement | 500ms | Cheap insurance — you will want the slow query when retrieval starts feeling sluggish. |
Modern Postgres uses POSIX shared memory, so no kern.sysv.shmmax tinkering is needed for a 2 GB shared_buffers. And hnsw.ef_search is a qualified custom setting — Postgres accepts it at startup as a placeholder and pgvector picks it up when the extension loads.
db/init/01-schema.sql runs only on an empty database. Any later change to the memories table is a migration — the pg-safe-migration skill — never an edit to db/init/ alone.
Local model sizing on 24 GB
Only relevant if you want the Hermes boss layer running locally via Ollama, vLLM or LM Studio. The specialists are unaffected — they run on Claude regardless. Weights below are roughly Q4_K_M; add KV cache on top, which grows with context (a 32k window on a 14 B model is another ~2 GB).
| Model class | Weights | Verdict on this box |
|---|---|---|
| 7–8 B | ~5 GB | Comfortable |
| 12–14 B | ~9 GB | The practical ceiling — fine alongside Postgres, tight with two Claude Code sessions. |
| 24–27 B | ~16 GB | Only with everything else idle. Expect swap the moment a task runs. |
| 32 B and up | 20 GB+ | No. Not on a box that also serves a database and spawns Node processes. |
sudo sysctl iogpu.wired_limit_mb=… lets the GPU claim more of the unified pool than macOS's default share. On a dedicated inference box that is a reasonable knob; here it starves Postgres and the gateway, and it does not survive a reboot. Pick a smaller model instead.
Troubleshooting, Mac mini edition
| Symptom | Cause and fix |
|---|---|
| Bot and database both dead after a power cut | LaunchAgents need a logged-in session. FileVault on without auto-login parks the machine at the login window — revisit step 1.2. |
| Bot silent after a reboot, database fine | launchd fell back to a detached process (macOS 26+). hermes gateway status, then start it manually. |
Bot silent right after gateway restart | The restart race. hermes gateway stop && sleep 5 && hermes gateway start. |
brew services shows postgresql@18 as none | The agent is registered per-user. Re-run brew services start postgresql@18 as the hudson account, not via sudo — Postgres refuses to run as root. |
| Setup script refuses: port 5432 in use | Something else holds it, usually a stray Docker container. docker compose down, or lsof -nP -iTCP:5432 -sTCP:LISTEN to identify it. |
Session ignores CLAUDE.md and the agents | The skill's workdir is not the repo root. |
| A task stops mid-way with nothing in the log | A permission prompt nobody answered. Add the tool to .claude/settings.json or --allowedTools. |
--resume fails | Resume has to run from the original workdir. |
| Machine swapping hard during ordinary tasks | Check for a local model still resident (ollama ps), then shared_buffers. Compare with the budget. |
| Two machines fighting over the bot | One token per gateway. Stop the other instance. |
Command card
| Command | What it does |
|---|---|
| hermes gateway status | Is the boss alive. First thing after any reboot. |
| tail -f ~/.hermes/logs/gateway.log | Watch the gateway in real time. |
| brew services list | Is Postgres running, and under which user. |
| claude doctor | Verify the Claude Code install and login. |
| /reload-plugins | Inside Claude Code — pick up agent or skill edits without a new session. |
| python3 scripts/sync-copilot-agents.py | Regenerate the Copilot agent files after editing any agent (--check for drift). |
| python3 scripts/gen-skills-readme.py | Regenerate the skills index after adding or re-describing a skill (--check for drift). |
| scripts/setup-native-postgres.sh | Idempotent — safe to re-run any time the database looks wrong. |
| scripts/pack-for-move.sh | Bundle repo + memory + DB dump for the next machine. |
| pmset -g | Confirm the never-sleep settings survived an OS update. |
| memory_pressure | head -5 | The honest answer to "is 24 GB enough today". |
Running the team without Hermes works too: claude inside the repo is the boss, per CLAUDE.md. The same agents and skills also load in GitHub Copilot — though a cloud-hosted Copilot agent can only reach a Neon database, never this local one.