Claude Code schreibt schnell Code. Manchmal zu schnell — auf Kosten der Architektur. Wie ein automatisierter Wächter dafür sorgt, dass Architekturentscheidungen in der Praxis überleben.

Das Problem: KI-Assistenten vergessen, was ihnen wichtig ist

Jeder, der mit Claude Code oder ähnlichen KI-Assistenten gearbeitet hat, kennt das Phänomen: Man erklärt dem Modell sorgfältig das Architekturmuster, auf das man sich geeinigt hat — zum Beispiel, dass alle Fehlerbehandlung über Result-Typen laufen soll und niemals über throw. Claude nickt (metaphorisch) und beginnt zu coden.

Fünf Minuten später, in einer anderen Funktion, schreibt Claude genau das, was man gerade verboten hat: throw new Error(...).

Das Modell ist dabei nicht schuld — das Problem sitzt tiefer: Claude hat kein dauerhaftes Gedächtnis für Architekturentscheidungen, die über den aktuellen Kontext hinausgehen. Je länger das Gespräch, desto mehr Kontext verdrängt frühere Absprachen. Und in einem Monorepo mit Hexagonaler Architektur, funktionalen Servicemustern und durchgängiger Railway-Programmierung ist diese Drift fatal.

Die klassische Antwort wäre: "Schreib es in die CLAUDE.md." Das hilft — bis Claude in einer neuen Session subtil davon abweicht, ohne dass man es sofort merkt.

Gedächtnis allein bringt noch keine Architektur-Konformität. Was man braucht, ist ein automatisierter Wächter, der jeden Schreibvorgang prüft, bevor er in der Codebasis landet.


Bully: Agentic Linting direkt im KI-Workflow

Bully ist ein Linting-Plugin für Claude Code, das exakt an dieser Stelle greift. Es hängt sich als PostToolUse-Hook in Claude Code ein: Jedes Mal, wenn Claude eine Datei schreibt oder editiert, prüft Bully die Änderung gegen einen Regelkatalog in .bully.yml.

Was Bully von klassischen Linting-Tools wie ESLint unterscheidet, ist die Regeltiefe. Drei Regel-Engines stehen zur Verfügung:

Engine Einsatzbereich
script Shell-Kommandos (grep, ESLint-Aufruf, eigene Skripte)
ast Muster im Syntaxbaum — erkennt Code-Konstrukte, unabhängig von der Formatierung
semantic Natürlichsprachliche Beschreibung — Claude selbst bewertet die Verletzung

Der entscheidende Unterschied zur klassischen CI-Pipeline: Der Wächter sitzt zwischen KI und Codebase, nicht danach. Claude bekommt die Verletzung als Feedback und muss die Datei sofort korrigieren, bevor der nächste Schritt beginnt.


Eine Architektur-Regel, Zeile für Zeile

Hier ist eine konkrete Regel aus dem Produktivbetrieb — sie schützt das Result-Railway-Muster:

no-throw-in-domain:
  description: >
    Domain and application layers must never throw exceptions. Return
    Err(...) from ts-results-es instead. Throwing bypasses the Result
    railway and makes error handling invisible to callers.
  engine: semantic
  scope: ["**/domain/**/*.ts", "**/application/**/*.ts"]
  severity: error

Die semantic-Engine ist hier bewusst gewählt: throw kann in vielen Formen auftauchen — als throw new Error(), als Weitergabe von Ausnahmen, als implizites Rethrow. Ein grep-Skript würde legitime Fälle (z.B. in Testfixtures) treffen oder echte Verletzungen übersehen. Claude selbst bewertet die Semantik und entscheidet, ob ein Verstoß vorliegt.

Eine zweite Regel deckt den häufigen Anti-Pattern ab, Services als Klassen zu exportieren statt als Factory-Funktionen:

no-export-class-in-services:
  description: >
    Services in the application layer must be factory functions, not
    classes. Pattern: (deps) => (params) => AsyncResult<T, Error>.
    Classes hide dependencies and break the functional composition model.
  engine: script
  scope: ["**/application/**/*.ts"]
  severity: error
  script: "grep -nE '^export class ' {file} && exit 1 || exit 0"

Und eine AST-basierte Regel, die einen sehr spezifischen Anti-Pattern verhindert — unnötiges, manuelles Wrapping von Werten mit AsyncResult und Promise.resolve:

no-new-asyncresult-promise-resolve:
  description: >
    Do not construct AsyncResult with `new AsyncResult(Promise.resolve(x))`.
    Enter the railway early with `pure(startValue).andThen(...)` so you can
    simply return `Ok(x)` inside the chain.
  engine: ast
  scope: ["*.ts", "*.tsx"]
  severity: error
  pattern: "new AsyncResult(Promise.resolve($$$))"

Dieses Muster entsteht fast immer, wenn Claude versucht, eine synchrone Berechnung in einen asynchronen Kontext zu hieven, ohne das Result-Railway-Idiom zu kennen. Die AST-Engine findet es unabhängig davon, ob es über eine oder drei Zeilen verteilt ist.


Vom Architektur-Meeting zur laufenden Durchsetzung — in fünf Schritten

1. Architekturentscheidungen in Regeln übersetzen

Bevor man .bully.yml anlegt, lohnt es sich, die Architecture Decision Records (ADRs) oder den Architektur-Styleguide zu sichten: Welche Entscheidungen sind nicht-verhandelbar? Welche werden am häufigsten verletzt? Diese werden zu error-Regeln. Konventionen, bei denen man Abweichungen toleriert, werden zu warning.

# Bully initialisieren — erkennt den Tech-Stack und schlägt erste Regeln vor
bully init

2. Den Hook aktivieren

In .claude/settings.json des Projekts (oder global in ~/.claude/settings.json) registriert man das Bully-Plugin:

{
  "enabledPlugins": {
    "bully@bully-marketplace": true
  },
  "permissions": {
    "allow": ["Bash(bully :*)"]
  }
}

Ab diesem Moment wird jede Edit- oder Write-Operation von Claude Code durch Bully geleitet.

3. Regeln iterativ schärfen

Beim ersten Lauf wird man schnell merken, welche Regeln zu weit greifen und Fehlalarme produzieren. Das ist gewollt: Bully enthält einen telemetry-Mechanismus, der protokolliert, welche Regeln wie oft auslösen und ob Claude die Verletzung als korrekt eingestuft hat.

# Regelgesundheit prüfen
/bully-review

Die Ausgabe zeigt "tote" Regeln (nie ausgelöst), "laute" Regeln (zu viele Fehlalarme) und Kandidaten für eine Aufwertung oder Herabstufung.

4. Beschreibungen als Erklärungen schreiben

Der description-Text einer Regel ist nicht nur Dokumentation — er ist das, was Claude liest, wenn eine Verletzung gefunden wird. Eine gute Beschreibung erklärt warum die Regel existiert und zeigt ein Gegenbeispiel. Knappe Beschreibungen führen zu unklaren Korrekturen.

# Schwach: Claude weiß nicht, was stattdessen zu tun ist
no-unwrap:
  description: "Do not use .unwrap()"

# Stark: Claude versteht Motivation und Alternative
no-unwrap:
  description: >
    Avoid `.unwrap()` — it throws if the Result is Err. Use
    `.unwrapOr(default)` for a safe fallback, or check `.isOk()` first
    and access `.value`.

5. Regeln versionieren wie Code

.bully.yml gehört ins Repository, neben CLAUDE.md und die ADRs. Jede neue Architekturentscheidung, die zu einem Review geführt hat, bekommt eine Regel. Der Commit-Verlauf zeigt dann, wie sich der Architektur-Konsens über Zeit entwickelt hat.


Offene Fragen — wohin ich als nächstes schauen will

Das aktuelle Setup — Bully als reaktiver Wächter nach jedem Edit — funktioniert. Aber es wirft sofort neue Fragen auf, denen ich noch nachgehen möchte. Ob und wie gut das alles umsetzbar ist, weiß ich noch nicht.

Kann Bully in der CI/CD-Pipeline laufen? Mein Ziel wäre es, dieselben Regeln aus .bully.yml auch in Pull-Request-Checks einzusetzen, sodass Architekturverletzungen den Merge blockieren — unabhängig davon, ob jemand Claude Code benutzt hat oder nicht. Bully ist bisher als Claude-Code-Plugin konzipiert; ob es sich sinnvoll aus diesem Kontext lösen lässt, ist eine offene Frage.

Regelevolution durch den bully-scheduler. Bully scheint einen Hintergrund-Agenten mitzubringen, der Telemetrie-Daten auswertet und Regeln vorschlägt, die abgeschwächt, verschärft oder entfernt werden sollten. Ich habe das noch nicht im Produktiveinsatz erlebt — aber wenn es funktioniert, wie beschrieben, würde Architekturpflege zum kontinuierlichen Prozess statt zum vierteljährlichen Workshop.

Bounded-Context-spezifische Regelsets. Glob-Patterns in scope funktionieren bereits — das habe ich in meiner Konfiguration im Einsatz. Was ich noch nicht weiß: Lässt sich das so weit treiben, dass verschiedene Bounded Contexts grundsätzlich unterschiedliche Regeln bekommen, ohne dass die .bully.yml unbeherrschbar wird?

Komplexe semantische Regeln. Die semantic-Engine läuft bei mir für Regeln wie "kein throw in der Domain-Schicht" — und das funktioniert gut. Ob sie auch strukturelle Invarianten erkennt, die über einzelne Dateien hinausgehen ("Entities dürfen keine Repository-Aufrufe enthalten"), weiß ich nicht. Das wäre einen Versuch wert.


Architektur, die sich selbst verteidigt

Was sich hier ändert, ist mehr als eine Technikfrage. Bisher war Softwarearchitektur im KI-gestützten Entwicklungsprozess ein Aspirationszustand: Man beschrieb, was man wollte, und hoffte, dass der Assistent es einhielt. Architektur-Reviews fanden post-hoc statt, wenn die Verletzung bereits im Codebase war.

Mit Bully hängt Architektur-Konformität am Werkzeug — nicht am Wohlwollen einzelner Beteiligter. Claude bekommt sofort Feedback, wenn es vom Muster abweicht, und korrigiert sich. Vergessen ist keine Option mehr. Und wenn Dokumentation und Implementierung auseinanderlaufen, zeigt das Werkzeug es — bevor der Code committed wird.

Das Programm "Softwarearchitektur für KI-basierte Systeme" greift genau hier an: Architekturentscheidungen nicht nur treffen, sondern in einer Welt, in der KI-Assistenten immer mehr implementieren, auch wirksam verankern.


Matthias Bohlen ist Trainer und Berater für Softwarearchitektur. In seinem Programm "Softwarearchitektur für KI-basierte Systeme" lernen Softwarearchitekten, wie sie Architekturentscheidungen in KI-gestützten Entwicklungsprozessen wirksam verankern.


Bully ausprobieren: Das Bully-Plugin ist in einem Claude Code Marketplace verfügbar. Dort findet Ihr auch die Installationsanleitung.