Architekturdiagramm — constitution.yaml speist einen Python-Executor mit halt-on-reject-Prüfschicht
9 min read

LLM-Agenten ignorieren Regeln. Dieses Muster nicht.

#LLM #Agents #Python #Claude Code #Anthropic #Policy #Open Source

Das Problem

Wenn ein LLM-Agent über eine echte Codebasis läuft, muss er Regeln einhalten. Nicht Empfehlungen — Regeln. „Das ist die rechtliche Geschäftsadresse; niemals eine Zieladresse einsetzen.” „Keine Force-Pushes.” „Diese verbotenen Markenbegriffe niemals in Texten verwenden.” „Das LocalBusiness-Schema muss immer die Heimadresse verwenden, nicht das Einzugsgebiet.”

Der naheliegende Ansatz: Die Regeln in den System-Prompt packen. „Du DARFST das Adressfeld NICHT ändern.” Das Problem ist, dass LLMs System-Prompt-Anweisungen unter Aufgabendruck, bei adversarialem Input oder schlicht aus Versehen ignorieren. Das ist keine theoretische Sorge. Es ist dokumentiert und reproduzierbar.

Issue #19874 im Repository anthropics/claude-code — im Januar 2026 geöffnet und bei v2.1.14 ohne Behebung der zugrundeliegenden Architektur als „not planned” geschlossen, auch wenn der zugrundeliegende Bug seit v1.0.95 (August 2025) in mehreren früheren Issues dokumentiert ist — formuliert es klar:

Plan mode provides no actual enforcement of read-only restrictions. The “safety guarantee” is implemented purely as a system prompt instruction that the LLM can (and regularly does) ignore.

Das Issue enthält ein Ablaufdiagramm, das die architektonische Lücke explizit macht: Es gibt keine Durchsetzungsschicht zwischen der LLM-Entscheidung und der Tool-Ausführung. Wenn der LLM die Sicherheitsanweisung ignoriert, erfolgt der Datei-Schreibvorgang trotzdem.

Zwei verwandte Issues illustrieren das Muster: #21292 beschreibt, wie Claude mit nicht-readonly-Aktionen fortfährt, wenn ExitPlanMode vom Nutzer abgelehnt wird; #19021 dokumentiert einen konkreten Fall, in dem der Agent nach dem Bearbeiten von Dateien im Plan Mode versuchte, seine Änderungen mit git checkout rückgängig zu machen und dabei nicht-committete Arbeit des Nutzers zerstörte. Die Ursache ist dieselbe: Die Ablehnungsnachricht wird als Implementierungshinweis nach der Ausführung behandelt, nicht als Fortsetzung der Plan-Mode-Beschränkung.

Die richtige Lösung ist kein besserer Prompt. Sie ist die Durchsetzung auf Executor-Ebene — eine Prüfung im Code, die der LLM nicht umgehen kann.

Was die vorhandenen Werkzeuge tun (und nicht tun)

Drei Ansätze adressieren Teile dieses Problems. Keiner deckt es vollständig ab.

Claude Code Plan Mode — Überprüfung zur Entwurfszeit, keine Executor-Durchsetzung

Plan Mode löst den Proposal-Review-Loop: Der Agent zeigt, was er zu tun beabsichtigt, bevor er es tut. Das ist nützlich, aber es ist keine executor-seitige Durchsetzung. Wenn der LLM trotz einer Ablehnung weitergeht — was er tut, wie die Issues oben belegen — gibt es keinen Sicherheitsstopp. Die Freigabe erfolgt zur Entwurfszeit; die Ausführung bleibt ungeschützt.

AWS Bedrock Guardrails — Inferenz-Filterung, keine Aktionsdurchsetzung

Bedrock Guardrails setzt zur Inferenzzeit durch. Es unterstützt mittlerweile kontoübergreifende und organisationsweite Durchsetzung, was eine sinnvolle Verbesserung ist. Aber es operiert auf Modellausgaben: Inhaltsfilterung, Topic-Denylisten, PII-Erkennung. Man kann nicht ausdrücken: „Wenn diese Agentenaktion die falsche NAP-Adresse in ein LocalBusiness-Schema schreiben würde, stopp.” Man kann ausdrücken: „Wenn die Modellausgabe PII enthält, filtere sie.” Das ist wertvoll, aber es operiert auf Tokens, nicht auf Aktionen — Bedrock weiß nichts vom LocalBusiness-Schema oder dem NAP-Feld, das dein Agent gerade schreiben will.

LangGraph interrupt() — Human-in-the-Loop, kein Richtlinienobjekt

LangGraph interrupt() hält einen laufenden Graph an und übergibt die Kontrolle an einen Menschen. Das Command(resume=...) Muster ermöglicht es, die nächste Aktion vor der Ausführung zu genehmigen, abzulehnen oder zu modifizieren. Das ist die richtige Form für Human-in-the-Loop-Workflows. Aber LangGraph hat kein Richtlinienobjekt — keinen Mechanismus, um deklarative Regeln aus einer Datei zu laden, Lauf-spezifische Overrides zusammenzuführen und das Ergebnis deterministisch durchzusetzen. Die Prüfungen werden manuell in den Nodes verdrahtet — jeder Node, der eine Regel durchsetzen muss, schreibt seine eigene Python-Bedingung im Code, ohne einen zentralen Ort, an dem man „diese sind die Projekt-Invarianten” deklarieren kann.

GitHub Spec Kit — Kontext-Injektion, keine Durchsetzung

GitHub Spec Kit (github/spec-kit) führt .specify/memory/constitution.md ein — eine nicht verhandelbare Prinzipiendatei, die als LLM-Kontext vor Lifecycle-Befehlen geladen wird. Die Benennung ist treffend und die Idee ist richtig. Aber es ist Kontext-Injektion, keine Durchsetzung. Der LLM liest die Verfassung und soll sie einhalten. Es gibt keinen Code-Level-Stopp, wenn er es nicht tut.

Die Lücke ist die Kombination: deklarative geschichtete Regeln, deterministisch zusammengeführt, mit einem Stopp, der im Executor durchgesetzt wird, nicht im Prompt.

Die Konvergenz

Das Schichtungsproblem wurde in der Infrastrukturwelt seit 2018 gelöst: Kustomize Overlays.

Kustomize adressiert „Ich habe eine Basiskonfiguration und brauche umgebungsspezifische Korrekturen” mit einem deterministischen Rightmost-Wins-Merge. Die Basis definiert Defaults; Overlays überschreiben spezifische Felder; das Ergebnis ist reproduzierbar und debuggbar. Das Design vermeidet explizit jeden logikgesteuerten Merge — der Merge ist eine reine strukturelle Operation.

Auf Agenten-Richtlinien angewendet, sieht die Form so aus:

.agent/
  constitution.yaml    # menschlich verfasst, committed, Basis-Invarianten
  corrections.yaml     # laufspezifische Overrides, gitignored

Der Merge-Algorithmus ist einfach: final_rules = deep_merge(constitution, corrections). Dicts werden rekursiv zusammengeführt; Skalare und Listen werden durch den rechtsseitigen Wert ersetzt. Keine Logik, keine LLM-Beteiligung, keine Überraschungen.

In der aktuellen Implementierung wird corrections.yaml vom Entwickler für einen spezifischen Lauf manuell geschrieben — zum Beispiel: „Diese Migration darf 200 Dateien anfassen, nicht 50”. Ein automatisierter Genehmigungs-Workflow, bei dem Plan Mode Korrekturen vorschlägt und der Nutzer genehmigt, bevor sie angewendet werden, ist eine natürliche Integration, liegt aber außerhalb der v0.1-Bibliothek.

Dann — der Teil, der in jedem aktuellen Werkzeug fehlt — werden die zusammengeführten Regeln in Python durchgesetzt, nicht im Prompt:

@halt_on_reject(constitution)
def write_local_business_schema(address: dict) -> None:
    legal = constitution.get("business.home_address")
    if address.get("addressLocality") != legal.get("addressLocality"):
        raise PolicyReject(
            f"address mismatch: schema has {address['addressLocality']!r}, "
            f"constitution requires {legal['addressLocality']!r}"
        )
    # ... eigentliche Schreiblogik

PolicyReject propagiert bedingungslos. Der LLM kann der Python-Runtime nicht anweisen, ein raise zu überspringen. Das ist der Sicherheitsstopp, den Plan Mode nicht hat.

Referenzimplementierung

Ich habe dieses Muster als kleine Python-Bibliothek gebaut: constitution-overlay — MIT-lizenziert, ~300 Zeilen, mypy --strict sauber.

Die öffentliche API hat vier Symbole:

from constitution_overlay import (
    Constitution,        # lädt und merged YAML/dict-Schichten
    halt_on_reject,      # Decorator-Factory — Durchsetzung
    PolicyReject,        # Exception-Typ — stoppt den Executor
    ConstitutionContext, # Read-only-View für Prüffunktionen
)

Laden und Mergen

# Aus Dicts
base = {
    "business": {
        "legal_name": "Arnold Wender",
        "home_address": {"addressLocality": "Halle (Saale)"},
    },
    "limits": {"max_files_per_commit": 50},
}
corrections = {"limits": {"max_files_per_commit": 200}}  # dieser Lauf braucht mehr

constitution = Constitution.from_layers(base, corrections)
constitution.get("limits.max_files_per_commit")  # 200
constitution.get("business.home_address.addressLocality")  # "Halle (Saale)"

# Aus Dateien
constitution = Constitution.from_layers(
    "constitution.yaml",
    "corrections.yaml",
)

from_layers() akzeptiert Dicts, String-Pfade und Path-Objekte austauschbar. Fehlende Dateien lösen sofort FileNotFoundError aus — kein stilles Fallback.

Durchsetzen

@halt_on_reject(constitution)
def commit_files(files: list[str]) -> None:
    limit = constitution.get("limits.max_files_per_commit")
    if len(files) > limit:
        raise PolicyReject(f"too many files: {len(files)} > {limit}")
    # mit Commit fortfahren

@halt_on_reject(constitution)
def write_copy(text: str) -> None:
    prohibited = constitution.get("brand.prohibited_terms", [])
    hits = [t for t in prohibited if t in text.lower()]
    if hits:
        raise PolicyReject(f"prohibited brand terms: {hits}")
    # mit Schreiben fortfahren

Der Decorator ist ein Contract-Marker. PolicyReject propagiert bedingungslos — nichts im Decorator schluckt es. Zukünftige Versionen werden Pre-Execution-Regelprüfungen direkt aus der Constitution unterstützen, aber v0.1 bleibt einfach: Prüfungen leben in der gewickelten Funktion, Durchsetzung im Decorator.

Eine realistische constitution.yaml

business:
  model: remote-services
  legal_name: "Arnold Wender"
  home_address:
    streetAddress: "Franckestrasse 3a"
    addressLocality: "Halle (Saale)"
    postalCode: "06110"
    addressCountry: "DE"
  telephone: "+49 345 6867 6857"
service_area:
  - "Bayern"
brand:
  prohibited_terms: ["billig", "guenstigster"]
constraints:
  - "LocalBusiness schema MUST use home_address as address; service_area goes in areaServed"
  - "City pages reference target locality in content only — never in NAP"
require_review:
  - "schema.LocalBusiness"
  - "url-impact:destructive"

Die service_area-Liste ist, was die Website anvisiert. Die home_address ist, wo das Unternehmen tatsächlich operiert. Ein LLM-Agent, der an einer bayernbezogenen Site arbeitet, darf sie nicht verwechseln — die Constitution macht diese Beschränkung explizit und durchsetzbar.

Einschränkungen

v0.1 ist bewusst schmal. Was es nicht tut:

  • Kein Async-Support. Nur synchron. Async-Wrapper sind für v0.2 geplant.
  • Keine Constitution-Schema-Validierung. Die Bibliothek merged, welches YAML auch immer übergeben wird. JSON-Schema-Validierung der Constitution selbst steht auf der Roadmap.
  • Kein eingebautes Review-Gate. Der Genehmigungs-Workflow (Konflikt zeigen, genehmigen/ablehnen/bearbeiten, corrections.yaml schreiben) liegt außerhalb der Bibliothek. Die Bibliothek übernimmt nur Merge und Durchsetzung.
  • List-Merge ist nur Replace. Der rechtsseitige Wert gewinnt. Es gibt noch keine Append-Direktive, was bedeutet, dass prohibited_terms in einem Overlay die Basisliste ersetzt statt sie zu erweitern. Das steht auf der Roadmap.
  • Kein Ersatz für LangGraph, Claude Code oder Bedrock. Es ist eine Durchsetzungsschicht, die daneben sitzt.

Was ich in Claude Code sehen möchte

Das executor-seitige Stopp-Muster gehört nativ in Claude Code als erstklassiges Primitiv. Konkret:

Eine constitution.yaml im Projektstamm (oder in .claude/), die Claude Code vor jeder Tool-Ausführung liest — nicht als Kontext-Injektion, sondern als harte Beschränkungsliste. Vor jedem Edit-, Write- oder Bash-Aufruf prüft Claude Code die Aktion gegen die Constitution. Verstöße lösen aus, nicht warnen.

Korrekturen als erstklassiges Konzept: Wenn Plan Mode einen Konflikt zwischen einer vorgeschlagenen Aktion und der Constitution identifiziert, schreibt er eine corrections.yaml mit der vorgeschlagenen Auflösung. Der Executor liest diese Datei, merged sie über die Constitution und fährt mit der korrigierten Aktion fort — oder stoppt, wenn die Korrektur decision: rejected sagt.

Die Merge-Semantik ist dieselbe, die Kustomize seit 2018 verwendet hat: deterministisch, Rightmost-Wins, keine LLM-gesteuerte Merge-Logik. Das Ergebnis ist reproduzierbar und auditierbar.

Die Referenzimplementierung ist unter github.com/arnoldwender/constitution-overlay. Sie ist MIT-lizenziert, hat 69 Tests, 98% Coverage und besteht mypy --strict. Wenn das als Ausgangspunkt für einen TypeScript-Port oder als Design-Referenz nützlich ist, helfe ich gerne.

Wer das LLM-Workflow-Ökosystem aus praktischer Contributor-Perspektive erleben möchte, findet im MemPalace-Beitrag ein konkretes Beispiel für durchdachtes Open-Source-Engineering in Python.

Diese Arbeit zitieren

DOI

Wender, A. (2026). constitution-overlay: Kustomize-style YAML rule layering with executor-side halt-on-reject for LLM agents (Version v0.1.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.19773588

ORCID iD: ORCID iD icon https://orcid.org/0009-0005-1750-818X