Bootstrap the Spark

Bounce the wired NIC, run make bootstrap, check the results, and set up agent’s SSH and Claude Code login.

Everything here runs on the Spark, as you, in an SSH session from the Mac, except where a step says the Mac or agent.

Before you start

  1. On the Mac, SSH into the Spark over Wi-Fi (its .200 address) — not the wired interface. Bouncing the interface you’re connected through would drop your own session.

  2. On the Spark, find the wired interface’s name: nmcli device status.

  3. On the Spark, bounce it so it takes its DHCP reservation:

    sudo nmcli device disconnect <wired-iface> && sudo nmcli device connect <wired-iface>
  4. On the Spark, confirm the wired address now ends in .201.

Run it

On the Spark, as you, from the clone (cd ~/git/hub/local-ai), in an SSH session. Type make bootstrap, never sudo make bootstrap: the target asks for your password itself, and under an outer sudo it would give your groups to root.

  1. make bootstrap-dry-run — read the output before doing anything else. Its hold step prints GPU set: N packages, M already held: on a bootstrapped Spark, all of them held (151 at the 2026-09-24 bootstrap). Off the Spark (the Mac, CI), with no DGX kernel installed, it says a real run stops here instead. A package that isn’t cleanly installed stops the dry run too, with a hint for finishing it.
  2. make bootstrap — run it over SSH; it stops any running desktop session.

After bootstrap

  • Log out and back in — new group membership only takes effect on a fresh login.
  • systemctl get-default → multi-user.target
  • systemctl is-active earlyoom → active
  • sudo ufw status → OpenSSH allowed
  • id agent → no docker, sudo or spark-admin in the list
  • apt-mark showhold | grep -E '^(linux-image-nvidia-hwe|nvidia-driver|cuda-toolkit)-' → the kernel, the driver and CUDA among the held GPU set. From here on, updates follow Updates.
  • free -g → record this as the new baseline

Agent’s SSH and Claude Code login

  1. On the Mac, make agent its own key and copy the public half to the Spark. Keys and the Mac’s ~/.ssh/config are in SSH from the Mac:

    ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519-brightroar_agent -C "agent@brightroar"
    scp ~/.ssh/id_ed25519-brightroar_agent.pub brightroar:agent-key.pub
  2. On the Spark, install it for agent, from an interactive ssh brightroar session. sudo needs a terminal to ask for your password, and a command piped into ssh doesn’t have one:

    sudo -u agent sh -c 'umask 077 && mkdir -p /home/agent/.ssh && cat >> /home/agent/.ssh/authorized_keys' < ~/agent-key.pub && rm ~/agent-key.pub

    Your shell opens the key file and hands it over on standard input, and agent writes it into its own home: umask 077 makes .ssh 0700 and authorized_keys 0600 if they are new. Root never writes, chowns or chmods anything under /home/agent. agent controls that folder, so a link it planted there could turn a root write into a write to any file on the box. The copy is deleted only once the key is in. Run again, it stops at the missing copy and changes nothing; with a fresh copy, it adds the key a second time, which is harmless.

  3. On the Mac, log in as agent: ssh brightroar-agent (the alias from SSH from the Mac)

  4. On the Spark, as agent — the installer refuses to run under sudo:

    curl -fsSL https://claude.ai/install.sh | bash
  5. On the Spark, still as agent, log in: run claude. With no browser on the box, press c to copy the login URL, open it on the Mac, and paste the code back into agent’s claude on the Spark. (Added 2026-09-28: its secrets guard comes in pi, before agent gets a key.)

agent makes its own ~/work too, the same way: its first git clone into it creates it. Bootstrap doesn’t, since it runs as root.

Live checks (you run these — they need sudo)

On the Spark, as you — not in step 3’s agent session, which has no sudo:

  • sudo -u agent sh -c 'if test -x "$1"; then echo "OPEN: stop and fix permissions"; else echo "closed: good"; fi' _ "$HOME" → closed: good. $HOME expands in your shell, so it names your home whatever your login is; test -x asks whether agent can enter it at all, without reading anything in it. The verdict comes from agent’s own shell, so a failed sudo shows its error, never a false “good”. Plain -u, not -iu: -i re-reads the command in agent’s login shell, which would expand $1 there, to nothing.
  • sudo -iu agent docker ps → permission denied
  • sudo -iu agent nvidia-smi --query-gpu=name --format=csv,noheader → the GPU’s name, without the per-unit UUID that nvidia-smi -L prints

Re-run once

make bootstrap again → finishes with no errors, and no new users, groups or config lines.