working with Claude Code

How I work

How I run Claude Code as a small engineering team. A skill suite I wrote routes every task through the same process, role-specific subagents split bigger features, QA comes with evidence, and the setup fixes itself when it turns out wrong.

Out of the box, Claude Code is one capable generalist. I treat it as a small engineering team instead. A global CLAUDE.md sets four rules: route work through my own 20-skill suite, verify instead of assuming, give every session its own Git worktree, and fix the skills when they turn out wrong. An orchestrator skill picks the workflow (feature, bug, incident) and loads only the skills each phase needs. Bigger features go through 13 subagents I defined, from a PM to a dev lead who signs off, and I approve the plan and the merge.

It’s the setup behind Avenly, Clozit and the Mobile QA agent.

4global rules in CLAUDE.md
20self-authored skills, versioned in git
13subagents I defined
2,626lines of method in skill bodies
8routing arcs, from bug fix to pricing
2human gates in the agent pipeline

What the agent sees, and when

Claude Code provides the slots: files that are always loaded, skills pulled in on demand, and subagents with their own context. Those are built in. The design is in what goes into each slot and how much of it: a small fixed core, method that loads only when a phase needs it, and bulky role work pushed out to subagents that return a short report. Built-in pieces are greyed out below.

my prompt ↓ lands in a session that already holds
Main session · Opus · 1M-token window

Always loaded

at session start · small and fixed
~/.claude/CLAUDE.md4 global rules: routing, verify first, worktrees, fix skills
<repo>/CLAUDE.mdproject conventions, such as this site's tagging rules
memory/MEMORY.mdbuilt-in auto-memory index
20 skill descriptions≤1,024 chars each; they do the routing
13 agent descriptionswhen to dispatch each subagent
→Skill toolonce per phase

Loaded on demand

when a phase or question needs it
SKILL.md bodies2,626 lines across the suite; each loaded at most once
references/ · scripts/17 docs + 2 scripts, read only during that sub-task
memory/*.mdbuilt-in; the full note behind an index line
⇄Agent toolbrief in, report out

Separate contexts

fresh window per dispatch
13 subagentsPM, planner, designers, devs, QA, dev lead and 5 specialists, each with a fixed handoff format
fresh-eyes reviewermy rule: the final check runs in a clean context, so the author never grades their own work
at close: skill fixes are committed to the skills repo
HarnessOutside the model: stock settings.json options, plus my weekly launchd cleanup script.

Where each file ends up

always in contexton demandsubagent's contextread by the harness
FileAlwaysOn demandSubagentHarnessContent
~/.claude/CLAUDE.mdmine
<repo>/CLAUDE.mdmine
skills/*/SKILL.mddescriptionbodypath, if briefedmine
skills/*/references/mine
agents/*.mddescriptionsystem promptmine
memory/MEMORY.mdbuilt-in
memory/*.mdbuilt-in
settings.jsonbuilt-in

A skill description's “for X use skill-y” clauses break ties between neighbouring skills. They're load-bearing, so I re-run a trigger test whenever one changes.

Every task runs the same loop

This is the execution-method skill’s loop. Under each step is the tool or command it actually comes down to. Building happens in a tight inner loop of edit, run, commit. What the session learns goes back to disk at the end, so the next session starts further ahead.

Before the first edit

01Route

Pick the arc and the skills it needs. Skip routing when one skill clearly matches.

Skill(orchestrator)

02Ground

Read recorded state, then check the code, schema and config. Anything checkable is never presented as an assumption.

before “assuming”: name the source, then read it

03Isolate

Own worktree and branch, so parallel sessions never share files.

git worktree add ../<repo>-<slug> -b claude/<slug>
→
Build

04Plan the whole

Contracts, seams and the index first, because boundary mistakes are the expensive ones. Each decision gets one line of why and the rejected alternative.

05 · repeat per increment
editsmallest runnable unit
→
runpass path + failure path
→
commitone per green, with why
next increment
fails?Re-read reality (the file, the exact error), fix the mental model, then retry. Never retry blind.
→
Finish

06Fresh eyes

A clean subagent verifies under real conditions, not the author's.

Agent(fresh context)

07Ship

Push the session branch and open a PR, draft if unfinished. Never push to the default branch.

08Close the loop

Lead the report with the outcome and an honest scope note, record state, tear down what was created.

skill fix + commit · worktree remove · delete simulators
memory + skill fixes → the next session's step 02

Check, default, or ask

Before writing the word “assuming”, name the source that would settle it. If you can name one, go read it instead.

This rule removed most of the friction. Every unknown goes through the same two questions. The agent stops hedging on facts it could look up, stops asking me what grep would answer, and still stops for the decisions that are mine.

an unknown
Could a source I can reach settle it?code, schema, migrations, config, docs, a command
yes →
CHECKRead it or run it. The guess becomes an observed fact in seconds.
no ↓
Is it a gate?destructive or irreversible · real spend · kill / keep · publishing
yes →
ASKStop and ask me. These are the only interruptions.
no ↓
DEFAULTTake the strongest defensible choice and state it in one line: “Defaulting to X because Y; flag if wrong.” A load-bearing default goes first in the report, not buried.

Sorting one task's unknowns

The skill's worked example: “add an AI expense categorizer” in an unfamiliar repo. Five of the seven unknowns could be settled by reading or computing, so none of them become assumptions.

UnknownBinWhy
Is the app multi-tenant?CHECKtenant_id columns in the schema answer it in seconds
Categories: per-tenant table or global enum?CHECKschema and migrations
An existing jobs pipeline to reuse?CHECKgrep before designing a new one
Cost-viable model tier?CHECKcurrent pricing × measured token counts
Send transaction text to a third-party API?DEFAULT ⚑no repo source settles policy; default to the provider's data terms, flagged as load-bearing
Sync or async categorization?DEFAULTreversible; default async, one line of why
Auto-apply above a confidence bar?ASKturns suggestions into ledger writes, which changes the blast radius
Claims are ranked by how they were obtained
observed · ran it, read it>inferred · labelled with its basis>remembered · never shown as fact

The orchestrator’s arcs

A request is matched to an arc, and each phase loads its skill only when the work actually reaches it. A typical feature loads 4–7 skills over its life, never the whole suite. Every skipped phase gets a one-line reason, so skipping is a decision that was made, not something forgotten.

The “new feature” arc

Read it top to bottom: each row is one phase, in order. The Build rows repeat for every slice. “Only if” rows are skipped when their condition fails, and the skip is logged with its reason.

#SkillRunsLeaves behind
Defineonce per feature
01product-spec-writing
only ifthe change isn’t cheap, reversible and soloelse: skip; the change is its own spec
Problem, primary user, non-goals, observable acceptance criteria, day-30 success metric. Checked with lint-spec.sh.
02product-scoping
only ifthere’s more than one sliceelse: skip
Vertical slices ordered riskiest assumption first, with the cuts decided in advance.
↻ Buildrepeats for each slice
03eng-design-doc
only ifit’s a one-way doorelse: one line in the ADR log
Design doc with real alternatives, trade-offs on the axes that differ, and failure modes.
04facet skills
only if touched
new endpoint, webhook, tool schemaeng-api-design
schema or migrationeng-data-modeling
auth, PII, money, uploadseng-security-baseline
prompts, models, agentseng-ai-features
05product-ux-flows
only ifthe slice is user-facingelse: skip
Screens with one primary action each, plus a grid of states: empty, loading, partial, error, success, denied.
06qa-test-strategy
alwaysbefore any code
A list of test cases by layer, chosen by risk, with boundary and state cases.
07eng-implementation
always
Smallest runnable increments, failure paths wired in the same pass, one commit per green test.
08eng-code-review
only ifthe diff is agent-written, risky or largeelse: ops-release’s self-read gate covers it
Correctness first, then contract, security and failure modes. Checks for hallucinated APIs and tests that can’t fail.
09ops-release
always
Pre-merge gates, rollback criteria written before deploying. Migrations run against schema N−1 and N, so rollback reverts code, never schema.
10ops-observability
alwaysin the same PR
Structured logs, the few metrics that matter, and alerts on user-visible symptoms.
Learnat launch, then at the review date
11product-metrics
my callat the review date
Review date set at launch. At the review: an explicit iterate / kill / keep decision.
↺ “iterate” goes back to 02 · product-scoping for the next cut
The ledger: one line per phase, kept in the task list
spec-writing   invoked  # day-30 metric set, lint-spec passed
scoping        skipped  # single slice
design-doc     skipped  # two-way door, one ADR line for storage
api-design     invoked  # new endpoint + async job resource
test-strategy  invoked  # case list written before code

Other arcs

SignalRoute
Bug, wrong behaviourqa-debugging → regression case via qa-test-strategy → ops-release
Production incidentqa-debugging (capture, mitigate, postmortem) → ops-observability → ops-release
“What should I build?”product-brainstorming → winner goes to product-spec-writing
“Is this secure / ready?”eng-security-baseline → ops-release gates
Pricing, tiersproduct-pricing → shipped as a launch with product-metrics thresholds
“Did it work?”product-metrics → product-scoping for the next cut

A feature costs a quarter of the suite

With 20 skills, the obvious risk is paying for all of them on every task. The routing rules prevent that. Here is what each scenario puts into the main session’s context, measured from the files (estimated at about 4 characters per token).

~4.9kfixed cost per session: 20 skill descriptions, 13 agent descriptions and the global CLAUDE.md, about 0.5% of the window
26%of the suite's method a full feature arc loads (7 of 20 skill bodies), with no reference docs unless a sub-task needs them
~13ktokens of agent instructions that never enter the main session; each loads only inside its own subagent

Where the savings come from

descriptions route, bodies executeRouting reads only the always-loaded descriptions. A body loads when its phase actually starts.
once per sessionA loaded skill is applied from context afterwards. It's re-read only if compaction dropped it or the file changed.
most specific winsA task that matches one skill goes straight to it, skipping the orchestrator's ~2.3k-token body.
reasoned skipsScoping, design docs, UX flows and code review each have a skip condition. A skipped phase costs one ledger line.
references just in time16.5k tokens of deep material load only during the sub-task that needs them.
scripts over re-derivationlint-spec.sh and flake-rate.sh run and return a result. The checks aren't re-reasoned in context each time.
distilled briefsA subagent gets the 3–5 rules that bind it, never a pasted skill body. Its noisy reads stay in its own window.
outcome-first reportsReports lead with the result and a scope note; the journey stays in the transcript. The ledger stops the next session re-deriving decisions.

A subagent team shaped like a product org

For larger features, the main session acts as a hub. It dispatches each role-specific agent with a brief, receives a structured report, and passes the relevant parts to the next stage. Subagents never call each other. Every agent reads the codebase’s existing patterns before writing anything, and two points in the pipeline wait for me.

main sessiondispatches every stage ↓ and relays the reports
Shape
product-managerasks questions, challenges assumptions, explores edge cases
A defined feature: problem, scope, trade-offs
Plan
plannerreads the code, compares approaches, assigns every task to an agent
Task plan. Flags any migration, and that proto changes need make gen
I approve the plan. Nothing is built before that.
Design
infra-designerschema and API contracts
∥
ui-designerlayouts, flows, states, real UI strings
Copy-pasteable migration SQL, proto3 messages and RPCs · a UI spec frontend-dev can build directly
Build
backend-devcodegen first, sqlc queries, auth check on every handler
∥
frontend-devloading, error and empty states, mobile, lint clean
called in here test-writer
New endpoints and query names, migrations still to run, field and enum changes the other side needs
Verify
qa-testerbrowser checks of every changed feature, with screenshots
called in here security-auditperf-audit
Self-contained HTML report, hosted and linked in a PR comment, never committed
Sign off
dev-leadcross-checks schema ↔ backend ↔ frontend, then runs build, lint and the PR
“Ready to merge ✅” or “Blocked ❌”, naming the agent that has to fix each issue
I approve the merge.
Blocked goes back to the named agent, then through sign-off again
Outside the pipeline: ci-debug when a CI run fails changelog when cutting a release

What I added on top of stock subagents

  • Read before write. Every builder agent starts by reading the project's CLAUDE.md and the neighbouring handlers, queries or components, and copies their patterns.
  • Fixed handoff formats. Each agent ends with a set list (new RPCs, query names, nullable fields, migrations still to run), so the hub passes exact facts instead of retelling them.
  • Briefs carry the rules. Subagents don't inherit loaded skills, so the brief includes the 3–5 rules that bind that agent.
  • Stack-specific guardrails. Never edit generated files, run codegen before handlers, auth check on every handler, batch queries instead of N+1.

Every claim of “it works” comes with evidence

QA here is about evidence, not a green checkmark. Five gates sit between a change and its PR comment. Each one has a defined way to fail, and none of them can quietly pass something it never checked. The web harness runs against Avenly and the iOS harness against Clozit.

before codeCases first

Test cases by layer and risk, including boundary and state cases.

no case list → no implementation
after codeHonesty check

Revert the change just made and see which test fails.

none fails → coverage is decorative; diagnose it
running appDrive it

Expected values come from a DB query or the code path, never from the UI under test.

mismatch → FAIL, with the measured values
verdictThree states

PASS FAIL UNVERIFIED

can't reach it → UNVERIFIED + reason, never PASS
deliveryVerify the verifier

curl GET, then a real browser load of the hosted report.

title, image or case card missing → re-encode, re-host
gatewhat happens when it fails

Two harnesses, one report

Web
Playwright→measure DOMgetBoundingClientRect, computed style→screenshotat the moment that proves it; 390×844 for phone
iOS
simulator build→debug launch routersample data, no login, no backend→screenshot per route+ a JSON test plan (29 so far)
Same report shape summary table + one card per case screenshots embedded as cropped JPEG, under 1 MB hosted as a page → linked in a PR comment

Case study · iOS #357

before~1 in 5full-suite runs failed on a flaky capture-flow test
→
after0 / 225runs failed: 200× the suite, 25× the full suite (12,175 tests)
  1. Proved the cause. Marking the view model @MainActor made the test build fail on main-actor state used off-actor. The race is stated by the compiler, not guessed from a flake.
  2. Fixed without collateral. Image processing stays off the main thread; only the write-back hops to main.
  3. Proved the fix by amplification, under the exact condition that produced the flake.
  4. Filed what else it found. Amplification exposed a separate timing flake (a 500 ms sleep margin). It was tracked on its own, not absorbed into this fix.

How the delivery rules were earned

Each rule in the QA agent's delivery step traces back to a real failure, recorded in the agent file next to the rule.

what went wrongthe rule now
PR #499 · #520Bare gist links showed HTML source→Publish a link that renders as a page
PR #523Reviewer asked to download a local file→A local path is a footnote, never the delivery
2026-09-22curl got 200; real browsers got an ad wall→curl proves reachability; a browser load proves the report
PR #5372.39 MB report cut to one case, still 200→Cropped JPEGs (138 KB); confirm every case card renders
standingHEAD returns 403 on working links→Always check with a GET
a green honesty checkgets diagnosed, not patched. Usually a test asserts a cheap derived value (a label, a count) while the real side effect is broken.
flake fixes need a receiptAbout 300 clean runs gives ~95% confidence at a 1% flake rate. “Passed three times” proves nothing. flake-rate.sh -n 300
review for agent failure modesPlausible but nonexistent APIs, tests that can't fail, swallowed errors, TODO stubs presented as finished.
see it, don't infer itUI is checked in a real browser at the right viewport, not from logs. A simulator pass isn't proof for device-only behaviour.

Small pipelines, strict rules

The CI itself is modest. What’s worth showing is how much each step does, and the triage the agents follow when a run goes red. The iOS pipeline ships Clozit to TestFlight; the GitHub Actions workflow is the Mobile QA agent’s.

iOS: merge to TestFlight with no manual steps

triggerPR merged
→
ci_post_clone.sh · runs on the Xcode Cloud runner
Regenerate projectXcodeGen from project.yml
Stamp build numberfrom the CI counter; TestFlight rejects duplicates
Pin packagespackage resolution is blocked on the runner, so a committed Package.resolved goes in
Write “What to Test”from the merged PR's feat / fix / perf commits; internal scopes filtered; never fails the build
→
resultTestFlight buildwith tester notes

When a run goes red

CI test failedci-debug agent · qa-flaky-tests skill
fails on every CI run, never locally
Not a flake. Diff the environments: cores, locale, timezone, container, versions.
fails sometimes
  1. Capture first: log, seed, runner, co-scheduled tests
  2. Amplify for a repro rate: flake-rate.sh -n 50
  3. Match the signature to a cause class
  4. Fix the class: condition waits, isolated fixtures, injected clock
  5. Receipt: -n 300 clean
can't fix now → quarantine with an owner, a deadline and an issue, or delete it
an external dependency is unreliable
Retry that one step only, with the retry rate logged and alerted on growth.
Never: blanket test-level retries. They ship product races behind a green check.
The test workflow in mobile-qa-agent, annotated
# Unit tests on every push and PR. No devices or API keys:
# the suite runs on fakes. Live device runs and the model
# eval suite are manual (they need simulators and cost money).
permissions:
  contents: read                                ← ①
strategy:
  fail-fast: false
  matrix: { python-version: ["3.12", "3.13"] }  ← ②
steps:
  - uses: actions/checkout@11bd719…             ← ③
  - uses: actions/setup-python@0b93645…
  - run: python -m pytest -q                    ← ④

What each line buys

  • ① The token can read the repo and nothing else, so a compromised step can't push.
  • ② Both supported Pythons; one failing version doesn't hide the other's result.
  • ③ Actions pinned to commit SHAs, not movable tags, so an upstream retag can't change what runs.
  • ④ Fakes only: free, fast, no secrets in CI. The expensive checks are deliberate and manual.

The budget behind it: the pre-merge suite stays under ~10 minutes, and slower tiers run on a schedule.

The setup improves itself as I use it

The skills are living documents in their own Git repo. When real use shows a skill gave wrong or missing guidance, or I correct an approach it prescribed, the fix goes into that skill in the same session: one gap, one targeted edit, one commit that says why. Never a batch rewrite.

during a sessionUseA skill guides real work.
→
triggerGapReality disagrees, or I correct the approach.
→
same sessionEditOne targeted change to that one skill.
→
guardrailsVerifyName check; a trigger test if a description changed.
→
skills repoCommitThe message says why.
the next session loads the corrected skill

Every commit to the suite so far

Each fix came from a real session. A gotcha specific to one project, such as an iOS Safari quirk, goes to that project's built-in memory instead. Only lessons that apply everywhere change a skill.

Guardrails on every skill edit
# frontmatter contract
name == folder name
description ≤ 1024 chars: what + when + triggers
body ≤ ~500 lines, depth goes to references/

# validate after any edit
for d in */; do
  name=$(awk '/^name:/{print $2; exit}' "$d/SKILL.md")
  [ "$name" = "${d%/}" ] || echo "NAME MISMATCH: $d"
done

# after a description change: trigger test
fresh subagent + all 20 descriptions + ~6 prompts
→ right skill fires, no neighbour steals it

Cleanup for a worktree per session

A worktree per session keeps parallel sessions apart, but they pile up fast, along with simulators (1.5–3 GB each) and Playwright browser profiles. A launchd job runs ~/.claude/scripts/disk-cleanup.sh every Sunday at 11:00. A worktree is removed only after passing every check, and each keep is logged with its reason.

12worktrees foundrun of 2026-10-11
lsof -d cwda process is running inside
4 kept
git status --porcelainuncommitted changes
2 kept
rev-list HEAD --not --remotesunpushed commits
0 kept
index mtime / last commitactive in the last 3 days
2 kept
4removed with git worktree remove, never --force; branches kept3 were leftover subagent worktrees · ~2.2 GB freed

Also cleared in the same run: iOS simulators shut down for 3+ days, and Playwright MCP profiles (one per worktree path) that no running browser uses. --dry-run prints the plan first. The rest of the environment is stock: Opus with 1M context, auto permission mode, Remote Control, and official plugins such as superpowers, playwright and code-review.