All docs
JournalBehind the build

Operating rules — working with an AI agent as a partner

Four standing rules that decide what Claude Code may do on vibi without asking: which commands it can run, which defaults it holds while writing code, and what it is obliged to write down. Two came from an instruction given in the moment; two came from the same mistake recurring until it earned a rule. Each is written up with whichever of the two produced it, and with the artifact that enforces it.

A rule set that only says no describes a leash, not a partner. So this note has two halves — what the agent is handed, then where it stops. The routing machinery behind the first half lives in claude-toolbox/; this note covers the boundary.

What the agent runs unsupervised

  • Routing its own work. Neither I nor a wrapper script decides whether a task is BFF or mobile. The two subagent description fields carry their own territory, and Claude Code dispatches on them (claude-toolbox/agents.md).
  • Designing cross-directory changes. /plan produces the full six-phase implementation plan — impact scope, BFF-first contract, mobile mapping, verification gates, risk check — before a human reads a line of it (claude-toolbox/commands.md).
  • Verifying the contract. /sync-api cross-checks BFF routes against the mobile client on five axes. It is the enforcement point for "the BFF is the source of truth," and it runs without supervision.
  • Reviewing its own diff. /review picks the per-directory checklist and applies it.
  • Editing code in the directory it was dispatched to.

What is not on that list is narrow and specific: running builds, and deciding anything the four rules below already decide.

Rule 1 — No automatic gradle/xcodebuild/simctl invocation

Unless the user explicitly says "build it" / "run the tests", Claude Code only proposes build/run commands.

Where it came from: no incident. A standing instruction, given directly — builds happen when I say so — with two stated reasons: time spent, and context noise.

Why it stuck: the benefit of an unrequested build accrues to the agent, which gets to check its own work; the cost lands on the user. Even a single compile check to verify a change is skipped unless it was asked for.

Application:

  • Building to verify after a code change is user-triggered.
  • Release gates like /ship only produce a checklist — a human runs the gradle commands, and Claude grades the output afterward (claude-toolbox/commands.md § /ship).

This is the first rule that breaks out of the "AI agent = one click and it's done" illusion. Execution stays in human hands, and Claude focuses on proposing and grading.

Rule 2 — Simple v1, secrets never hardcoded

The first-pass plan is the smallest unit — large design spaces are not handled in one shot. Secrets and IDs always come from env / properties / xcconfig, never as constants in code.

Where it came from: no incident either. Both halves arrived as one piece of feedback during the Google OAuth + splash screen plan (2026-05-07) — the plan was too broad, and nothing should be hardcoded.

Why it stuck: neither half was a disaster averted; both were a preference stated once and then generalized into a default. A plan padded with nice-to-haves can't be verified in one pass, and a client ID written as a constant is a leak that hasn't happened yet.

Application:

  • Nice-to-haves are excluded from a v1 by default — parallel platform work, interface splits, auxiliary routes, optional ViewModel separation. The gate plus the core flow, nothing else.
  • Every secret has a designated injection path: BFF client IDs / API keys / signing secrets → .env; iOS OAuth client ID and BFF base URL → gitignored Configs/*.xcconfigInfo.plist $(KEY) placeholder → Bundle.main.object(forInfoDictionaryKey:); Android secrets → local.properties → BuildConfig or Koin property.
  • One trap worth naming: a URL in an .xcconfig breaks on its own //, which the format reads as a comment. Write it as http:$()//host/.
  • Temporary changes for demos or debugging (e.g. auth bypass) get a restore-before-commit note pinned in memory.

Rule 3 — Call frequency gets asked before the implementation

Before proposing an implementation, the agent has to answer how often this runs. Flow onEach bodies and mutation handlers default to in-memory; heavy player instances are capped at one per screen.

The incident: two of them, in one sitting, both written by the agent and neither flagged by it. InputViewModel.observeAllProjects looked up segments serially inside a Flow onEach, and the draft list built a full video player instance per card. Separately, TimelineViewModel.pushUndoState fanned out to seven async repository calls on every mutation — while _uiState.value already held the same data in memory.

How it was caught: not by profiling, and not by the agent. The user said the app felt slow again. That is the uncomfortable part of this one — the detection layer was a human noticing lag, and it stayed that way through both mistakes.

Why it stuck: for the same code, how often it is called is half the decision, and it is exactly the thing a diff does not show. Both of the above read as perfectly ordinary code in review; only the call site made them expensive. Pinning call frequency as the first question makes the difference visible before the code exists.

Application:

  • A repository call inside a Flow onEach, a mutation handler, or a Composable is suspect until proven necessary. If the same data is already mirrored in _uiState.value, read it from there.
  • N items get coroutineScope { items.map { async { … } }.awaitAll() }, not a serial for-loop.
  • Media previews in cards and lists are static thumbnails, never player instances.
  • A lightweight lookup gets a LIMIT 1 single-column DAO method, not getByProjectId(...).first() hydrating every row.
  • An existing serial or fan-out pattern is not evidence of intent — check whether it is deliberate before matching it.

Rule 4 — Known-bug logging policy

The moment the same pitfall is hit twice, it gets appended as a pattern — iOS/KMP patterns in vibi-mobile/shared/.claude/skills/ios-kn-patterns.md, BFF patterns in vibi-bff/CLAUDE.md "Known BFF bug patterns". That section is read before writing new K/N code or a new multipart endpoint.

The incident: the NSURL absolute-path pitfall (ios-pitfalls-with-kmp.md § Pitfall 1) recurred three timesIosVideoMetadataExtractor, VideoPlayer.ios.kt, then IosMediaJobUploader. The first diagnosis cost double-digit hours because the API returns an invalid object instead of nil. The second and third cost their own debugging sessions, because nothing recorded the first.

Why it stuck: the goal is not to shorten debugging, it is to not debug again. It also took three occurrences rather than two before the rule was written — the honest version of "hit it twice, patternize it" is that the policy itself formed a recurrence late.

Application:

  • Patterns stack in one place per domain, so starting new K/N work means reading exactly one file. The iOS list holds seven (ios-pitfalls-with-kmp.md); the BFF keeps its own under "Known BFF bug patterns".
  • A new entry is a fixed shape: symptom / cause / solution pattern / the list of sites it is applied at.
  • The iOS list started as a section inside vibi-mobile/shared/CLAUDE.md and moved to its own skill file in 6fd27f8 (2026-05-11) once it outgrew being auto-loaded on every turn.

How the rules are enforced

Rules that live only in prose are resolutions. These are attached to artifacts:

RuleWhat holds it
1Stated in per-user memory; /ship is written as a checklist that emits commands rather than running them
2.env.example is a boot-prerequisites checklist, not a config manual (cloud-run-deploy-journey.md § Pitfall 6)
3Per-user memory, surfaced as a required question in the /plan risk-check phase
4The pattern files themselves — one is a skill (auto-exposed inside vibi-mobile/shared/), one is a CLAUDE.md section (auto-loaded in vibi-bff/)

There is also a blunt boundary underneath all four: each subagent declares tools: Bash, Read, Edit, Write, Grep, Glob and nothing else. Web tools and other MCP servers are excluded by omission (claude-toolbox/agents.md).

The meta — these are not produced by being taught

None of the four came from reading an official guide and applying it. But they didn't all come from disasters either, and it's worth being precise about the split: Rules 1 and 2 were instructions — stated once, in the middle of unrelated work, then generalized into a standing default. Rules 3 and 4 were recurrences — the same class of mistake showing up enough times to be worth a rule instead of a fix.

The second kind is the expensive kind. A preference costs one sentence to capture; a recurrence costs however many debugging sessions it took to notice the pattern — three, for Rule 4.

What this means:

  • A new project also starts from zero rules — copying the four above from day one will not fit some of them. A rule earns its keep only after the situation it guards against actually shows up.
  • Say which kind each rule is — an instruction can be revised by asking the person who gave it. A recurrence can only be revised by evidence. Conflating the two makes a rule set look more principled than it is, and harder to change later.
  • You can carry rules by shape, not literal text — "Known-bug logging policy" is tied to vibi's specifics, but patterning repeated pitfalls into a single place is a shape that applies anywhere.
  • The location of the rule set is itself a rule — vibi keeps them in per-user memory (~/.claude/projects/.../memory/). Pinning them in the workspace CLAUDE.md is the alternative; I split along the boundary between personal workflow and project policy, so a teammate cloning the repo inherits the project half without inheriting my habits.