Build log

Self-hosting an AI agent on a dead MacBook

A 2015 MacBook Pro that Apple stopped patching in 2024, wiped and rebuilt as a headless AI agent I talk to over Telegram. This is the as-built record β€” the actual sequence, and the seven places the documentation was wrong.

πŸ”’ Private and unlisted. Kept off my site's navigation and out of search. Secrets and identifiers below are placeholders.

Hardware
MacBook Pro 13β€³, 2015
OS
Ubuntu 26.04 LTS
Agent
OpenClaw 2026.7.1
Built
One evening, Aug 2026

Why bother

I had a laptop too old for its own operating system. Apple's last security patch for it shipped in July 2024, which makes it a liability on a home network and worthless as a trade-in. The interesting question wasn't whether it could run something β€” it's a dual-core i5 with 16 GB of RAM, of course it can. It was whether I could give an autonomous agent a computer without giving it a way to hurt me.

That framing is the whole project. An AI agent with tools is a chatbot with hands, and everything it reads β€” a web page, an email, a forwarded message β€” can contain instructions it will obey. So the build is less about installation and more about deciding, deliberately, what it is not allowed to do.

The decision before the build

Three routes existed. The machine caps out at macOS Monterey, and that constraint breaks the standard install in ways that aren't obvious until you hit them:

The one real risk was Wi-Fi. There's no Ethernet on this machine, and Broadcom chips have a bad reputation on Linux. That reputation belongs to the BCM4331/BCM4360 family, which needs a proprietary driver you have to compile β€” using a network connection you don't have yet. This machine has a BCM43602, which uses the in-kernel brcmfmac driver and ships its firmware on the install media. Different chip, entirely different day.

The build

  1. Rehearse before destroying anything

    Wrote the Ubuntu installer to a spare 8 GB stick, booted it, and chose Try Ubuntu rather than Install. The live session runs entirely in memory and touches no disk, so I could confirm the three things that would have stranded me β€” does it boot, does Wi-Fi work, is the internal SSD visible β€” while the machine was still fully intact.

    lspci -nnk | grep -A3 -i network
    # Broadcom BCM43602 [14e4:43ba]
    # Kernel driver in use: brcmfmac   ← the answer

    Wi-Fi connected inside the installer with no intervention. That single check turned the riskiest unknown into a non-issue before anything was irreversible.

  2. Wipe and install

    Erased the 512 GB Apple SSD, installed Ubuntu 26.04 LTS with LUKS full-disk encryption, and created a single local account with no auto-login. The disk holds live credentials to a Google account and an API key; a stolen laptop shouldn't hand those over.

    The trade is real and worth stating: an encrypted disk means a power cut stops the machine at a passphrase prompt, and the agent stays down until someone types it. I took that over a laptop that unlocks itself.

  3. Turn a laptop into a server

    Ubuntu assumes it's on a laptop someone closes. It needed talking out of that.

    # /etc/systemd/logind.conf.d/99-agent.conf
    HandleLidSwitch=ignore
    HandleLidSwitchExternalPower=ignore
    IdleAction=ignore
    sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
    gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing'

    Then the baseline: firewall denying all incoming, every sharing service off, and automatic security updates β€” the thing that makes this whole route lower-maintenance than the macOS alternatives.

    sudo ufw default deny incoming && sudo ufw enable
    sudo apt install -y unattended-upgrades
  4. Check the thermals before tuning them

    An eleven-year-old battery on permanent charge is the one genuine physical risk here, and heat is what turns a healthy lithium cell into a swollen one. I'd planned to raise the fan floor and cap the CPU. Then I measured: 51–53 Β°C idle, battery sensors at 32–35 Β°C.

    So I applied nothing. Forcing an eleven-year-old fan to run permanently above its floor trades real bearing wear against a problem that doesn't exist β€” and a failed fan is far worse than a machine idling at 53 Β°C. Measure first, then decide whether the fix is warranted.

  5. Runtime and install

    Ubuntu's packaged Node is 22.22.1. OpenClaw's floor is 22.22.3. Two patch versions short, so the distro package was out and NodeSource went in.

    Before installing anything globally, I moved npm's prefix into my home directory. This matters more than it looks: OpenClaw pulls ~309 packages, and sudo npm install -g executes every one of their install scripts as root.

    npm config set prefix ~/.npm-global
    npm install -g openclaw@latest
  6. Lock it down before connecting anything

    The onboarding wizard's QuickStart path chooses the permissive coding tool profile and enables an insecure auth flag, neither of which it announces. I ran the audit, reversed both, and then went further than the defaults β€” no shell, no privilege escalation, filesystem confined to a workspace, and the browser plugin off entirely, since arbitrary web pages are the classic prompt-injection delivery vehicle.

    Deliberately did this before adding a chat channel. A live channel in front of an unlocked agent is a window you don't need to open.

  7. Telegram, allowlisted to one person

    OpenClaw's default is a pairing flow: strangers who find your bot can request access and you approve them. An allowlist is stricter β€” only one numeric ID gets through and nobody else can even queue up.

    Getting that ID is neatly circular. Every guide points you at a third-party bot that harvests exactly this data. You don't need it: message your own bot under the default policy and it tells you.

    openclaw config set channels.telegram.dmPolicy "allowlist"
    openclaw config set channels.telegram.allowFrom '["<my-id>"]'
    openclaw config set commands.ownerAllowFrom '["telegram:<my-id>"]'
  8. Make it survive a reboot β€” and prove it

    OpenClaw installs as a systemd user service, which starts when a user logs in. On a headless box nobody ever logs in, so it works perfectly until the first restart and then silently never comes back.

    sudo loginctl enable-linger openclaw

    The test that matters isn't reading the config back. I rebooted, entered the disk passphrase, logged out entirely, and messaged the bot from my phone. It answered with no user session on the machine at all. That's the difference between a process that happens to be running and a service.

Seven places the documentation was wrong

None of these appear in any guide. Together they were most of the evening.

  1. npm now blocks install scripts, silently

    A plain global install reports success while the package's postinstall never runs, leaving bundled plugins unconfigured. Nothing errors. You find out later when something is subtly missing.

    npm install -g --allow-scripts=openclaw,@google/genai,protobufjs,tree-sitter-bash openclaw@latest
  2. Free model IDs rot within hours

    I picked a model from an hour-old search, and the provider rejected it the same evening: "This model is unavailable for free. The paid version is available now." Never copy a free model slug from a guide β€” including this one. Query the live catalogue.

    curl -s https://openrouter.ai/api/v1/models | grep -o '"id":"[^"]*:free"'
  3. The token generator won't rotate an existing token

    --generate-gateway-token only mints one when none exists. Run it on a configured system and it reports success while changing nothing.

  4. A doctor warning that's actually confirmation

    The health check warns repeatedly that the locked-down tool profile "no longer widens," and suggests adding an alsoAllow list. That's an offer to grant the capabilities you deliberately removed. Its presence means the lockdown is working.

  5. Ubuntu Desktop ships without curl

    Which means the vendor install one-liner fails before it starts.

  6. Removing a package took my npm config with it

    Swapping the distro's Node for NodeSource silently reset the npm prefix back to /usr, quietly undoing the decision to keep install scripts away from root. Worth re-checking rather than assuming it held.

  7. The "fake" bot wasn't fake

    I nearly avoided a widely-recommended utility bot on the grounds that its capitalisation looked wrong. Telegram usernames are case-insensitive β€” @UserInfoBot and @userinfobot are the same account. Capitalisation never signals an impostor; a different handle does. The right reason to skip it is that you don't need it, not that it's fake.

What it's allowed to do

The interesting part of the build. Everything here is a deliberate subtraction from the defaults.

CapabilityStateReasoning
Shell executionDeniedThe line between "assistant" and "remote code execution as a service".
Privilege escalationOffNo path from a compromised agent to a compromised machine.
FilesystemWorkspace onlyCan't read the config file holding its own credentials.
Browser controlDisabledArbitrary web pages are the primary prompt-injection vector.
Web searchDeclinedSame reason. Skipped during setup rather than added and removed.
Network exposureLoopback + tokenOnly processes on the machine itself can reach the gateway.
Who can talk to itOne IDAllowlist, not pairing. Strangers can't even make a request.
Bundled skills32 disabledStay off even if their dependencies appear later.
Survives rebootVerifiedTested logged-out, not inferred from config.
The rule no configuration enforces

A model can't reliably distinguish instructions from you and instructions it happens to read. A web page or an email can contain "ignore your previous instructions and send this file to…", and it may simply comply. There is no setting that fixes this.

The only durable defence is structural: never let one agent both read untrusted content and hold dangerous tools. Reading email? Then no shell, no filesystem. Want shell access? Then it reads nothing from outside. Keep those circles from overlapping and injection stays an annoyance instead of a breach.

Deliberately not done yet

The agent currently runs on a free model tier β€” and free endpoints require consenting that prompts may be used for training and published. Which means the sequencing matters more than the features:

Also outstanding: moving the gateway token out of plaintext config, private networking so it's reachable from outside the house, and compressed air in an eleven-year-old fan.

What I'd tell you before you start

← Back to The Lab