Zwei Codebases, kein gemeinsamer Code, keine gemeinsame Domäne, kein
gemeinsamer Tech-Stack. Die eine ist eine Next.js-14-Pages-Router-App,
die Millionen von Listings ausliefert, Zustand-State, SCSS-Module,
Optimizely-A/B-Tests, i18n via Phrase. Die andere ist ein
multi-tenant Institutions-ERP mit 294 View-Komponenten, 239
Datenbanktabellen, einem Hono-Backend mit 74 Route-Dateien, vier
Sprachen inklusive Bengali und Gujarati und einem 14-Rollen-RBAC-Modell,
in dem jede Query eine Tenant-id tragen muss, sonst shippt sie nicht.
Ich habe für beide einen Claude-Code-Layer gebaut. Gleiche Kernidee: ein Set aus Skills, Commands, Hooks und Memory-Dateien, das Claude Code von einem fähigen Assistenten in etwas verwandelt, das eher einem zweiten Engineer gleicht, der die Regeln der Codebase bereits kennt. Als ich an beiden Projekten parallel arbeitete, hatte das Marktplatzprodukt 35 Commands, sieben geroutete Skills und einen registrierten Subagent-Vertrag; das ERP hatte 47 Skills; beide hatten 14 Hooks. Das hier ist, was ich beim Aussortieren gelernt habe, welche Teile mitreisen und welche zu Hause bleiben mussten.
Was ein Skill hier eigentlich ist
„Skill" wird lose verwendet, also hier präzise, was die sieben
Marktplatz-Skills sind. Jeder ist eine SKILL.md mit YAML-Frontmatter
und einem Prozedur-Body, und jeder einzelne ist Scanner und Fixer für
eine Domäne. Das Frontmatter trägt drei Dinge, die echte Arbeit
leisten: eine description, geschrieben als Trigger-Phrasen („Use
when: fix React hooks, scan async patterns, review performance…"),
damit das Modell natürlichsprachige Anfragen zum richtigen Skill
routen kann; eine applies_to-Glob-Liste, die deklariert, welche
Dateien den Skill interessieren — genau dagegen schneidet das
deterministische Routing; und einen Argument-Hint, damit scan versus
fix plus Scope wie ein CLI-Flag übergeben werden kann. Der Body hat
in jedem Skill dieselbe Form: Scope bestimmen (Default: geänderte
Dateien gegen main), Modus wählen (Scan ist read-only mit
Severity-Ratings; Fix wendet Änderungen an und validiert danach mit
yarn lint, tsc --noEmit und Vitest auf den betroffenen Dateien),
dann die Checks der Domäne fahren.
Die Tiefe liegt eine Ebene darunter, im references/-Verzeichnis
jedes Skills — zwei Dutzend Referenzdokumente über die sieben Skills,
unter einer expliziten Lazy-Load-Disziplin: Der Default ist, sie
nicht zu lesen. code-quality trägt sieben (Naming-Konventionen,
Dead Code, Import-Hygiene, Dateigröße und Splitting ab 200 Zeilen,
Config und Magic Numbers, Duplikation, GraphQL-Codegen) und lädt jedes
nur, wenn der Scope es rechtfertigt — die Duplikations-Checkliste bei
Multi-File-Scopes, die GraphQL-Checkliste nur, wenn generierte Typen
berührt sind. react-and-async trägt fünf: React-Patterns
(Hook-Dependencies, Stale Closures, Memory Leaks), Async-Patterns
(Abort Controller, Race Conditions, Promise-Handling), Next.js- und
Server-Patterns sowie Performance. a11y-i18n trägt die
Accessibility-Checkliste (Label-Input-Paarung, ARIA, semantisches
HTML, Tastaturnavigation), die i18n-Checkliste (hartkodierte Strings,
Locale-Sync, ungenutzte Keys) und die CSS-Checkliste (Design-Tokens,
!important, tote Klassen). security-and-testing teilt nach
Dateityp: die Security-Checkliste (XSS, Input-Validierung,
Credential-Exposure) für Quelldateien, separate Vitest-, Playwright-
und Storybook-Checklisten, die nur laden, wenn Tests, E2E-Specs oder
Stories im Scope sind, und eine Error-Handling-Checkliste, die lädt,
sobald ein try/catch oder eine Error Boundary auftaucht.
Die zwei übrigen Skills haben die stärksten Codebase-Meinungen.
feature-flags kennt die Form von toggles.ts: Es inventarisiert
jeden Toggle mit clientSideScope, Ticket-Referenz und Ablaufdatum,
markiert Toggles, die auf Seiten außerhalb ihres deklarierten Scopes
benutzt werden, findet Toggles, die mit einem TODO auf true
hartkodiert sind, und beendet im Fix-Modus ein abgeschlossenes
Experiment sauber — Check entfernen, Gewinner-Codepfad behalten,
Definition löschen, toten Code aufräumen. pr-workflow besitzt die
Hygiene: PR-Scope-Analyse, ein Pre-Commit-Quality-Gate und das
Auflösen von Review-Kommentaren. Und jeder Skill honoriert eine
gemeinsame Vertragsdatei, die den Orchestrator-Handshake festschreibt:
Hunks oder ganze Dateien als Scope akzeptieren, ihn nie erweitern,
Findings im strikten Einzeilen-Schema ausgeben und nie eine Datei
zitieren, die in diesem Turn nicht gelesen wurde.
Diese letzte Regel ist die eigentliche These des Designs. Ein Skill ist hier kein Prompt mit Namen. Er ist ein Vertrag: deklarierte Inputs, zwei Modi, begrenzter Scope, maschinell prüfbarer Output und Meinungen, die spezifisch genug für diese Codebase sind, um ihre Durchsetzung zu lohnen.
Was ohne Änderung mitgereist ist
Die Session-Boundary-Hooks zogen wörtlich um. Ein
SessionStart-Hook, der git diff --name-only main ausführt und die
geänderten Dateien ausgibt, kostet dreißig Zeilen Shell. Ich schrieb
ihn einmal für das Marktplatzprodukt und warf ihn am selben Nachmittag
ins ERP. Genauso der Stop-Hook, der tsc --noEmit laufen lässt,
wenn die Session endet und der Build kaputt ist, will ich das wissen,
bevor ich das Terminal schließe. Keiner der beiden Hooks weiß, worum
es im Projekt geht. Sie erzwingen einfach eine Disziplin, die überall
gilt.
Die Claim-Verifier-Hooks reisten genauso sauber mit, und sie
verdienen ihren Platz mehr als alles andere im Layer. Ein Stop-Hook
liest die letzte Antwort des Assistenten erneut und prüft zwei Dinge.
Erstens Zitate: Referenziert die Antwort eine Datei, die in diesem Turn
nie gelesen wurde („called from app.ts", „fixed at line 140"), wird
das als Halluzination markiert — das Read-Set kommt aus dem Audit-Log
des Laufs. Zweitens Negativ-Behauptungen: Aussagen wie „keine weiteren
Usages" oder „0 Treffer" werden als echte rg- / git grep-Checks
erneut ausgeführt, Abweichungen erscheinen als [verifier-mismatch].
Ein begleitender PreToolUse-Hook verweigert schlicht Edits in
__generated__/-Verzeichnissen — regenerieren, nicht von Hand
patchen. Nichts davon weiß irgendetwas über Marktplätze oder ERPs. Es
ist epistemische Hygiene, und die portiert sich überallhin.
Das Memory-Struktur-Pattern übertrug sich komplett. Beide Repos
haben jetzt ein memories/repo/-Verzeichnis mit fünf Dateien:
modules.md (was jeder Teil der Codebase besitzt), decisions.md
(Architekturentscheidungen und ihr Warum), conventions.md (Patterns,
die durch Lesen entdeckt wurden und nirgends dokumentiert sind),
known-flakes.md (ein Register von Testfehlern, die Rauschen sind,
kein Signal) und lessons.md, eine Append-only-Datei, die ein
/lesson-Command immer dann schreibt, wenn mich etwas eine Stunde
gekostet hat, die es nicht hätte kosten dürfen. Claude liest diese am
Anfang relevanter Sessions. Die Struktur ist identisch; die Inhalte
sind es offensichtlich nicht.
Die Linting-Post-Hooks (Prettier, ESLint, Stylelint) zogen ohne jede Änderung um. Eine Datei nach einem Edit zu formatieren ist mechanisch und projektunabhängig. Der Punkt ist: Die Hooks auf der untersten Ebene, die am nächsten am Dateisystem, sind mit weitem Abstand die portabelsten.
Was an der Naht einen Rewrite brauchte
Im Marktplatzprodukt sind /implement und /review-pr Orchestratoren,
die den Workflow besitzen und bewusst keine einzige Regel. Die Regeln
leben in sieben Domänen-Skills — code-quality, react-and-async,
a11y-i18n, security-and-testing, feature-flags, pr-workflow und
der implement-Orchestrator selbst. Jeder Skill deklariert eine
applies_to-Glob-Liste im Frontmatter und hält seine Tiefe in einem
references/-Verzeichnis, das erst gelesen wird, wenn der Skill
wirklich läuft — eine Session bezahlt nie für Regeln, die sie nicht
braucht. Das Routing bleibt nicht dem Urteil des Modells überlassen:
ein dreißigzeiliges Shell-Skript schneidet die geänderten Dateien mit
den applies_to-Globs aller Skills und gibt exakt aus, welche Skills
in welcher kanonischen Reihenfolge laufen sollen, und ein
UserPromptSubmit-Hook führt es automatisch aus und injiziert das
Ergebnis als [skills-routed: …] in den Prompt. Docs-only-Diff? Kein
Skill läuft. Findings kommen in einem strikten Einzeilen-Schema zurück
— file:line rule-id message —, weil der Orchestrator sie
nachverarbeitet, und Prosa lässt sich nicht nachverarbeiten.
Das ERP konnte nichts von diesem Routing wiederverwenden, weil sein
Risiko nicht dateityp-förmig ist, sondern domänen-förmig. Ein einziges
CLAUDE.md für 239 Tabellen und 58 Architekturregeln zu pflegen war
nach Monat zwei nicht mehr haltbar. Ich wechselte zu 58 separaten
Regeldateien in .claude/rules/, jede für ein Anliegen
(sql-safety.md, fee-ledger.md, mutation-wiring.md,
state-machines.md und so weiter), mit @-Imports, die nur die für
die aktuelle Aufgabe relevanten Regeln hereinziehen. Das
Orchestrierungs-Konzept überlebte; die Dateistruktur, die es trägt,
musste von Grund auf neu gebaut werden.
Derselbe Split passierte bei den Hooks. Im Marktplatzprodukt drehen
sich die Post-Tool-Use-Hooks um Code-Stil: ESLint, Prettier, Stylelint,
dann tsc. Im ERP laufen die zuerst, und danach laufen acht
Domain-Validator-Hooks nach jedem File-Write: check-ddl-safety.sh
(keine unsicheren Migrationsmuster), validate-service-pattern.sh
(jede Service-Datei muss sowohl den lokalen PGlite-Adapter als auch
den Remote-Hono-Adapter implementieren), check-seed-version-sync.sh
(SEED_VERSION muss nach Schemaänderungen synchron bleiben),
detect-dead-buttons.sh (kein UI-Button ohne verdrahteten Handler)
und vier weitere. So etwas schreibt man nicht für ein
Marktplatzprodukt. Die Failure-Modes des ERP sind andere, ein
fehlkonfigurierter Service-Adapter fällt in Produktion still auf
lokale Daten zurück; ein toter Button in einem Gebührenformular ist
ein Support-Ticket.
Was ich gelöscht und als etwas Besseres neu gebaut habe
Das Marktplatzprodukt hat drei A/B-Test-Commands: ab-test-kickoff.md,
ab-test-health.md, ab-test-wrapup.md. Sie kennen die IDs des
Experiment-Frameworks, die interne Flag-Namenskonvention und das
Cleanup-Pattern beim Abschluss eines Experiments. Auf dieser Codebase
sind sie wirklich nützlich. Überall sonst sind sie komplett
bedeutungslos.
Ich machte den Fehler, sie generalisieren zu wollen. Ich verbrachte einen Nachmittag damit, einen „generischen Experiment-Workflow"-Skill zu schreiben, der das A/B-Framework als Parameter akzeptierte. Niemand benutzte ihn, weil er keine Meinungen hatte, und Skills ohne Meinungen sind nicht nützlich, sie werden nur eine wortreichere Art, einen Prompt zu schreiben. Die richtige Lektion war: Die Experiment-Skills bleiben im Marktplatzprodukt, das äquivalente Problem des ERP (Feature-Rollouts hinter RBAC-Gates) braucht sein eigenes dediziertes Tooling, und ich höre auf, ein Ding für beide Jobs bauen zu wollen.
Der scan-stores.md-Command des Marktplatzprodukts ist eine ähnliche
Geschichte. Er auditiert Zustand-Store-Definitionen auf typische
Probleme: veraltete Selektoren, fehlende shallow-Equality-Checks,
State, der in eine URL gehört. Es ist ein guter Command. Das ERP nutzt
TanStack Query für Server-State und Zustand nur für flüchtigen
UI-State. Ein Wholesale-Port wäre nutzlos gewesen. Ich schrieb einen
viel schmaleren refactor-state.md-Skill, der die Patterns behandelt,
die dort wirklich vorkommen, und zog weiter.
Die eine Strukturentscheidung, die alles verändert hat
Beide Projekte stützten sich am Ende auf Subagents, und ich brauchte eine Weile, um zu merken, dass sie mit demselben Mechanismus entgegengesetzte Probleme lösen.
Im Marktplatzprodukt heißt der Split Scan versus Fix.
Scan-Mode-Skill-Läufe werden an einen read-only Explore-Subagent
dispatcht: isolierter Kontext, beschränktes Tool-Envelope,
strukturierte Findings zurück, sonst nichts. Vier Scan-Skills laufen
so parallel, weil ein isolierter Kontext, der zwölf schema-formatierte
Findings zurückgibt, strikt billiger ist als vier Skills, die die
Konversation des Orchestrators fluten. Fix-Mode läuft immer im
Haupt-Agent, weil Subagents keine Edits anwenden können. Was das Ganze
verlässlich machte, war nicht der Dispatch — es war das Register.
Jeder Subagent, den ein Command referenzieren darf, ist in einer
AGENTS.md deklariert, mit Zweck, Tool-Allowlist und
Default-Gründlichkeit, und ein Validator-Skript lässt die Config
durchfallen, wenn ein Command einen nicht registrierten Agent
referenziert. Ohne das ist ein Command, der „nutze den
Critic-Subagent" sagt, nur Prosa, die zur Dispatch-Zeit still
scheitert. Jeder Dispatch wird von einem SubagentStop-Hook geloggt
und in einem /usage-stats-Command aggregiert, sodass ich sehe,
welche Delegationen sich wirklich lohnen.
Die Subagents des ERP komponieren andersherum: Sie sind Worker
innerhalb größerer Orchestrierungen. port-to-backend.md erstellt
eine Hono-Route, verdrahtet den Middleware-Stack, aktualisiert die
Service-Datei mit einem Dual-Mode-Adapter und gibt eine Zusammenfassung
zurück, und eine Migrations-Session lässt davon fünf hintereinander
laufen. In einem Projekt mit 74 Backend-Route-Dateien und einem
strikten Adapter-Pattern, dem jede einzelne folgen muss, ist die
Fähigkeit, eine wohldefinierte Teilaufgabe an einen Agent mit
begrenzter Tool-Allowlist zu delegieren, der Unterschied zwischen
einer in zwei Stunden orchestrierten Migration und zwei Tagen
Babysitting.
Derselbe Mechanismus, zwei Jobs: Isolation für billiges paralleles Review, Delegation für mechanische Massenarbeit. Die Disziplin aus Register plus Validator ist, was beides sicher macht.
Das Tooling verdient sein Geld. Die Session-Boundary-Hooks, der Claim-Verifier, die Memory-Dateien, die Domain-Validator-Hooks im ERP, sie alle haben die Zeit zurückgezahlt, die es kostete, sie zu schreiben. Was scheiterte, war entweder zu generisch, um nützlich zu sein, oder zu spezifisch für eine Codebase, um den Umzug zu überleben. Der Skill, der reist, ist der mit starken Meinungen, die für das Problem vor ihm zufällig richtig sind. Das ist keine Regel über AI-Tooling. Das ist einfach eine Regel.