Install the plugin
At the end you will have the plugin loaded in Claude Code, node tools/doctor.mjs reporting 5/5 checks passed, and proof that the hooks write the ledger. This page is for the person setting up the machine once; driving it daily is Drive a run.
What you need first
Each item below is fatal to the preflight, so check them before step 1. Do them all, then move on.
READ-DO checklist- Node 22.12 or newer.
node --versionmust printv22.12or higher. Install from nodejs.org. - Google Chrome or Chromium. Every measurement drives a real browser. If Chrome is not in a standard location, set
CHROME_PATHto the binary. - GitHub CLI, logged in.
gh auth statusmust name an account. Steprepoasks GitHub whether the site's repo is private. Install gh. - Wrangler, logged in.
npx wrangler whoamimust name an account. The publish steps deploy to Cloudflare Pages. Install wrangler. - Claude Code with the Chrome DevTools MCP server (that is: a bridge that lets the agent drive a browser) registered for all projects:
claude mcp add --scope user chrome-devtools -- npx chrome-devtools-mcp@latest. That is the agent's browser. The gate's predicates launch their own headless Chrome and do not need it.
The preflight will fail, and that is the intended behaviour: the machine refuses to start a build it cannot finish. The only legal way past is the preflight-failed blocker, which needs the failing preflight.json the doctor writes. See blockers.
Step 1 — get the code and its dependencies
Clone the repository and install its dependencies. Keep this clone: every node tools/… command in these docs runs from it. Everything the predicates need beyond these packages runs pinned through npx at the moment it is needed.
git clone https://github.com/fabkrum/site-machine.git
cd site-machine
npm ci
npm ci ends with added 5 packages (give or take one on a later version) and no ERR! lines. ls tools/predicates lists 23 files.
Step 2 — load the plugin into Claude Code
The repository is its own marketplace: .claude-plugin/marketplace.json points at .. Add it, then install the plugin from it.
claude plugin marketplace add /path/to/site-machine
claude plugin install site-machine
For development, a temporary alternative loads the directory for one session without installing: claude --plugin-dir /path/to/site-machine.
You now have two copies: the clone, which you type commands against, and the installed copy, which Claude Code uses for the hooks and skills. Keep them on the same version — re-install after pulling.
The install reports ✔ enabled and Skills (8) — the seven actor skills plus the vendored Modern Web Guidance.
The hooks are discovered from hooks/hooks.json inside the plugin and resolved through ${CLAUDE_PLUGIN_ROOT}. That placeholder is substituted only for plugin hooks — in a project's .claude/settings.json it stays literal, which is why they used to fire on exactly one laptop. Do not copy them there; a copy fires every hook twice.
Step 3 — restart the session
Hooks load when a session starts. Installing the plugin mid-session changes nothing until you start a new one, and without the hooks the ledger is never written.
Quit Claude Code and open it again in any project except the site-machine clone itself — even after the install, a session opened on the plugin's own folder loads none of its hooks. On start, the SessionStart hook announces open runs; on a fresh machine it says nothing, which is correct.
/hooks in Claude Code lists a PostToolUse hook and a PreToolUse hook pointing at site-log.mjs and site-guard.mjs under the plugin's path.
Step 4 — record the operator attestation
Once a year, a named person confirms two things: the GitHub and Cloudflare accounts use two-factor authentication, and the permissions of the Cloudflare API token wrangler uses were reviewed (in the Cloudflare dashboard under API Tokens). The preflight refuses without this record. It is written to your own state directory, never to the plugin.
node tools/attest.mjs record --by "Your Name" --two-factor --token-scopes "reviewed 2026-08-29: Pages deploy scope only"
It prints attested — Your Name, <date> (expires in a year; preflight will re-ask) and the path under ~/.local/state/site-machine/.
Step 5 — run the doctor (your first win)
The doctor is step 1 of every run, but it also runs on its own. It checks node, Chrome, gh, wrangler and the attestation, and it writes preflight.json into the directory you name, whether it passes or not. Name a scratch directory, so the file does not land in the plugin:
mkdir -p ~/sites/scratch
node tools/doctor.mjs ~/sites/scratch
Five lines starting ok , then preflight ok — 5/5 checks passed (preflight.json written). A failure prints preflight FAILED — <id> (fix: …) with a per-platform hint; fix that item and run the doctor again.
Step 6 — prove the hooks write the ledger
This is the check most people skip and later regret. Components written without a live ledger make step build refuse, and they have to be written again. Prove it now, on a scratch site.
Two actors take part here: you in your own terminal, and the agent in Claude Code. First, in your terminal, from the clone, start a run in the scratch directory. This creates run-state.json there, which is what makes it a run the hooks can see.
node tools/site-step.mjs start ~/sites/scratch preflight
Then open Claude Code in ~/sites/scratch and type: create a file called hello.txt here.
After the agent writes the file, ~/sites/scratch/run-log.jsonl exists and its last line is a JSON record with "event":"tool_call" and the path you wrote. If the file is missing, the hooks are not loaded — go back to step 3.
You are done when
All three hold: the doctor prints 5/5, the scratch site's run-log.jsonl grew when a file was written, and the plugin directory itself contains no preflight.json and no .claude/state/. The last point is the machine's own promise — a first run writes nothing under the plugin — and tools/tests/first-run.test.mjs asserts it.
If it did not work
The plugin fails to load entirely
Check that .claude-plugin/plugin.json does not declare a hooks key. hooks/hooks.json is auto-discovered; naming it again reads as a duplicate and the whole plugin is refused. The manifest declares only skills.
Doctor says wrangler-auth failed but I am logged in
wrangler whoami must name an account. An exit code of 0 with no account line still counts as a failure. Run it by hand and read the output.
Doctor says chrome failed on Linux
The known paths live in tools/chrome.mjs. Point CHROME_PATH at your binary. Use the same value for the whole run — Lighthouse honours it too, and two different Chromes give two different numbers.
Doctor says operator-attestation failed
The attestation is older than a year, or the --token-scopes text is shorter than 20 characters, or the state directory is not where the doctor looks. SITE_MACHINE_STATE and XDG_STATE_HOME must be absolute paths; a relative one is ignored.
run-log.jsonl never appears
Either the session predates the install (restart), or you opened the plugin repository itself as the project. A session opened on this repo loads no plugin hooks — only an install or --plugin-dir does. Work in a separate site directory.
Windows
Unsupported. No Chrome path is listed, and path comparisons are case- and separator-sensitive. It fails closed rather than silently, and nobody has run it there to find the rest.