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.
CLAUDE.mdWhat 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.
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 rulesmemory/MEMORY.mdbuilt-in auto-memory index20 skill descriptions≤1,024 chars each; they do the routing13 agent descriptionswhen to dispatch each subagentLoaded on demand
when a phase or question needs itSKILL.md bodies2,626 lines across the suite; each loaded at most oncereferences/ · scripts/17 docs + 2 scripts, read only during that sub-taskmemory/*.mdbuilt-in; the full note behind an index lineSeparate contexts
fresh window per dispatch13 subagentsPM, planner, designers, devs, QA, dev lead and 5 specialists, each with a fixed handoff formatfresh-eyes reviewermy rule: the final check runs in a clean context, so the author never grades their own worksettings.json options, plus my weekly launchd cleanup script.Where each file ends up
| File | Always | On demand | Subagent | Harness | Content |
|---|---|---|---|---|---|
| ~/.claude/CLAUDE.md | mine | ||||
| <repo>/CLAUDE.md | mine | ||||
| skills/*/SKILL.md | description | body | path, if briefed | mine | |
| skills/*/references/ | mine | ||||
| agents/*.md | description | system prompt | mine | ||
| memory/MEMORY.md | built-in | ||||
| memory/*.md | built-in | ||||
| settings.json | built-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.
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 it03Isolate
Own worktree and branch, so parallel sessions never share files.
git worktree add ../<repo>-<slug> -b claude/<slug>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.
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 simulatorsCheck, 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.
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.
| Unknown | Bin | Why |
|---|---|---|
| Is the app multi-tenant? | CHECK | tenant_id columns in the schema answer it in seconds |
| Categories: per-tenant table or global enum? | CHECK | schema and migrations |
| An existing jobs pipeline to reuse? | CHECK | grep before designing a new one |
| Cost-viable model tier? | CHECK | current 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? | DEFAULT | reversible; default async, one line of why |
| Auto-apply above a confidence bar? | ASK | turns suggestions into ledger writes, which changes the blast radius |
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.
lint-spec.sh.only if touched
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
| Signal | Route |
|---|---|
| Bug, wrong behaviour | qa-debugging → regression case via qa-test-strategy → ops-release |
| Production incident | qa-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, tiers | product-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).
Where the savings come from
lint-spec.sh and flake-rate.sh run and return a result. The checks aren't re-reasoned in context each time.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.
make genWhat 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.
Test cases by layer and risk, including boundary and state cases.
Revert the change just made and see which test fails.
Expected values come from a DB query or the code path, never from the UI under test.
PASS FAIL UNVERIFIED
curl GET, then a real browser load of the hosted report.
Two harnesses, one report
Case study · iOS #357
- Proved the cause. Marking the view model
@MainActormade the test build fail on main-actor state used off-actor. The race is stated by the compiler, not guessed from a flake. - Fixed without collateral. Image processing stays off the main thread; only the write-back hops to main.
- Proved the fix by amplification, under the exact condition that produced the flake.
- 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.
flake-rate.sh -n 300Small 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
ci_post_clone.sh · runs on the Xcode Cloud runner
project.ymlPackage.resolved goes inWhen a run goes red
ci-debug agent · qa-flaky-tests skill- Capture first: log, seed, runner, co-scheduled tests
- Amplify for a repro rate:
flake-rate.sh -n 50 - Match the signature to a cause class
- Fix the class: condition waits, isolated fixtures, injected clock
- Receipt:
-n 300clean
# 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.
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.
# 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.
lsof -d cwda process is running insidegit status --porcelainuncommitted changesrev-list HEAD --not --remotesunpushed commitsindex mtime / last commitactive in the last 3 daysgit worktree remove, never --force; branches kept3 were leftover subagent worktrees · ~2.2 GB freedAlso 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.