Onboarding a new client into the analytics agent used to be the most error-prone thing I did all month. Not because any single step was hard — because there were a dozen of them, each depending on the one before, and missing one produced a site that looked configured but silently reported nothing.
This piece is about two things that shipped together: a Claude skill that does the entire onboarding in one session, and a CI guard that keeps that skill — which is full of internal paths and secret layout — off any public branch. Both exist because of the same underlying tension: the more powerful the playbook, the more dangerous it is to leak.
The work that doesn’t survive being done by hand
The analytics agent runs a weekly operations report for several B&Bs in Hengchun. Each site needs the same scaffolding: a config file, a business-context document the analysis AI reads to avoid misreading traffic, an ads-health context, an operations runbook, a mail-recipients list, and an operator-events.yaml log. None of that is the hard part.
The hard part is that the scaffolding has to reflect the real site, and it has to wire tracking on both sides of the system. The website’s analytics.ts needs GA4 and Google Ads tags. The Google Ads account needs conversion actions created — and those return real AW- tags and conversion labels that then have to be pasted back into analytics.ts. If a placeholder label ships, the on-site CTA clicks — phone taps, contact-button taps, book-now clicks — never register as conversions, and the whole point of the pipeline quietly fails.
That round-trip is what nobody does correctly by hand on the first try. You create the conversion action, you forget to retrieve the label, you ship a placeholder, and three weeks later the conversion column reads zero and you have to reconstruct what you skipped.
This is the kind of work that exists at all only because AI makes it affordable. A small B&B can’t fund an engineer to spend a careful day wiring analytics, an ads specialist to create conversion actions, and someone to verify the round-trip closed. Bundled, that was never in a small operator’s budget. The skill collapses it into one session.
The skill reads the website before it writes anything
The design principle I committed to: never write a placeholder value. Every ID, label, email, and URL in the generated files must be real before the file is written. If a value isn’t known yet, the skill collects it first.
So the skill starts by reading the website’s source code — a local path or a fresh GitHub clone — and extracting the actual business data: contact channels, pages, room and product information, the CTAs. The generated business-context.md reflects this specific site, not a generic template. That matters downstream: the weekly analysis AI reads that document to know the property’s peak seasons and local context, so it doesn’t misattribute a normal off-season dip to a problem.
Then it generates the seven sites/<slug>/ files from templates that live beside the skill prompt. Templates as a starting point, adapted to what the code revealed — not as final output.
The phase that justifies the whole thing is the last one. The skill updates the website’s analytics.ts, then uses the Google Ads API to create the conversion actions, reads back the real tag and labels, and inserts them. The round-trip closes inside one session, by the same actor that created both ends of it. That’s the step where a human loses the thread; the skill doesn’t, because it doesn’t move to the next file until it has the real value.
The skill is too useful to be public
Here’s the problem the second commit solves. The skill is genuinely valuable — and that value comes from it referencing project internals: the sites/ and scripts/ layout, the ads client path, the secrets directory structure, the installed packages it depends on. The same specificity that makes it work makes it a liability if it leaks. A skill that names where secrets live and how the ads client is wired is a map I don’t want on a public branch.
The same is true for the work logs and decision notes — HANDOFF.md, WORKLOG.md, DECISIONS.md. These are dev-only by nature. They describe the system honestly, including the things you’d never put in a README.
The naive fix is to remember to delete these before merging. That fails the first time you’re tired. So I built a CI guard instead.
The guard reads a list, not a workflow
The design decision worth naming: the list of dev-only paths lives in a plain text file the CI guard reads, not hardcoded in the workflow YAML. One path per line. A trailing slash means a directory — anything inside it counts as a violation. No trailing slash means an exact file match. Comments start with #.
The guard fails the build if any listed path appears on a non-dev branch. To protect a new dev-only path, you append one line to that file. The guard picks it up on the next push — no workflow edit, no second place to keep in sync.
That last point is the whole reason it’s a separate file. The failure mode for security guards isn’t that they’re missing — it’s that the thing they protect and the list of what to protect drift apart. Put the list in the workflow, and the next person who adds a dev-only doc has to know the workflow exists and edit it correctly. Put it in a file the workflow reads, and adding protection is a one-line append in the same directory you’re already working in. The guard stays correct because keeping it correct costs nothing.
Why this is one story, not two
The skill and the guard look unrelated — one onboards clients, one polices branches. They’re the same decision seen from two sides. The skill’s power is its specificity: it knows exactly where everything lives. That specificity is precisely what cannot be public. Build the capability and the containment in the same merge, or the first one is a leak waiting to happen.
The onboarding I used to dread is now one command — and the thing that makes it work can’t escape the branch it belongs on.