0 / 0 steps

lciamp / project-hudson · machine runbook

Mac mini 24 GB setup sheet

The hands-on file. Open this on the mini and work top to bottom — every command is copy-ready and every number is already sized for 24 GB of unified memory. Ticks are saved in this browser, so you can stop and resume. For the why behind any step, the install guide is the companion document.

TargetMac mini · 24 GBAlways-on, headless, wired
RunsHermes + Claude Code17 specialists · 60 skills
DatabasePostgres 18 + pgvectorNative Homebrew, localhost only
Time≈ 90 minutesPlus one reboot test

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.

  1. Run all four. hw.memsize must 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}'
    Gate

    Keep 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.

  2. 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
  3. 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.

  4. 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 -getremotelogin

    Then prove it from your laptop before the mini goes headless:

    Laptop
    ssh hudson@hudson-mini.local
  5. 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.

  1. 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 1

    Display sleep is fine to leave on — it does not suspend the daemon, and a headless mini has no display to speak of. autorestart 1 is the one that matters after a building power blip.

  2. Both halves of this stack start as LaunchAgents: hermes gateway install registers one, and brew services start registers 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.

    OptionBoots back unattended?What you accept
    FileVault off + automatic login
    recommended for a physically secure location
    YesDisk 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-loginNoEvery reboot and power cut needs a person at a screen. Use sudo fdesetup authrestart for planned reboots — it unlocks once on next boot — but power loss still parks it.
    FileVault on + a UPSMostlyRemoves 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 once

    Write the choice into the machine record. Every "the bot went quiet overnight" report traces back to this line.

  3. 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
  4. 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.sh gives 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.

  1. Terminal
    xcode-select --install   # GUI installer; skip if already present
    xcode-select -p          # expect /Library/Developer/CommandLineTools
  2. Apple silicon installs to /opt/homebrew, which is not on the default PATH. The shellenv line 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
  3. Terminal
    brew install node python ripgrep ffmpeg git uv

    node runs the Claude Code CLI · uv runs the Postgres MCP server via uvx · ripgrep and ffmpeg are Hermes dependencies.

    Skip Docker on this box

    The 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.

  4. 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.

  1. Terminal
    mkdir -p ~/repos
    git clone https://github.com/lciamp/project-hudson.git ~/repos/project-hudson
    cd ~/repos/project-hudson

    The 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.

  2. ~/repos/project-hudson
    scripts/setup-native-postgres.sh

    It installs postgresql@18 + pgvector, starts the service, rewrites pg_hba.conf so TCP connections need scram-sha-256, creates the project_hudson role and database, applies db/init/01-schema.sql, and then proves the hardening by checking that a passwordless TCP connection is refused on both 127.0.0.1 and ::1. It is idempotent, and it refuses to run if anything else already holds port 5432.

  3. Homebrew ships shared_buffers = 128MB and maintenance_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@18
    Terminal · 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 start

    Restore 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.

  4. Run claude once inside the repo and approve the postgres server 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
  5. 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 (0 on a fresh install) means the whole path works: TCP, password auth, extension, schema.

  6. Only if this mini is replacing an existing setup. Unzip the bundle from scripts/pack-for-move.sh and follow its RESTORE.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.

  1. 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-update

    Config lands in ~/.hermes/config.yaml, secrets in ~/.hermes/.env.

  2. Terminal
    hermes model   # interactive picker
    # Claude Max subscription (no per-token billing; Pro is not eligible):
    hermes auth add anthropic --type oauth

    Only the boss layer runs on this model — the 17 specialists always run on Claude via the claude login from phase 2. Qwen, OpenAI, DeepSeek, OpenRouter and Bedrock all work here.

    On 24 GB, prefer a hosted boss model

    A 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.

    1. Message @BotFather/newbot → copy the token.
    2. Message @userinfobot → copy your numeric user ID.
    3. 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/.env
    Never run allow-all

    Always set TELEGRAM_ALLOWED_USERS, and never GATEWAY_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.

  3. Terminal
    hermes gateway setup     # wizard
    hermes gateway start
    hermes gateway status    # logs: ~/.hermes/logs/gateway.log
    hermes gateway install   # LaunchAgent — starts at login
    launchd quirks worth knowing now
    • Never use a bare restart — there is a race. Use hermes 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 status after every reboot.
    • One bot token per gateway — Telegram rejects concurrent polling, so stop the laptop's instance if you have one.
  4. 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.

  1. 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 subcommands

    Never write Bash(git *). git runs 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.

  2. Always --max-turns and --max-budget-usd. Never --dangerously-skip-permissions or bypassPermissions on this box — the approval boundaries in AGENTS.md (no live terraform apply, no destructive SQL, no paging real humans) only bind while enforcement is on.

  3. Terminal
    lsof -nP -iTCP:5432 -sTCP:LISTEN

    Expect 127.0.0.1:5432 and [::1]:5432 — never *:5432. The project_hudson:project_hudson_dev credential 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.

  1. 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@18 started. 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.

    1. Ask the bot: "in the project-hudson repo, list your agents" → expect the 17 specialists.
    2. Send a routed task: "find 5 product ideas in the dev-tools niche" → it should dispatch into Claude Code and come back through product-researcher and the market-niche-scan skill.

    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.

  2. 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,mem

    Healthy 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_buffers if you raised it past 2 GB. Compare against the budget.

  3. ~/repos/project-hudson
    scripts/pack-for-move.sh ~/Desktop

    Repo + 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.

  4. 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 ~/backups

    Same 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.

ComponentSteadyPeakNotes
macOS + system services~4 GB~5 GBSpotlight indexing after a big clone is a temporary spike.
Postgres 18 (shared_buffers 2 GB + backends)~2.5 GB~3.5 GBPeak is an HNSW index rebuild pulling on maintenance_work_mem.
Hermes gateway~0.6 GB~1 GBIdle polling is cheap.
Claude Code headless session — each~0.8 GB~1.5 GBBudget for two concurrent. This is the number that surprises people.
File cache and burst headroom~4 GBmacOS will use whatever is free; that is correct, not a leak.
Left for a local model~10 GBAnd 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.

SettingValueWhy this, here
shared_buffers2GB~8% of RAM, not the textbook 25%. The entire memory store fits comfortably, and the other 22 GB has claimants.
effective_cache_size6GBA planner hint, not an allocation — tells it macOS's unified page cache is holding the rest.
work_mem32MBPer sort/hash node, so it multiplies by concurrency. Safe because connections are few.
maintenance_work_mem1GBThe one that matters for pgvector: HNSW builds are dramatically faster with room to work.
max_connections40Agent sessions number in the handful. Each idle backend still costs memory.
random_page_cost1.1NVMe. The default 4.0 assumes a spinning disk and pushes the planner off indexes.
effective_io_concurrency200Same reason.
jitoffJIT compilation costs more than it saves on short vector lookups.
hnsw.ef_search100pgvector's recall/latency dial (default 40). Raise toward 200 if retrieval misses things; it costs latency, not memory.
log_min_duration_statement500msCheap insurance — you will want the slow query when retrieval starts feeling sluggish.
Two macOS specifics

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.

Schema changes go through migrations

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 classWeightsVerdict on this box
7–8 B~5 GBComfortable
12–14 B~9 GBThe practical ceiling — fine alongside Postgres, tight with two Claude Code sessions.
24–27 B~16 GBOnly with everything else idle. Expect swap the moment a task runs.
32 B and up20 GB+No. Not on a box that also serves a database and spawns Node processes.
Don't raise the GPU wired limit

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

SymptomCause and fix
Bot and database both dead after a power cutLaunchAgents 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 finelaunchd fell back to a detached process (macOS 26+). hermes gateway status, then start it manually.
Bot silent right after gateway restartThe restart race. hermes gateway stop && sleep 5 && hermes gateway start.
brew services shows postgresql@18 as noneThe 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 useSomething 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 agentsThe skill's workdir is not the repo root.
A task stops mid-way with nothing in the logA permission prompt nobody answered. Add the tool to .claude/settings.json or --allowedTools.
--resume failsResume has to run from the original workdir.
Machine swapping hard during ordinary tasksCheck for a local model still resident (ollama ps), then shared_buffers. Compare with the budget.
Two machines fighting over the botOne token per gateway. Stop the other instance.

Command card

CommandWhat it does
hermes gateway statusIs the boss alive. First thing after any reboot.
tail -f ~/.hermes/logs/gateway.logWatch the gateway in real time.
brew services listIs Postgres running, and under which user.
claude doctorVerify the Claude Code install and login.
/reload-pluginsInside Claude Code — pick up agent or skill edits without a new session.
python3 scripts/sync-copilot-agents.pyRegenerate the Copilot agent files after editing any agent (--check for drift).
python3 scripts/gen-skills-readme.pyRegenerate the skills index after adding or re-describing a skill (--check for drift).
scripts/setup-native-postgres.shIdempotent — safe to re-run any time the database looks wrong.
scripts/pack-for-move.shBundle repo + memory + DB dump for the next machine.
pmset -gConfirm the never-sleep settings survived an OS update.
memory_pressure | head -5The 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.