Run xcodebuild test with -only-testing:ScriptmateTests/ImportTests and it prints “Executed 0 tests” followed by “TEST SUCCEEDED”. The identifier has to be the Swift suite type name, not the folder name, and nothing warns you. An agent that has just changed the import code will read that line, report green, and move on. That trap, and the dozen like it, are what this post is about. Not the prompts. The rules that make an agent’s green mean something.
Scriptmate is a line rehearsal app for actors that I design, build and ship alone. It has 693 commits since April, and 324 of them carry a Claude co-author trailer. It has 771 Swift Testing tests in 77 files. It is on the App Store with paying subscribers in five languages. And for the first four months, my Claude Code setup was a template like everyone else’s. The operating manual came in August, after enough regressions.
Write CLAUDE.md as an onboarding doc for a senior hire
The file is 58 lines. It has no personality, no “you are an expert Swift engineer”, and no prompts. It reads the way I would brief a contractor on their first morning, and the sections are the ones a human would need:
- Overview and stack. One paragraph on what the app does, then the facts that change decisions: Swift 5 mode with approachable concurrency enabled, iOS 16 minimum, Xcode 26, dependencies via Swift Package Manager, and that the parsing backend is a Cloudflare Worker whose source lives in a different repository.
- Build, run, test. The exact
xcodebuildcommands, which schemes matter and which to ignore, how to run a single suite, and the Fastlane lane that ships to TestFlight with the environment variables it needs. - Architecture and conventions. MVVM plus a composer per screen, the three files every screen folder contains, where fakes live, how networking, analytics and feature flags are wired. Enough that the agent puts new code where I would.
- Do not touch. The short list that prevents the expensive mistakes.
- Gotchas. The things I would otherwise explain in a code review for the third time.
The most useful line in the file is in the testing section: “Always run the relevant tests after changing logic; don’t mark a task done on a build alone.” Without it, an agent reports success on a green compile. With it, the default changes to proof.
Two other lines earn their place. The test plan skips the one suite that hits the live parsing API with a real PDF, and CLAUDE.md says not to un-skip it, so the agent cannot spend money on the backend by being thorough. And debug builds tag every analytics event as internal traffic, which the analytics property filters out, so an agent driving the simulator all afternoon never shows up in the product numbers.
The gotchas section holds the second tier. The analytics folder is misspelled on purpose and stays that way, because renaming costs more than the typo. User-facing copy never uses em or en dashes. Debug builds talk to the dev worker and use RevenueCat’s test store key, which cannot simulate free trials, so trial flows need a launch argument and a sandbox account on a device. Localized strings live in a string catalog in five languages with manual extraction, so a new string means five entries by hand. An agent cannot infer any of this from the code, and each item produces a wrong change if missing.
The do-not-touch list
Permissions decide what the agent can run. The do-not-touch list decides what it must not change even when it can. Mine has four entries:
- Never hand-edit
project.pbxprojto add files. Source folders are filesystem-synchronized, the Xcode 16 style: the project watches the folder instead of listing every file, so nothing has to be registered. The project has been that way since its first commit, and I have never had an agent-created file produce a project file conflict. On older projects I have, and the fix is this setting. - Don’t change signing or provisioning, in the project or in the Fastlane configuration.
- Don’t commit anything under the build or derived data folders.
- Don’t edit the Cloudflare Worker from dashboard instructions. Worker changes happen in the worker repository and deploy with wrangler, so the agent cannot drift production by following a pasted snippet.
The allow list drifts, and nobody reviews it
The project’s Claude Code settings hold 97 allow rules and no hooks. My mental model was that they were the build and test commands from CLAUDE.md, simulator control, git and the file tools, and nothing that deploys or touches money.
Writing this post was the first time anyone audited the file. It contained wrangler deploy, from the week the Worker moved out of this repository. It contained a sed that deletes a line from project.pbxproj, the file the do-not-touch list says never to hand-edit, from the same week. It contained the RevenueCat customer list through an MCP server, allowed once for a support question and never revoked. Each one was a reasonable click on a specific afternoon. The allow list accumulates by pressing allow, and it drifts the same way any config does.
Those three rules are gone now. The principle that survives is the same as the do-not-touch list from the other side. The agent should build, test, inspect the simulator and read logs without asking me every ninety seconds. It should never be able to ship. A release is one Fastlane command, typed by me, on a branch named for the release candidate, and Fastlane is not in the allow list at all.
Architecture notes next to the code, written for the agent
CLAUDE.md is a map. The territory lives in five Markdown files beside the code they describe: the data layer, the view hierarchy, the data models, the test strategy, and the device rehearsal tests. Each is an architecture reference of 100 to 160 lines, and each was written with the agent as the first reader.
The test strategy document pays for itself weekly. It records the simulator facts that took days to learn: the test host crashes on an iOS 16 simulator, so use an iOS 26 one. Name-based simulator destinations can silently fail to match, so fall back to the device identifier. Parallel test execution hangs the run mid-suite at zero percent CPU until killed, so run serially. It maps every subsystem to its suites and to its seams, the places where a test can swap the real thing for a fake. It has a section called silent passes that names the two tests which return early instead of failing when the simulator lacks what they need, so the run reports greener than the coverage actually is. And it has a coverage gaps section that lists what is not tested and why: the network providers, the money paths, the camera.
The documents also correct each other. CLAUDE.md names two production fakes for the recognizer and the reader; the strategy doc notes that in practice both are preview-only and the tests define their own richer doubles. CLAUDE.md still says any available iPhone simulator works for the test command, and the strategy doc says that does not hold. That drift is a bug in my docs, and I am fixing it as part of publishing this. An agent that reads both files gets the true picture, which is more than a single tidy file would give it.
The rehearsal tests document is the strangest of the five. It describes a device-only harness that plays the actor’s lines out loud through a speaker so the sound crosses the air into the real microphone, what counts as a failure, and every finding it has produced, dated. One of them: the locale resolver picked the alphabetically first German variant when the phone’s region had none, which was de-AT, which had no dictation assets installed, so every speech task failed with “No Assistant asset for language de-AT” and not one cue fired for the whole script. Nothing in the unit suite could have found that. When the agent works on speech code, that file is the difference between a plausible change and a correct one.
771 tests as the gate
The device harness sits inside the same test target as everything else. Of the 771 tests, 41 are device-only and skipped unless an environment flag is set; the other 730 run on a simulator with no network, no microphone and no store. About 92 cover cue recognition, the part of the app that decides when an actor has finished a line.
They can run that way because the dependencies that matter for rehearsal are protocols with fakes beside them, and every screen’s composer has a compose() that wires production and a preview() that wires fakes. The coverage gaps section names the ones that are not, starting with the network providers and the money paths. That design predates the agent, but it is what makes the agent safe. A change to the matcher, the state machine or a view model gets the same answer from the same suite regardless of who typed it.
Two habits keep the gate honest. When the agent adds a test, it goes in a feature folder that already exists, using a fake defined in the same test file, the way the suite already does it. And when a run carries failures, we change what the test expects or the code under test; we never loosen the check or delete it. For two weeks in late August the run carried fourteen deterministic failures in two view model suites. They were stale expectations after a navigation debounce was added. The fix was rewriting the expectations, deleting a dead init parameter the tests had been exercising, and a small polling helper that awaits asynchronous side effects instead of asserting synchronously. The strategy doc records all three, because the next reader, human or agent, will hit the same shape of failure.
Four agents in worktrees, one afternoon, half a harvest
On September 10 I ran four agents at once for a test coverage push, each in its own git worktree and each on a slice: the import and network layer, the onboarding view models, the screen view models and composers, and self tape. The worktree keeps the agents out of each other’s files, and each was pointed at its own simulator so their test runs would not hang each other, which they will if they share one.
Sixteen days later the four branches have zero commits and all four worktrees are still dirty. Sixteen new test files sit in them. Exactly one, the import routing tests, has made it to main, copied over by hand. The rest are waiting on view model seams the agents added to make them testable, and I have not yet decided whether those seams are worth the churn. That is the honest score for a parallel sprint on a solo project: the agents finished in an afternoon, and the review is the bottleneck. The pattern works for broad, shallow work when someone has the review time budgeted. It does not work for the speech engine, which has to run on a physical phone with a microphone and a speaker, so that is one agent, one device, and me sitting next to it.
What Claude Code is for here
Reviewer first. Every change of mine gets a review pass, and the reviewer is good at exactly the things I am worst at after ten hours: an unhandled state, a missing restore in a defer, a test that passes for the wrong reason.
Second pair of hands for the broad work: the five language string catalog, analytics event schemas, documentation that stays in sync with the code, tests for the long tail of view models. That is where most of the 324 co-authored commits are.
Not the hard decisions. How many cue words must be heard before a line counts as spoken, the audio session strategy, what the paywall says. Those come from users, from the device harness and from me. The agent implements them and proves they hold.
What transfers
Everything above is a description of a good onboarding process, and teams already know how to do that. Write the manual a senior hire would need. Put the architecture notes beside the code and let them contradict each other where the code does. Audit the allow list the way you would audit production access. And make it structurally impossible for a human or an agent to ship without a person in the loop.
Scriptmate is on the App Store, built solo. The engineering behind the speech and audio work is in the case study.