Der Begriff Harness Engineering hat sich in den letzten Monaten etabliert. Die Formel dahinter ist einfach: Agent = Modell + Harness. Der Harness ist alles, was einen KI-Agenten ausmacht, ausser dem Modell selbst. Dazu gehören Instruktionsdateien wie CLAUDE.md oder AGENTS.md, Skills, Tools, Tests, Linter, Hooks und CI-Gates.

Der Kern von Harness Engineering ist eine Verschiebung des Fokus. Wir hören auf, dem Agenten im Prompt zu sagen, was er tun soll, und beginnen, das System um den Agenten herum so zu gestalten, dass er gar nicht anders kann. Ein Satz wie „halte dich an unsere Architektur“ im Prompt ist eine Hoffnung. Ein Test, der den Build bricht, ist eine Garantie.

Im AI Unified Process ist der Harness eine von drei Schichten. In diesem Beitrag zeige ich anhand des Projekts aiup-petclinic, wie die Schichten zusammenspielen, warum das CLAUDE.md dort bewusst klein ist, warum die Architektur-Dokumente die eigentliche Arbeit leisten, und wo Tests aufhören und Hooks beginnen.

Die drei Schichten des AI Unified Process

Der AI Unified Process unterscheidet drei Schichten:

What: Was soll das System tun? Hier leben die Anforderungen, das Use-Case-Diagramm, die Use-Case-Spezifikationen, die Geschäftsregeln und das Entity-Modell. Diese Artefakte sind technologieneutral. Sie beschreiben das Verhalten aus der Sicht der Benutzer und des Geschäfts.

Harness: Wie stellen wir sicher, dass der Agent das Richtige richtig baut? Hier leben die Instruktionsdateien, die Skills, die Architektur-Dokumente, die Teststrategie, die Hooks und die CI-Gates.

How: Der eigentliche Code, die Migrationen, die Tests. Das ist der Output des Agenten.

Die Reihenfolge ist wichtig. Ohne What weiss der Agent nicht, was er bauen soll. Ohne Harness baut er es so, dass es funktioniert, aber nicht zu unserem System passt.

Grössere Instruktionsdateien sind nicht besser

Die verbreitete Annahme lautet: Je mehr Kontext der Agent hat, desto besser arbeitet er. Also landet alles im CLAUDE.md: Projektbeschreibung, Verzeichnisbaum, Architektur, Coding-Konventionen, Testregeln, Befehle.

Eine Studie der ETH Zürich vom Februar 2026 (Gloaguen, Mündler, Müller, Raychev, Vechev: „Evaluating AGENTS.md“) hat das empirisch geprüft. Die Resultate sind ernüchternd. Automatisch generierte Kontextdateien senkten die Erfolgsrate der Agenten im Durchschnitt um 3 Prozent und erhöhten die Kosten um über 20 Prozent. Von Menschen geschriebene Dateien brachten nur etwa 4 Prozent, bei einem ähnlichen Kostenanstieg. Und die Agenten haben die Anweisungen befolgt. Das Problem war nicht mangelnder Gehorsam, sondern dass die Anweisungen Rauschen, redundante Schritte und unnötige Einschränkungen hinzufügten.

Ein Detail der Studie ist interessant: Als die Forscher die bestehende Dokumentation aus den Repositories entfernten, halfen die Kontextdateien plötzlich. Die Schlussfolgerung: Eine Kontextdatei ist nützlich, wenn sie Wissen liefert, das der Agent sonst nirgends findet. Sie ist nutzlos, wenn sie wiederholt, was schon in der Dokumentation steht, und sie ist schädlich, wenn sie den Kontext mit Ballast füllt.

Für Harness Engineering heisst das: Das CLAUDE.md ist keine Wissensdatenbank. Es ist eine Landkarte.

Das CLAUDE.md im PetClinic-Projekt

Das CLAUDE.md in aiup-petclinic ist rund 170 Zeilen lang und enthält im Wesentlichen vier Dinge:

  1. Was die Quelle der Wahrheit ist. docs/ ist die Quelle der Wahrheit, nicht der Code. Wenn ein Use Case und der Code sich widersprechen, gewinnt der Use Case. Und welche Sensoren diese Behauptung prüfen.
  2. Den Stack und die Befehle. Java 25, Spring Boot 4.1, Vaadin 25.2, jOOQ 3.21, Flyway, PostgreSQL. Wie man baut, testet und die jOOQ-Klassen neu generiert. Das ist Wissen, das der Agent sonst nirgends findet.
  3. Wann welches Dokument zu lesen ist. Vor der Implementierung eines Use Case: die Use-Case-Spezifikation. Vor jeder Änderung in src/main/java: docs/architecture/development.md. Vor jedem Test: docs/architecture/testing.md. Vor Änderungen an .claude/: der Abschnitt über die Agent Guardrails.
  4. Welche Skills es gibt. Die Skills des AI Unified Process sind aufgelistet, damit der Agent sie der Ad-hoc-Generierung vorzieht.

Was nicht im CLAUDE.md steht: die Architekturregeln selbst. Kein Package-Layout, keine jOOQ-Patterns, keine Vaadin-Konventionen. Die stehen in den Architektur-Dokumenten, und der Agent liest sie nur, wenn er sie braucht.

Das ist der Unterschied zwischen Kontext, der immer geladen ist, und Kontext, der bei Bedarf geladen wird. Das CLAUDE.md wird in jeder Session in den Kontext geladen. Das Architektur-Dokument nur, wenn der Agent Code schreibt. Beim Schreiben einer Use-Case-Spezifikation belastet es den Kontext nicht.

Das muss man aktiv verteidigen. Als die Traceability-Sensoren hinzukamen (dazu später mehr), wuchs das CLAUDE.md um einen Absatz, der erklärte, was jeder Sensor prüft. Der Absatz war korrekt, aber er wiederholte, was testing.mdausführlicher sagt. Im nächsten Commit wurde er auf wenige Sätze und einen Link gekürzt: Es gibt drei Sensoren, die Status:-Zeile ist deshalb eine Behauptung und kein Etikett, und was jeder Sensor prüft, steht in testing.md. Die Landkarte zeigt, wo das Wissen ist. Sie ist nicht das Wissen.

Das Software Architecture Document als Bindeglied

Im Rational Unified Process war das Software Architecture Document das zentrale Dokument der Elaboration-Phase. Viele Teams haben es in den letzten Jahren fallen lassen. Zu viel Aufwand, niemand las es, nach drei Monaten war es veraltet.

Mit KI-Agenten ändert sich die Rechnung. Der Agent liest das Dokument, bei jeder Aufgabe, die Code berührt. Und er hält sich daran, solange das Dokument kurz und konkret ist.

Im PetClinic-Projekt liegt die Architektur in docs/architecture/, geschrieben als die 4+1-Sichten, die Philippe Kruchten 1995 beschrieben hat. Die Use-Case-Sicht ist das „+1“ und liegt ausserhalb des Ordners, weil sie die Basis ist, auf der die Architektur aufbaut. Die anderen vier Sichten beantworten je eine Frage:

SichtFrageDokument
LogicalWelche fachlichen Bausteine gibt es?logical.md
ProcessWie verhält sich das System zur Laufzeit?process.md
DevelopmentWie ist der Code organisiert, gebaut und ehrlich gehalten?development.md und testing.md
PhysicalWo läuft das System?physical.md

Ein README.md sagt, welches Dokument welche Frage beantwortet, und elf ADRs halten die Entscheidungen hinter den Sichten fest. Für alle diese Dokumente gelten drei Regeln: Text statt Bilder, also Mermaid oder PlantUML für Diagramme; im Repository statt daneben, damit Architektur und Code im gleichen Pull Request ändern; und kurz statt vollständig, denn was nicht geschrieben steht, wird erfunden, und was zu lang geschrieben steht, wird überflogen.

Für den Agenten ist development.md die wichtigste Sicht. Sie beantwortet genau die Fragen, die der Agent sonst mit dem beantworten würde, was er im Training gesehen hat:

  • Package-Struktur: Package-by-Feature unter ai.unifiedprocess.petclinic. Jedes Feature (owner, pet, visit, vet, welcome) hat die Sub-Packages ui und domain. Keine separate Service-Schicht, keine DTO-Schicht, ausser ein Use Case verlangt es.
  • Datenzugriff: jOOQ, kein JPA, keine Spring-Data-Repositories. Records werden mit Records.mapping(Type::new) gemappt, nie mit fetchInto. Verschachtelte Records verwenden row(...).mapping(Nested::new), Parent-Child-Beziehungen multiset, um N+1 zu vermeiden.
  • Persistenz-Stereotyp: Klassen heissen <Entity>Repository, sind mit @Repository annotiert, damit Springs Exception Translation greift, und sie sind die Transaktionsgrenze. Eine View ist nie transaktional.
  • Vaadin-Konventionen: Eine View pro Use Case. Jede Route rendert innerhalb von MainLayout. Styling nur über LumoUtility, nie über getStyle().set(). Validierung im Formular, nicht im Domain-Record.
  • Cross-Feature-Regel: Zugriff auf ein anderes Feature nur über dessen domain-Package, mit einer klar definierten Ausnahme für Routing-Tokens und Route-Parameter-Holder.

Ohne dieses Dokument hätte der Agent wahrscheinlich OwnerController, OwnerService, OwnerRepository und OwnerDTO gebaut. Technisch korrekt, aber eine andere Architektur, und nach zwanzig Use Cases ein Projekt, das niemand mehr versteht.

Die Geschäftsregeln haben ein eigenes Dokument bekommen. Eine Regel, die zu einem einzelnen Use Case gehört, bleibt in diesem Use Case als BR-NNN. Eine Regel, die mehrere Use Cases teilen, steht einmal in docs/business_rules.md als GR-NNN, und jeder Use Case referenziert sie, statt sie zu wiederholen. Der Grund ist derselbe wie beim CLAUDE.md: Jede Tatsache hat genau ein Zuhause.

Vom Dokument zum Harness: Guides und Sensoren

Ein Architektur-Dokument allein ist noch kein Harness. Es wird erst zum Harness, wenn wir es in zwei Formen operationalisieren.

Guides: Das Dokument lenkt den Agenten

Guides lenken den Agenten vor der Arbeit. Im PetClinic-Projekt gibt es zwei Stufen:

  • Das CLAUDE.md verweist auf die Architektur-Dokumente und sagt, wann sie zu lesen sind.
  • Der Skill aiup-vaadin-jooq:implement kennt die Development-Sicht bereits und wendet die Konventionen bei der Implementierung an.

Der Skill ist im Wesentlichen ein ausführbarer Auszug aus dem Architektur-Dokument. Er sagt nicht nur, dass es ein ui– und ein domain-Package gibt. Er zeigt, wie eine View aussieht, wie ein Repository aussieht und wie beide getestet werden.

Sensoren: Das System prüft den Agenten

Sensoren prüfen nach der Arbeit, ob der Agent die Regeln befolgt hat. Hier wird das Architektur-Dokument zu etwas, das den Build bricht.

Der wichtigste Sensor im PetClinic-Projekt ist ArchitectureTest, ein ArchUnit-Test im Root-Package. Er ist die ausführbare Version von development.md. Jede Regel nennt in ihrer because-Klausel das Dokument, aus dem sie stammt, sodass ein Fehler direkt zurückzeigt:

@AnalyzeClasses(
        packages = "ai.unifiedprocess.petclinic",
        importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {

    @ArchTest
    static final ArchRule featuresHaveOnlyUiAndDomain =
            classes()
                    .that().resideInAPackage("ai.unifiedprocess.petclinic.(*)..")
                    .and().resideOutsideOfPackage("..core..")
                    .should().resideInAnyPackage("..ui..", "..domain..")
                    .because("development.md: each feature has exactly the sub-packages ui and domain");

    @ArchTest
    static final ArchRule noJpaNoSpringData =
            noClasses()
                    .should().dependOnClassesThat()
                    .resideInAnyPackage("jakarta.persistence..", "org.springframework.data..")
                    .because("development.md: jOOQ only, no JPA, no Spring Data");

    @ArchTest
    static final ArchRule noFetchInto =
            noClasses()
                    .should().callMethodWhere(
                            JavaCall.Predicates.target(HasName.Predicates.name("fetchInto")))
                    .because("development.md: use Records.mapping(Type::new) for compile-time column checking");

    @ArchTest
    static final ArchRule repositoriesAreNamedAndAnnotated =
            classes()
                    .that().haveSimpleNameEndingWith("Repository")
                    .should().beAnnotatedWith(Repository.class)
                    .andShould().resideInAPackage("..domain..")
                    .because("development.md: @Repository enables exception translation");

    // ... weitere Regeln für Transaktionen, Domain-Records, Views und Styling
}

Der Test deckt die Package-Struktur, das Fehlen von Service- und DTO-Schichten, die Domain-als-Grenze-Regel, das jOOQ-Mapping, den Persistenz-Stereotyp, die Transaktionsgrenze, die Serialisierbarkeit der Session und die Vaadin-Konventionen ab. Und das Dokument zeigt zurück auf den Test. Der erste Absatz von development.md sagt: Das meiste, was folgt, wird von ArchitectureTest durchgesetzt. Wer eine Konvention ändert, ändert die Regel im gleichen Commit. Eine Regel, die dem Dokument widerspricht, ist die Regel, die falsch ist.

Damit schliesst sich das Paar. Das Dokument ist die lesbare Form, der Test die ausführbare. Der Agent liest beides, und der Build prüft, dass sie übereinstimmen.

Beim Schreiben des Tests sind zwei Dinge passiert, die den Wert dieses Ansatzes zeigen:

  • Die Regel „keine Zyklen zwischen Features“ war als Prosa plausibel, als Test aber falsch. Das Dokument erlaubt ausdrücklich, dass die ui eines Features in die domain eines anderen greift und andere Views als Routing-Tokens referenziert. Auf Feature-Ebene ist der Graph deshalb absichtlich zyklisch. Der Test prüft nur die domain-Packages auf Zyklen. Das Dokument war ungenau, und erst der Test hat das sichtbar gemacht.
  • Die Regel „kein getStyle().set()“ liess sich nicht mit callMethod(HasStyle.class, "getStyle") prüfen, weil ArchUnit den konkreten Komponententyp als Ziel sieht, nicht das Interface. Die Regel braucht ein Prädikat auf assignableTo(HasStyle.class). Ein Detail, das man erst beim Ausführen lernt.

Ein drittes ist später passiert. Die Regel, dass ein Browserless-Test *Test heissen muss und ein Playwright-Test *IT, stand eine Weile als Prosa im CLAUDE.md. Ein falsch benannter Test wird stillschweigend nie ausgeführt, die schlimmste Art von Fehler. Sie wurde zu TestLayerConventionsTest, zwei ArchUnit-Regeln, die den Build brechen. Wo immer eine Regel als Test ausgedrückt werden kann, sollte sie einer sein.

Drei Sensoren auf der What-Schicht

ArchUnit prüft die How-Schicht: Ist der Code so gebaut, wie es das Architektur-Dokument verlangt? Der AI Unified Process behauptet aber mehr. Er sagt, docs/ sei die Quelle der Wahrheit. Lange Zeit war diese Behauptung nicht prüfbar. Ein Use Case konnte Status: Done sein, während ein alternativer Ablauf oder eine Geschäftsregel nie getestet wurde. Ein Test Case konnte Status: Automated sein, ohne dass ein Journey-Test dahinterstand. Und wenn jemand einen Ablauf in der Spezifikation umbenannte, zeigte die Annotation im Test stillschweigend ins Leere.

Drei Tests schliessen diese Lücke, einer pro Dokumentfamilie. Alle lesen die Dokumente von der Platte, und alle melden jede Verletzung auf einmal, sodass der Fehler wie eine Arbeitsliste zu lesen ist.

UseCaseTraceabilityTest liest jedes docs/use_cases/UC-*.md und jede @UseCase-Annotation auf dem Test-Classpath und vergleicht die beiden in beide Richtungen:

  • Referenzielle Integrität, für jeden Use Case, unabhängig vom Status. Jede Annotation muss auf etwas zeigen, das existiert: die id auf eine Spezifikationsdatei, das scenario auf eine ### A1: ...-Überschrift, jeder businessRules-Eintrag auf eine ### BR-NNN: ...-Überschrift. Wer einen alternativen Ablauf in der Spezifikation umbenennt, bricht den Build, bis die Annotationen folgen.
  • Abdeckung, nur für Use Cases mit Status: Done oder Tested. Ein solcher Use Case braucht einen Test für das Hauptszenario, für jeden alternativen Ablauf und für jede Geschäftsregel. Alle anderen Status sind ausgenommen, weil das Projekt die Spezifikation vor dem Code schreibt, und ein noch nicht implementierter Use Case ein normaler Zwischenzustand ist, kein Defekt.
@Test
void completedUseCasesCoverEveryAlternativeFlow() {
    List<String> violations = new ArrayList<>();
    completedSpecifications().forEach(spec -> {
        Set<String> tested = testedScenarios(spec.id()).collect(toSet());
        spec.alternativeFlows().stream()
                .filter(flow -> !tested.contains(flow))
                .forEach(flow -> violations.add(spec.id() + " is '" + spec.status()
                        + "' but alternative flow \"" + flow + "\" has no test."
                        + " Annotate one @UseCase(id = \"" + spec.id()
                        + "\", scenario = \"" + flow + "\")"));
    });
    assertNoViolations("Alternative flows of completed use cases without a test", violations.stream());
}

TestCaseTraceabilityTest macht dasselbe für docs/test_cases/TC-*.md. Ein Test Case ist eine Benutzerreise über mehrere Use Cases, im PetClinic-Projekt zum Beispiel TC-001: einen Besitzer registrieren, ihn wiederfinden, die Details ansehen, ein Haustier hinzufügen, einen Besuch buchen. Der Journey-Test TC001NewOwnerFirstVisitIT trägt eine @TestCase-Annotation auf der Klasse:

@TestCase(id = "TC-001", useCases = {"UC-003", "UC-004", "UC-005", "UC-007", "UC-009"})
class TC001NewOwnerFirstVisitIT extends AbstractBasePlaywrightIT {

Der Sensor prüft, dass ein Test Case mit Status: Automated eine TC<NNN><Name>IT-Klasse hat und umgekehrt, dass jede solche Klasse ein Dokument hat, dass die id mit dem Klassennamen übereinstimmt, und vor allem: dass die Liste in useCases genau die Use Cases nennt, die die Flow-Tabelle des Dokuments verlinkt, in beide Richtungen. Die Liste ist das Einzige, was der Klassenname nicht ausdrücken kann, und deshalb das Einzige, was sich zu prüfen lohnt.

Dazu kommt eine Regel über die Schichten hinweg: Ein automatisierter Test Case darf nur durch Use Cases laufen, die selbst Done oder Tested sind. Ein grüner End-to-End-Test über einen Use Case im Status Draft bedeutet, dass eine der beiden Statuszeilen lügt.

BusinessRuleTraceabilityTest schliesst den Kreis zwischen docs/business_rules.md und den Use Cases. Jede GR-NNN, die ein Use Case referenziert, muss existieren. Die Realized by:-Zeile einer geteilten Regel muss genau die Use-Case-Regeln nennen, die sie referenzieren. Die Übersichtstabelle muss mit den Regeln darunter übereinstimmen. Und eine Regel, die nur ein Use Case realisiert, gehört gar nicht in den Katalog. Nichts in Markdown setzt das durch. Der Test tut es.

Die Konsequenz steht in testing.md: Die Status:-Zeile ist eine Behauptung, kein Etikett. Wer sie auf Done oder Automated setzt, schaltet den Sensor ein. Genau deshalb darf sie nicht ohne vorheriges Coverage-Audit gesetzt werden. Hooks setzen das inzwischen durch; dazu gleich mehr.

Drei Details der Tests sind erwähnenswert:

  • Jede Annotation hat einen Ort. @UseCase sitzt auf der Methode, weil ein Use Case viele Abdeckungseinheiten hat (Hauptszenario, jeder Ablauf, jede Geschäftsregel) und nur der Autor weiss, welche Methode welche Einheit abdeckt. @TestCase sitzt auf der Klasse, weil ein Test Case genau eine Einheit hat: die Reise. Beide Sensoren lehnen ihre Annotation an jedem anderen Ort ab. Ohne diese Regel könnte ein Journey-Test stillschweigend einen der fünf Use Cases „abdecken“, durch die er läuft, was wie Abdeckung aussähe, während die alternativen Abläufe dieses Use Case ungetestet bleiben.
  • Ein Guard-Test prüft, dass überhaupt Spezifikationen und Annotationen gefunden wurden. Ohne ihn würde ein falsches Arbeitsverzeichnis jede Prüfung über eine leere Menge bestehen lassen, ein Sensor, der grün meldet, weil er blind ist.
  • Die Tests melden jede Verletzung auf einmal, nicht nur die erste. Der Fehlerbericht ist deshalb eine Arbeitsliste, und eine, die der Agent direkt abarbeiten kann.

Die weiteren Sensoren im Projekt:

  • jOOQ generiert Code aus dem Schema. Ein falscher Spaltenname bricht die Kompilierung. Die Flyway-Migration ist deshalb die Schema-DSL, und das Entity-Modell muss dazu passen.
  • Browserless-Tests (UC<NNN><Name>Test) prüfen jede View gegen die Use-Case-Spezifikation. Jede Testmethode trägt eine @UseCase-Annotation mit id, scenario und business rules. Das ist der Input für UseCaseTraceabilityTest.
  • Playwright-Tests (TC<NNN><Name>IT) prüfen ganze Benutzerreisen im Browser. Jede Klasse trägt eine @TestCase-Annotation. Das ist der Input für TestCaseTraceabilityTest.
  • TestLayerConventionsTest stellt sicher, dass die Namenskonvention, die die Build-Phase entscheidet (*Test läuft in test, *IT in verify), eingehalten wird, damit kein Test stillschweigend übersprungen wird.

Der Unterschied zwischen Guide und Sensor ist der Unterschied zwischen probabilistischer und deterministischer Einhaltung. Ein Guide erhöht die Wahrscheinlichkeit, dass der Agent korrekt arbeitet. Ein Sensor stellt sicher, dass falsche Arbeit nicht durchkommt. Ein guter Harness braucht beides.

Hooks: Sensoren für die Session

Alle Sensoren oben teilen eine Einschränkung, die man leicht übersieht: Sie wirken im Nachhinein. ArchUnit und die Traceability-Tests können Drift nur erkennen, wenn jemand sie ausführt. Nichts im Quellbaum kann ausdrücken: „Der Agent hat die Sensoren laufen lassen, bevor er gesagt hat, er sei fertig.“ Denn das ist eine Eigenschaft der Session, nicht des Repositories.

Ein Fehlerbild überlebt also jeden Test im Projekt. Der Agent ändert eine View, überspringt ./mvnw test und meldet Erfolg. Der Drift ist ab diesem Moment real, bis CI ihn erkennt, Minuten oder Stunden später, und nur, wenn jemand pusht.

Eine zweite Lücke hat dieselbe Form. Das CLAUDE.md sagt, dass eine Status:-Zeile eine Behauptung ist und nicht ohne Coverage-Audit gesetzt werden darf. Prosa formt Verhalten probabilistisch. Sie verhindert keine Änderung. Der Sensor erkennt einen falschen Status zwar, aber erst beim nächsten vollständigen Lauf, und ein eingeschränkter Lauf (-Dtest=...) überspringt die Sensoren ganz und meldet trotzdem grün.

Claude Code Hooks schliessen beide Lücken. Das sind Shell-Skripte, die Claude Code bei Ereignissen der Session ausführt: beim Start einer Session, vor oder nach einem Tool-Aufruf, wenn ein Subagent fertig ist, bevor ein Turn endet. Ein Hook, der mit Exit-Code 2 endet, blockiert die Aktion und gibt seine Nachricht an Claude zurück.

Die erste Version, und was ein Review fand

Die erste Version hatte vier Hooks. Einer merkte sich den Commit, auf dem die Session begonnen hatte. Einer beobachtete Edit- und Write-Aufrufe und meldete eine Status:-Zeile, die ihren Wert änderte. Einer beobachtete Bash-Aufrufe und merkte sich, wenn ein vollständiger ./mvnw test lief und die Konsole keinen Fehler zeigte, dass die Sensoren gelaufen waren. Und der Stop-Hook verweigerte das Beenden eines Turns, solange src/ oder docs/ seit diesem Lauf geändert worden waren.

Ein Review zeigte, dass jeder von ihnen erfüllt sein konnte, ohne dass die Eigenschaft galt, und immer aus demselben Grund: Der Hook stützte sich darauf, welches Tool lief oder was die Konsole ausgab, nicht auf den Zustand des Repositories.

  • Der Status-Guard reagierte auf Edit und Write. Ein sed oder ein Heredoc über Bash erreichte ihn nie. Und im Auto-Modus bevorzugt Claude Code Bash für Änderungen, der Bypass war also der Standardweg, kein Randfall.
  • Der Sensor-Recorder nahm jeden vollständigen Lauf ohne Fehlertext in der Ausgabe als grün. Ein Lauf im Hintergrund, durch tail gepipet, nach /dev/null umgeleitet oder mit || true dahinter erzeugte keinen Fehlertext und schrieb den Marker ohne erfolgreichen Build.
  • Der Status-Guard feuerte nach der Änderung. Die Behauptung stand bereits in der Datei, und „zuerst das Audit laufen lassen“ war eine Bitte, keine Regel.
  • Nur die vollständige Testcontainers-Suite zählte. Jeder Turn, der ein Dokument berührte, kostete einen Container-Start, und „Docker ist nicht verfügbar“ war ein erlaubter Weg, einen Turn unverifiziert zu beenden.

Das ist dieselbe Lektion wie bei den ArchUnit-Regeln, eine Stufe höher. Eine Regel, die als Prosa plausibel ist, ist als Prüfung oft falsch, und man merkt es erst, wenn man versucht, sie zu umgehen.

Die zweite Version: Zustand und Beweise

Die zweite Version folgt zwei Regeln, festgehalten in ADR-011.

Ein Guard liest den Zustand des Repositories, nie die Form eines Tool-Aufrufs. check-spec-status.sh läuft nach jedem Tool-Aufruf, liest jede Status:-Zeile in docs/ neu und vergleicht sie mit dem, was er zuletzt gesehen hat. Welches Tool die Änderung gemacht hat, ist egal. Ein PreToolUse-Guard für Edit und Write bleibt, weil er dort die Änderung ablehnen kann, bevor sie landet. Er ist der schnelle Weg, nicht die Garantie.

Beweise sind positiv. Ein Sensorlauf zählt, weil der Surefire-Report jeder Sensorklasse in target/surefire-reports/existiert, neuer ist als der Session-Start und als jede geänderte Datei, Tests ausgeführt hat und keinen Fehler zählt. Die Test-JVM schreibt diese Dateien, egal was die Konsole zeigt. Ein Audit zählt, weil der uc-coverage-Agent des AIUP-Plugins fertig geworden ist, was Claude Code als SubagentStop-Ereignis meldet. Die Guards akzeptieren Done, Testedoder Automated nur mit einem solchen Audit-Marker, der neuer ist als die letzte Änderung unter src/. Der Marker sagt, dass ein Audit stattgefunden hat, nicht, dass es nichts gefunden hat: Die Behauptung selbst beurteilen die Sensoren beim nächsten Lauf.

So sehen die sechs Hooks jetzt aus:

HookEreignisWas er tut
session-start.shSessionStartmerkt sich den Commit, auf dem die Session begonnen hat, und wann, verwirft die Marker der letzten Session und nimmt eine erste Lesung jeder Status:-Zeile
guard-spec-status.shPreToolUse(Edit, Write)lehnt eine Änderung ab, die einen Status: auf Done, Tested oder Automated setzt, ausser ein Coverage-Audit dieser Spezifikation ist in dieser Session nach der letzten Änderung unter src/ fertig geworden
check-spec-status.shPostToolUse(jedes Tool)liest nach jedem Tool-Aufruf jede Status:-Zeile neu und meldet eine, die jetzt Abdeckung behauptet, ohne dass ein Audit dahintersteht, egal welches Tool sie geändert hat
record-coverage-check.shSubagentStop(uc-coverage)hält fest, dass der Coverage-Agent einen UC oder TC geprüft hat: der Beweis, den die beiden Guards oben verlangen
record-sensor-run.shPostToolUse(Bash)hält nach einem Maven test oder verify die Änderungsmenge fest, die die Sensoren gesehen haben, genau dann, wenn der Surefire-Report jedes Sensors frisch und grün ist
require-sensors.shStopverweigert das Beenden eines Turns, solange src/, docs/ oder pom.xml geändert wurden und die Sensoren seit der neuesten Änderung nicht gelaufen sind, oder solange ein Hook geändert wurde und smoke.sh nicht gelaufen ist

Der Stop-Hook ist derjenige, der die Schicht rechtfertigt. Wenn Claude einen Turn beenden will, vergleicht der Hook die geänderten Dateien mit der Änderungsmenge, die der letzte grüne Lauf gesehen hat. Wurde eine Datei nach diesem Lauf geändert, oder hinzugefügt, gelöscht oder committet, ohne dass der Lauf sie gesehen hat, gibt der Hook Exit-Code 2 zurück, mit einer Nachricht, die sagt, was zu tun ist. Claude liest die Nachricht, lässt die Sensoren laufen und versucht erneut zu stoppen.

Die Sensoren so günstig machen, dass sie in jedem Turn laufen

Die Docker-Ausrede wurde geschlossen, indem der Grund dafür entfernt wurde. Die fünf Sensorklassen (ArchitectureTest, TestLayerConventionsTest und die drei Traceability-Tests) tragen das JUnit-Tag sensor, und ein Maven-Profil, das -Dgroups=sensor aktiviert, überspringt für diesen Lauf die jOOQ-Codegenerierung und den JaCoCo-Agenten:

./mvnw -q test -Dgroups=sensor

Sekunden statt Minuten, kein Container. Das ist der Lauf, den der Stop-Hook verlangt. Die vollständige Suite bleibt die Definition von fertig und das Commit-Gate; ein vollständiger test oder verify zählt ebenfalls.

Drei weitere Entscheidungen haben die Schicht brauchbar gehalten:

  • Ein Hook muss fail-open sein. Nicht parsbarer Input, ein fehlendes .git, kein jq: Der Hook beendet sich leise. Ein Guardrail, der die Session bricht, ist schlimmer als der Drift, den er verhindern sollte.
  • Der Stop-Hook ist eine erzwungene Erinnerung pro Turn, keine Mauer. Wenn Claude zum zweiten Mal stoppen will, setzt Claude Code stop_hook_active, und der Hook lässt den Turn enden. Ein Turn, der ohne Sensorlauf endet, ist im Transkript genau als das sichtbar. Das ist der eine bewusste Ausweg. Der andere aus der ersten Version, die Marker-Datei von Hand anzufassen, funktioniert nicht mehr, weil der Hook die Surefire-Reports hinter dem Marker liest.
  • Ein Commit darf den Guard nicht löschen. Der Stop-Hook vergleicht den Arbeitsbaum mit HEAD, was ein Commit stillschweigend zurücksetzen würde. Deshalb merkt sich session-start.sh den Commit, auf dem die Session begonnen hat, damit der Hook auch HEAD gegen diesen Commit vergleichen kann.

Die Hooks selbst werden ebenfalls bewacht. smoke.sh neben ihnen prüft alle sechs gegen ein Wegwerf-Repository und hält seinen eigenen grünen Lauf fest. Eine Änderung unter .claude/hooks/ oder an .claude/settings.json hält den Turn, bis smoke.sh gelaufen ist, und CI führt es vor dem Maven-Build aus. Es ist absichtlich ein Shell-Skript und kein Maven-Test: Die Hooks sind eine Eigenschaft einer Claude-Code-Session, und das ist das Nächste, was einer solchen gleichkommt.

Schliesslich erlaubt .claude/settings.json den Maven-Build, den Smoke-Test und lesende Git-Befehle, damit ein frischer Clone die Hooks ohne Rückfrage erfüllen kann. Sonst sagt der Hook Claude, einen Build zu starten, für den Claude dann erst um Erlaubnis fragen muss.

Die Grenze zwischen Hooks und Tests

Es war verlockend, die Hooks zu erweitern. Warum nicht auch die Namensregel für Tests in einem Hook prüfen? Weil diese Regel durch Lesen des Repositories prüfbar ist, und deshalb wurde sie stattdessen zu TestLayerConventionsTest. Die Regel für einen neuen Hook steht in ADR-010 und ADR-011: Wenn eine Regel durch Lesen des Repositories prüfbar ist, ist sie ein Test. Nur eine Regel darüber, was während einer Session passiert ist, ist ein Hook.

Der Grund ist Portabilität. Hooks leben in .claude/, feuern nur innerhalb einer Claude-Code-Session und laufen für niemanden sonst: nicht für CI, nicht für einen anderen Editor, nicht für einen menschlichen Mitwirkenden. Eine ArchUnit-Regel bindet alle und reist mit dem Clone. Hooks kaufen Latenz, Tests kaufen Korrektheit. Nur CI entscheidet, ob Code existieren darf.

Der Harness im PetClinic-Projekt hat damit vier Durchsetzungsschichten: das CLAUDE.md als Landkarte, die Architektur-Sichten und die Skills als Guides, die ArchUnit- und Traceability-Tests als Sensoren auf dem Repository, und die Hooks als Sensoren auf der Session. Die letzte Schicht ist die kleinste, und das soll sie bleiben.

Was das in der Praxis bedeutet

Das CLAUDE.md ist eine Landkarte, keine Bibliothek. Es enthält, was der Agent sonst nirgends findet: die Quelle der Wahrheit, den Stack, die Befehle, und wann welches Dokument zu lesen ist. Alles andere gehört in separate Dokumente, die bei Bedarf gelesen werden. Die ETH-Studie zeigt, dass mehr Kontext nicht mehr Qualität bringt, nur mehr Kosten. Und das CLAUDE.md wächst von selbst, wenn man es nicht regelmässig kürzt.

Das Software Architecture Document ist wieder ein Artefakt erster Klasse. Aufgeteilt in die 4+1-Sichten, jede kurz und konkret, mit Code-Beispielen, und jede beantwortet eine Frage. Zusammen beantworten sie die Fragen, die der Agent sonst mit generischen Antworten aus dem Training füllen würde.

Sensoren gehören auf beide Schichten. ArchUnit prüft, dass der Code zur Architektur passt. Die drei Traceability-Tests prüfen, dass die Tests zur Spezifikation passen und dass die Spezifikation mit sich selbst übereinstimmt, für Use Cases, Test Cases und Geschäftsregeln. Nur mit beidem ist die Behauptung „docs ist die Quelle der Wahrheit“ mehr als ein Satz im CLAUDE.md.

Architekturentscheidungen sollten als Sensoren umgesetzt werden. Jede Regel, die sich mit dem Compiler, ArchUnit oder einem Test prüfen lässt, sollte geprüft werden. Und das Dokument sollte auf den Test zeigen, und der Test auf das Dokument. Alles andere bleibt eine Hoffnung.

Tests bewachen das Repository, Hooks bewachen die Session. Ein Test prüft den Quellbaum. Ein Hook prüft, was in einer Session passiert ist: dass die Sensoren wirklich gelaufen sind, dass ein Status vor dem Setzen geprüft wurde. Ein Hook liest den Zustand des Repositories und verlangt positive Beweise, nie die Form eines Tool-Aufrufs. Die Hook-Schicht klein halten; was ein Test sein kann, soll ein Test sein.

Harness Engineering ist Architekturarbeit. Die Rolle des Architekten verschiebt sich vom Code-Reviewer zum Gestalter des Systems, in dem der Agent arbeitet. Wer die Architektur klar beschreibt und durchsetzt, bekommt Code, der zum System passt.

Der AI Unified Process macht diesen Zusammenhang explizit. Die What-Schicht sagt, was gebaut wird. Die Architektur-Dokumente in der Harness-Schicht sagen, wie es gebaut wird. Die Sensoren stellen sicher, dass es so gebaut wurde. Und die Hooks stellen sicher, dass die Sensoren gefragt wurden.