Living document. Last updated September 2026, against the model generation current at that date. Sections get cut when the model no longer needs them.

I build paid search pipelines with Claude Code from the terminal, and the apps that sit on top of them: Python on uv, BigQuery through the bq CLI, the Google Ads API client, pytest, ruff and mypy as the gates, CI cron as the scheduler, and on the frontend plain HTML, CSS and JavaScript with a small Node server behind it.

Every example here runs on a fictional Nike running account: a search term classifier, the screen where a marketer accepts or rejects its recommendations, the upload queue the accepted rows land in, and the Monday report to the team. Applied AI Engineering for Google Ads with Python describes that system; this page is how I work with the agent that builds it.

Two things have changed since I first wrote this page. The models got faster and more capable, and in most sessions I am the slow part. And the instructions I wrote for last quarter’s model now hold this one back. So every stage and instruction below is re-tested against the model I have today, and cut when it stops earning its place. The last third of the page is the same working method pointed at a screen instead of a pipeline.

Dev lifecycle

For any non-trivial change I run the sequence a good engineering team already uses. It is written once in the global CLAUDE.md at ~/.claude/, so it loads in every repo and no project file restates it, and each stage is packaged as a skill. The ones named below are mine, except /code-review, which ships with Claude Code. My judgment sits at the two ends: what done means, and whether the result meets it.

Stage My job The agent’s job
Intent Answer the interview: what, why, what could go wrong Ask one question at a time, write the intent down (interview-me)
Spec Name the structure, the criteria, the safeguards Draft it, stop for sign-off on money or production writes (spec-writer)
Build Nothing, until a guard pauses it or a criterion is ambiguous Failing test first, least code that passes, cleanup; a bug fix starts with the test that reproduces it
Review Read the findings, re-run one by hand A reviewer that did not write the code checks it (/code-review)
Closeoff Read the proof, decide what ships Tests, lint, dry runs, counts and a screenshot in the report, lessons routed, leftovers into tasks/todo.md (close-off)

The Closeoff checks run as their own commands and get read before anything acts on them: npm test | grep Tests && git commit asks grep whether the tests passed, and grep only knows whether it found some text.

Ceremony scales with risk. A negative keyword push to the live Nike running campaigns gets the full spec template, tests, a dry run and an adversarial review; a logic change in the classifier gets a failing test and a review; a label change in the review app gets a commit.

Closeoff is what makes this a cycle. Every change that ships also changes what the agent should know next time: the docs it reads at the start of a session, the roadmap it plans against, the trap that cost an hour and should not cost another. Updating those is the last step of the session, so the next one starts from the system as it is now rather than as it was last week.

Project CLAUDE.md and docs

The project CLAUDE.md holds what only this repo knows: the real commands, the architecture boundaries, and the invariants that corrupt data if ignored. Claude Code reads it in full at the start of every session, and so does a teammate or a CI agent that opens the repo.

Anthropic’s guidance for the file gives the reason to keep it short: a bloated file makes the model ignore the instructions that matter. Three filters decide what gets in.

  1. Can the model get it from the code, the configs or tool output? Leave it out.
  2. Is it true in every repo? It goes in ~/.claude/CLAUDE.md or a skill, the way the lifecycle above does: “write the test before the code” is method, so it is global; make test-bq is a fact, so it stays here.
  3. Does it read as a warning? Fix it in the code instead: a docstring, a clearer structure, or a test named for the rule.

Trimmed, from the Nike running repo:

# Paid Search Keyword Automation

> Index, landmines and thresholds for THIS repo.
> The change lifecycle lives in the global CLAUDE.md.

## Before starting work
- Read tasks/todo.md, then run make test.

## Commands and test tiers
- make test: mocked, what CI runs.
- make test-bq: real BigQuery, before any SQL or schema commit.

## What NOT to do
- Do not run a pipeline without --dry-run unless asked.
- Do not change a BigQuery schema without DDL in sql/ddl/.

Present tense throughout; history lives in git and the specs. The agent finds the rest of the repo itself with ls, git log and make test. The file points it at tasks/todo.md, read before starting and updated after, so a week later “let’s continue” picks up where it left off.

The docs beside it carry the half the code cannot. The agent reads the code and knows exactly how it behaves, because it wrote most of it. What it cannot recover is why the thing exists, what counts as done, and what must never break.

ROADMAP.md states the goal and the non-goals, which stop the agent building what I plan to replace next month. tasks/todo.md says what is in scope this week, read before it starts and updated after. tasks/specs/ holds the intent and the success criteria per change. docs/ holds the architecture, the testing protocol and the traps, and docs/decisions/ holds an ADR per decision, which is where the why survives a rewrite.

Each record answers a different question: git says what changed, a spec says what was intended, an ADR says why it went that way, a run log says what actually happened.

None of this is tied to one tool. Codex looks for AGENTS.md where Claude Code looks for CLAUDE.md, so every repo carries both, and the AGENTS.md is a stub of three lines: the rules live in CLAUDE.md, read that file, add nothing here. A second rule file drifts from the first, and one repo has a test that keeps the stub a stub.

The machine-level file does the same one level up. My global AGENTS.md tells Codex to read the repo’s CLAUDE.md first, then my global CLAUDE.md for working style and the tools file for what connects to what. Both agents then run the same lifecycle against the same docs, and moving between them costs nothing.

Instruction expiry

Most lines in a CLAUDE.md work around something the model could not do on the day I wrote them, and the models change every few weeks. The newer ones do better with room to work something out, so a rule written for a model that needed babysitting now takes that room away. I once told it to confirm every query with me before running it, written for a model that ran unbounded scans; on the current model that is an approval per query, with me as the bottleneck.

So the file gets a deletion pass twice a quarter, one question per line: what goes wrong if I delete this? “Nothing, it just does that now” means the line goes. “I do not know” means a test: delete the line on a branch, run the task it was written for, read the output. The last pass found a line in most files that was no longer true.

Boris Cherny, who created Claude Code, told YC’s Startup School in July 2026 that the team deleted about 80% of Claude Code’s own system prompt for the newest models, and that people using Claude Code should delete their CLAUDE.md, skills and hooks every six months and see what the model does. Anthropic’s write-up reports the cut with no measurable loss on its coding evals.

Two kinds of line survive every pass: a fact about the data and a preference about how the model writes to me. Closeoff adds a lesson to the file; this pass removes what the current model does unprompted.

Specs

A spec is now a structural brief: what the agent should build, what it connects to, the success criteria and the safeguards. It used to be five to fifteen lines on a five-section template, with an approach section the agent now owns.

The agent builds it, I test the output and push it further. I do not use plan mode: the review happens at the other end, against the finished work. The review app further down is the worked example.

Still in the brief Left to the agent
The goal, one sentence Function boundaries and module layout
The structure: which tables, which jobs, which surfaces, what connects to what Field-level logic and step order
Success criteria a test or a query can confirm Test design
Safeguards: the invariants, one line each UI detail

A success criterion reads “the export tab has 100 rows with no nulls in the routing column” rather than “the export works”. I write them before the code exists, because once code runs it is easy to convince myself it is correct on the grounds that it runs.

Safeguards name what the change could silently break: every search term that enters the classifier leaves it with a label, the spend definition stays identical across the SQL files that use it, the branded-term gate never inverts into a keyword add. They become the review’s checklist; the criteria that hold become tests in CI.

Guards

Auto mode, where the agent keeps working without asking permission at each step, is safe because the guards sit outside the agent and it cannot talk its way past them. Most of my work is scoped tasks with a checkable criterion; agents managing agents with nobody accountable is past where I stop.

The guards run automatically, in three layers:

  1. Deny list (.claude/settings.json): flat walls only a person can loosen. It blocks .env reads, rm -rf and force-pushes.
  2. Command hook (.claude/hooks/guardrail.py): a script that reads each shell command before it runs and pauses for an install, a live run that writes to the ad account, a git push or a BigQuery mutation. Edits, tests, dry runs, read-only queries and commits run unattended.
  3. The tool’s own classifier: judges everything else for whether it is reversible and whether it is aimed outside the environment.

The agent cannot widen its own permissions; a person does that, outside the session. Tightening one is fine. The guards stop irreversible harm and anything aimed outside the environment; a subtly wrong change passes all three, which is what the review is for.

I step back in when a criterion is ambiguous and it is about to guess, when it heads down an approach the brief did not anticipate, after three failed fixes on one issue, or when a guard pauses it.

Unattended work also needs the agent to be able to do the whole job: start the app, drive it, read its logs, see the change working without me. If it cannot, that gets fixed before the feature. The most useful sentence it hands back is “I could not verify X because I cannot do Y.”

Code review

For anything that had a brief the review is adversarial: /code-review, or a workflow where a separate skeptic agent verifies each finding. In one session that pass caught seven bugs in changes that had already passed tests, including a safety gate that had been silently inverted.

The skeptic pass misses things. On one sweep of instruction files, three findings survived it and died on a single command I ran by hand: a file an agent twice reported as deleted was sitting there, a count of eleven was two, a measured number was off by a quarter. I re-run one command before I repeat any agent finding.

Scheduled loops

CI is the scheduler. It runs on someone else’s machine, holds the credentials and posts to Slack, and one block of YAML that runs an existing CLI tool on a clock and posts the output is a loop with no session of mine in it. No new infrastructure until CI cannot do the job.

Mine runs on Monday mornings: a scheduled CI job runs the existing weekly report over the week’s decisions and posts it to the team channel. Two rules keep it safe:

  1. Report only. A loop that edits a prompt, changes a bid or opens a pull request is a different risk class from one that writes a message. After a month of agreeing with it, decide which single action it has earned.
  2. Fire it once by hand. A job in the config has not run yet, and a job that never fired produces no error.

Skills

A skill is a named instruction file with a one-line description. Some I invoke (/debug when something is stuck, /code-review for the adversarial pass); others carry a trigger clear enough that the agent reaches for them itself (a Slack update, an ADR).

The ones I use most wrap Python scripts: a Data Transfer freshness check before the classifier runs, a schema validator on what it returns. The agent frames the input and reads the output, the script does the work, and the same input gives the same output every run.

That is what I trust to fire against a production BigQuery dataset or a queue of accepted Google Ads mutations, so where a step can be a script, it is one.

Write each description the way someone phrases the request (“Use when a scheduled job fired on stale data”), because the agent matches on the description.

Testing

Technical. Does data flow through the pipeline, does the parser handle every format the LLM returns, does a malformed row fail cleanly. The agent writes and runs these: pytest with mocked APIs, ruff and mypy; a declared return type catches an invented shape the moment something uses it.

Domain. Do the negatives it proposed for the Nike running campaigns match what I would block reading the search terms report. That check is mine, by hand, and it goes into the brief as a success criterion.

Three checks under the technical layer:

  1. Mocked tier. make test in the CLAUDE.md above; CI runs it on every push.
  2. Real-database tier. make test-bq, before any commit touching SQL or schemas, and on a scheduled CI job so it cannot rot.
  3. Dry run. --dry-run runs the full pipeline against the real dataset with the final write skipped, after any change to SQL, table references or API calls.

Tests verify the code. Evals verify the AI output, and a prompt change ships when the gold set holds. The loop (a gold set first, then open coding on real rejections, then a prompt change behind two regression gates) is in Applied AI Engineering for Google Ads with Python, along with the production checklist.

Data loop

Three parts: BigQuery reads the account, an LLM decides something about what it read, and the Google Ads API writes the decision back.

BigQuery (analysis) -> Python (mutations) -> Google Ads API (live changes)
       ^                                                    |
       |_________________ Data Transfer __________________|

Read side. The Google Ads Data Transfer lands a documented set of reports in BigQuery as daily snapshots (campaigns, keywords, search terms, tROAS targets, budgets, stats). Each report arrives twice: a p_ads_* table partitioned by day, and an ads_* view over the same rows that carries every snapshot day plus _DATA_DATE (the snapshot day) and _LATEST_DATE (the newest one).

Two rules sit in the project CLAUDE.md. A dimension lookup filters _DATA_DATE = _LATEST_DATE, or every entity appears once per snapshot day and a join multiplies the stats. A stats query over a date range reads the p_ads_* table, filtered on segments_date for the reporting days it needs.

The join trap sits there too: the transfer embeds the ad group and keyword IDs as a resource path in segments_keyword_ad_group_criterion, and joining search term stats to the keyword that triggered them means parsing that string apart.

The file also carries table purpose and the canonical SQL for CAC, LTV:CAC and conversion rate; schemas it reads from INFORMATION_SCHEMA itself. For BigQuery I use the bq CLI rather than an MCP server: it reaches every project and the agent learns it from --help.

Decision and write side. The decision layer is a Python script: query BigQuery, send a batch to an LLM with a structured prompt, validate the response against a Pydantic schema, write to staging and MERGE into production.

A person gates every change to the account: recommendations queue in the review app, accepted rows land in an upload queue, and today they go into the account through Google Ads Editor. The API is the same step with the approval as its trigger, and it is the step I hold back longest, because a warehouse write rolls back and an account write needs a compensating mutation.

Data Transfer syncs the changed account back, so each run reads the state the previous run changed. I set the thresholds that trigger an action; the agent handles the API mechanics.

What the backend is made of. One line each; the full treatment is in the other article.

  • Staging table first, append-only with a run ID, then MERGE into production, so a re-run never duplicates and a bad run stays visible.
  • Every response validated against a Pydantic schema before it reaches the warehouse; invalid rows are dropped and the count lands in the run log.
  • Every UPDATE or DELETE scope-locked to an explicit list of terms or a fixed predicate, snapshotted, counted before and after, rolled back on a count mismatch.
  • Every model call batched and retried with backoff; a batch that fails is split in half and re-sent.
  • --dry-run on every pipeline that writes and --limit N on every one that calls the model; a daily CI run scores the prod gold set and fails under 90%, so a prompt change that regresses shows up the next morning.

From backend to frontend

The same setup builds the apps that sit on top of the pipelines, and the loop changes shape when it does. Everything the backend does leaves evidence the agent can read: a test result, a row count, a dry-run diff. A screen leaves almost none. An internal tool that a handful of reviewers use also never has to scale, which takes most of the usual frontend engineering off the table.

So the app is as plain as the job allows: HTML, CSS and JavaScript as native ES modules, no framework and no build step, with a small Node server beside it that holds the credentials and runs the queries. The browser never touches the warehouse.

The brief grew in three steps, each stated as behavior and none as field logic. Extract a working prototype into split modules behind a data contract. Then read the batch live and write decisions back: a reviewer accepts a row or rejects it with a reason, approvals MERGE into the queue the team implements from, rejections into the feedback table the pipeline learns from. Then refresh daily and on a button, and fall back to a fictional sample when there is no backend, saying so in the header.

Where the two repos meet. They share no code. The backend owns the SQL and the scoring. What crosses is one versioned JSON contract, owned by the backend and vendored into the app, with a tripwire test on each side: the backend checks its export still produces exactly the promised columns, the app checks its fixtures and its own contract-version constant against the copy. A refresh re-runs the export SQL and never triggers the classifier.

Giving design feedback. “Make it feel cleaner” tells the agent nothing, so the feedback has to point at where things go. An annotated screenshot is the default and covers most of it: screenshot the app, draw arrows and boxes on it, paste it back. A Figma frame over the remote MCP server works in both directions, and Claude Design authors a page that drops straight into a no-build app. Every capture is taken in sample mode, so real data never reaches an external file.

The agent also screenshots its own work: on one redesign, rendering the page headlessly and reading the pixels caught three layout defects that reading the code did not.

Artifacts. A published artifact is one self-contained page on a stable URL that anyone can open with no VPN and no install, which is what makes it useful for a frontend.

When a choice was visual (a headline layout, a color direction, the shape of the review list), the agent rendered every option on one page in the app’s own CSS on fictional figures. I picked by letter, and the decision was stamped back onto the same page: A shipped, B parked, C declined, so the page became the record of what was weighed.

The app itself gets published the same way, built into a single file on sample data, so the team clicks the thing instead of reading a description of it. Three rules make it work: it is a snapshot and does not update on push, it has no network at all so fonts and images are inlined, and it has no backend, so anything that would look live and do nothing gets fixed before publishing.

Brand. The brand assets live in their own folder in git: the logo files, the licensed typeface, the badge set. I pointed the agent at it and added two references of my own, the product gradient and the marketing site’s button.

It sampled the hexes into CSS tokens, each commented with where it came from, loaded the typeface from a local file rather than a CDN, and drew the logo as outlined paths so it never waits on a font. The rule it kept: the gradient stays off anything that carries a decision, one hue per verb, contrast measured against the text color.

Tests. Vitest covers the pure logic in Node, and a source scan fails the build if a file in the logic or state directories imports anything that touches the screen. Node tests drive the real request handler against a fake warehouse client and check the MERGE text and the order of the gates. One Playwright file drives the browser on injected fixtures, each test titled as a sentence, most carrying the spec number they pin, asserting on computed styles. CI runs lint, unit, contract and e2e on every push.

Using it myself. I tested every button and every action and walked the whole review flow before anyone else saw it. I am a paid search specialist, so I was the builder and the user at once, which is most of why the app fits the work; the rest came from the people who use it.

From there it is a back and forth: this filter needs another field, this step does not flow, this button should fit a smaller window, the column headers should stay put while the list scrolls. A per-keystroke typing bug slipped past every green browser test and showed on the first word I typed.

Setup over time

When I first wrote this page, in March, I ran one session at a time, read every tool call, and wrote a spec for every non-trivial change. Each row is something I stopped doing after re-testing it.

From To
Approving every tool call, reading the transcript Jobs on a clock with no session at all
A spec per change, five to fifteen lines A structural brief, success criteria and safeguards
A CLAUDE.md that grew every session One cut twice a quarter, each cut tested
Reviewing the plan Reviewing the result, adversarially
One session doing everything Subagents for self-contained work, workflows for fan-out, one reviewer told to refute the rest

I still own what ships. When someone asks why a batch of negatives was safe to apply, the brief, the dry run and the review are the answer, and I have to be able to explain the retry logic and change the routing rules. Engineers read my briefs and see the business logic; I follow their design reviews because I built the system.

The setup will change again within months. The question for each instruction, skill and stage stays the same: does the model I have today still need this, or can it work it out itself?