Kontextmanagement mit Claude Code: Warum ich Memory meide und stattdessen Git benutze

Veröffentlicht von
Table of Contents

Wer länger mit Claude Code arbeitet, merkt schnell: Das eigentliche Problem ist nicht die Codequalität des Modells. Das Problem ist der Kontext.

Ein Kontextfenster ist endlich. Es füllt sich mit Dateien, Tool-Outputs, Fehlermeldungen, Suchergebnissen, und irgendwann wird komprimiert oder die Session endet. Was dann verloren geht, ist nicht der Code (der liegt im Repo), sondern das Warum: warum eine Bibliothek verworfen wurde, welcher Ansatz schon dreimal gescheitert ist, was die offene Frage an den Kunden war.

Die naheliegende Antwort auf dieses Problem ist Memory. Ich halte das für den zweitbesten Weg. Dieser Artikel beschreibt, wie ich Kontext stattdessen organisiere: Markdown im Git-Repo als primäre Wissensbasis, ein Issue-Tracker für den Arbeitsstand, und Memory nur für das schmale Segment, das wirklich nirgendwo anders hingehört.

Die drei Ebenen des Kontexts

Bevor es um Werkzeuge geht, lohnt eine Unterscheidung. Nicht jeder Kontext ist gleich, und die verschiedenen Sorten gehören an verschiedene Orte:

Art Beispiel Lebensdauer Gehört nach
Projektwissen Architekturentscheidung, Deploy-Prozess, Zugangswege so lange das Projekt lebt Git-Repo (Markdown)
Arbeitsstand "Endpoint fertig, Refresh-Token fehlt noch" Tage bis Wochen Issue-Tracker
Präferenzen "Der User will keine Emoji-Commits" dauerhaft, projektübergreifend Memory

Der häufigste Fehler ist, alles drei in einen Topf zu werfen: meistens in Memory, weil das am bequemsten ist. Das rächt sich später.

Warum ich Memory weitgehend vermeide

Claude Code kennt zwei Sorten persistenten Speicher außerhalb des Repos: das globale Memory-Verzeichnis (projektübergreifend) und projektbezogenes Memory. Beides wird bei Sessionstart teilweise in den Kontext geladen. Das klingt praktisch und ist es auch, bis es das nicht mehr ist.

Memory ist unsichtbar. Eine Tatsache im Memory ist nicht Teil des Repos. Ein Kollege, der das Projekt klont, hat das nicht. Ein CI-Job sieht ihn nicht. Ich selbst sehe ihn nur, wenn ich aktiv nachschaue. Wissen, das nur eine einzige AI-Installation auf einem einzigen Rechner hat, ist kein Projektwissen sondern ein Single Point of Failure.

Memory hat keine Versionshistorie. Wenn sich eine Architekturentscheidung ändert, will ich sehen, wann und warum. git log beantwortet das. Eine überschriebene Memory-Datei nicht.

Memory veraltet. Das ist der gefährlichste Punkt. Eine Markdown-Datei im Repo, die auf eine gelöschte Funktion verweist, fällt beim Lesen des Repos auf. Ein Memory-Eintrag, der behauptet "Deployment läuft über Ansible", nachdem längst auf Kubernetes migriert wurde, wird bei jedem Sessionstart als Wahrheit eingespielt. Die AI widerspricht dem nicht; sie hat keinen Grund dazu. Sie baut darauf auf.

Memory kostet Kontext, bevor die Arbeit beginnt. Jeder Eintrag, der bei Sessionstart geladen wird, ist Kontext, der nicht mehr für den eigentlichen Task zur Verfügung steht. Zehn Memories à 200 Token sind noch harmlos. Achtzig gewachsene Einträge sind es nicht mehr.

Der entscheidende Punkt ist aber ein anderer: Memory ist Push, Markdown ist Pull. Memory wird eingespielt, ob relevant oder nicht. Eine Datei docs/deployment.md wird gelesen, wenn es ums Deployment geht, und sonst nicht. Bei einem einzelnen Fakt ist das egal. Über ein Projektjahr hinweg kann es der Unterschied zwischen einem schlanken und einem zugemüllten Kontextfenster sein.

Der Ersatz: Markdown im Repo

Statt Memory zu füttern, schreibe ich Projektwissen in Markdown-Dateien und verweise darauf. Das Muster ist simpel:

repo/
├── CLAUDE.md                    # Einstiegspunkt, kurz, verweist weiter
└── docs/
    ├── architecture.md          # Warum die Dinge so sind, wie sie sind
    ├── deployment.md            # Wie Code auf den Server kommt
    ├── decisions/
    │   ├── 001-postgres.md      # ADRs: Entscheidung + Alternativen + Begründung
    │   └── 002-no-graphql.md
    └── gotchas.md               # Fallstricke, die schon Zeit gekostet haben

CLAUDE.md bleibt bewusst dünn. Es ist ein Wegweiser, keine Enzyklopädie:

# Projekt: Foo API

Go-Service, deployed via Docker auf vps08.

- Architektur & Entscheidungen: docs/architecture.md, docs/decisions/
- Deployment & Zugriff: docs/deployment.md
- Bekannte Fallstricke: docs/gotchas.md

Vor Änderungen am Auth-Layer immer docs/decisions/003-jwt.md lesen.

Die Vorteile ergeben sich direkt aus den Nachteilen von Memory: Das Wissen ist versioniert, reviewbar, für Menschen wie für andere AI-Tools lesbar, und es wird nur dann in den Kontext geladen, wenn es gebraucht wird. Ein Pull Request, der die Deployment-Pipeline ändert, kann docs/deployment.md im selben Commit mitändern. Ein Memory-Eintrag kann so nicht aktualisiert werden.

Ein netter Nebeneffekt: Dokumentation, die eine AI liest, wird automatisch gepflegt. Klassische docs/-Ordner verrotten, weil niemand sie liest. Wenn die AI bei jedem zweiten Task darauf zugreift und der Fehler sofort sichtbar wird, entsteht ein Feedback-Loop, den rein menschliche Dokumentation selten hat.

Wofür Memory trotzdem taugt

Kategorisch ablehnen muss man Memory aber nicht. Es gibt einen schmalen Bereich, in den es gut passt: Dinge über mich, nicht über das Projekt.

  • Arbeitsstil-Präferenzen ("keine Zusammenfassungen am Ende jeder Antwort")
  • Korrekturen, die ich mehrfach geben musste
  • Wiederkehrende Umgebungsdetails, die in kein einzelnes Repo gehören

Faustregel: Wenn es in ein Repo committed werden könnte, gehört es ins Repo. Alles andere ist Memory-Kandidat. Nach diesem Filter bleiben wenige Einträge für den Memory übrig.

Memory regelmäßig ausmisten

Der Rest an Memory, den man behält, braucht Pflege. Memory wächst monoton: Es wird geschrieben, aber praktisch nie gelöscht, jedenfalls nicht von allein. Deshalb lasse ich in regelmäßigen Abständen (bei mir grob monatlich, oder wenn mir im Sessionstart etwas Falsches auffällt) einen expliziten Review-Durchlauf machen.

Beispielprompt dazu:

Lies alle Memories. Prüfe für jeden Eintrag:

1. Referenziert er Dateien, Funktionen oder Flags, die noch existieren? Verifiziere im Code, nicht aus dem Gedächtnis.

2. Ist er noch aktuell, oder durch eine spätere Entscheidung überholt?

3. Könnte er stattdessen als Markdown-Datei im Repo leben?

4. Ist er so spezifisch für eine einzelne vergangene Session, dass er nie wieder gebraucht wird?

Gib mir eine Liste: BEHALTEN / AKTUALISIEREN / LÖSCHEN / NACH-REPO-VERSCHIEBEN, jeweils mit Begründung. Lösche nichts ohne meine Bestätigung.

Wichtig ist Punkt 1: die Aufforderung, tatsächlich nachzusehen. Ohne diesen Zusatz bewertet das Modell den Eintrag danach, ob er plausibel klingt, und ein veralteter Memory-Eintrag klingt immer plausibel. Er war ja mal wahr.

Was meist bei mir ausfliegt:

  • Erledigte Migrationen. "Achtung, Umstellung auf VectorChord läuft" kann während der Migration nützlich sein, ist danach aber nur Ballast
  • Debugging-Details. Root-Cause-Analysen sind wertvoll, aber als docs/gotchas.md im betroffenen Repo und nicht als globales Memory
  • Duplikate. Derselbe Fakt in leicht unterschiedlicher Formulierung, weil er zweimal gespeichert wurde
  • Zu Spezifisches. Alles, was eine konkrete Portnummer, IP oder Pfadangabe aus einer einzelnen Session enthält

Was bleibt, sind meist, wenn überhaupt etwas bleibt, ein paar Dutzend Zeilen.

td als externes Arbeitsgedächtnis

Damit ist Projektwissen (Markdown) und Präferenzen (Memory) abgedeckt. Fehlt die dritte Ebene: der Arbeitsstand. Und der ist der eigentliche Kontextfresser.

Denn Arbeitsstand hat eine unangenehme Eigenschaft: Er lebt normalerweise nur im Kontextfenster selbst. Was ist fertig, was fehlt, was habe ich schon probiert, was hat nicht funktioniert: all das steht in der Conversation. Und ist weg, wenn die Session endet.

Ich benutze dafür td, einen minimalistischen CLI-Issue-Tracker, der explizit für AI-Coding-Sessions gebaut ist. Die README bringt das Problem auf den Punkt:

"When your context window ends, your agent's memory ends", heißt es in der README: "td is the external memory that lets the next session pick up exactly where the last one left off."

td ist ein Go-Binary mit lokaler Datenbank pro Projekt. Keine Cloud, kein Account, keine Web-UI. Installation:

brew install marcus/tap/td
# oder
go install github.com/marcus/td@latest

Und im Projekt:

td init

Die eine globale Anweisung

td ist eine der wenigen Regeln, die bei mir wirklich global in der zentralen ~/.claude/CLAUDE.md steht, also in jedem Projekt gilt, ohne dass ich etwas tun muss. Das ist bewusst so: Der Punkt einer globalen Regel ist, dass sie ausnahmslos greift. Sobald man anfängt, "meistens" oder "bei größeren Aufgaben" zu schreiben, wird sie ignoriert.

Der Kern der Anweisung, gekürzt:

## Task Management mit td

### Session-Start (IMMER zuerst)
    td init
    td usage --new-session

Zeigt: aktuellen Fokus, offene Reviews, Handoffs der letzten Sessions,
offene Issues nach Priorität. Nie überspringen: ohne das rätst du
über den Projektstand.

### Während der Arbeit
    td start <id>                              # Arbeit beginnen
    td log "OAuth-Callback implementiert"      # Fortschritt festhalten
    td log --decision "JWT statt Sessions"     # Entscheidung + Begründung
    td log --blocker "Warte auf API-Keys"      # Blocker

### Vor Kontextende (KRITISCH)
    td handoff <id> \
      --done      "Konkret Erledigtes" \
      --remaining "Konkret Offenes" \
      --decision  "Entscheidungen mit Begründung" \
      --uncertain "Offene Fragen"

### Regeln
1. Keine Arbeit ohne td start
2. Kein Sessionende ohne td handoff
3. Keine Selbst-Freigabe: ein anderer Session-Kontext reviewt
4. Entscheidungen loggen: die nächste Session braucht das *Warum*
5. Unsicherheiten loggen: verstecke nicht, was du nicht weißt

In der Praxis heißt das: Claude legt für jeden Task automatisch ein Issue an, aktualisiert es während der Arbeit, und hinterlässt am Ende einen Handoff. Ich muss dafür fast nichts machen.

Der Handoff-Trigger bei 50–60% Kontext

Der wichtigste Teil ist das Timing. Ich fordere den Handoff zwischen 50% und 60% Kontextfüllung an, nicht erst bei 90%.

Das wirkt verschwenderisch. Die Hälfte des Kontextfensters bleibt ungenutzt. Der Grund ist aber: Ein guter Handoff braucht selbst Kontext.

Bei 90% Füllung ist das Modell in genau der Lage, in der es am wenigsten leisten kann. Frühere Teile der Conversation sind bereits komprimiert oder verdrängt. Die Zusammenfassung wird dünn, generisch, und lässt eventuell Details weg, die die nächste Session braucht: "Auth implementiert" statt "Auth implementiert, aber Refresh-Token-Rotation fehlt, weil unklar ist, ob mehrere parallele Sessions pro User erlaubt sein sollen".

Bei 50–60% ist die Session noch vollständig präsent. Der Handoff wird konkret. Und es bleibt genug Puffer, um ihn sauber zu formulieren und offene Kleinigkeiten noch abzuschließen.

Die zweite Begründung ist Qualität während der Arbeit: Ein Kontextfenster jenseits von 70% produziert erfahrungsgemäß deutlich schlechteren Code. Details aus der Mitte der Conversation gehen verloren, Wiederholungen häufen sich, das Modell fängt an, Dinge neu zu erfinden, die vor 300 Zeilen schon entschieden waren. Ein früher Schnitt vermeidet diese Zone komplett.

Praktisch sieht das so aus (was Claude Code von sich aus macht):

td handoff td-a1b2 \
  --done      "OAuth-Callback-Endpoint, Token-Storage, Login-UI" \
  --remaining "Refresh-Token-Rotation, Logout-Endpoint, Error-States" \
  --decision  "httpOnly-Cookies statt localStorage (sicherer gegen XSS)" \
  --uncertain "Sollen mehrere aktive Sessions pro User erlaubt sein?"

Danach: /clear. Die neue Session startet, in dem man td start <td-issue-id>, eingibt. Claude liest den Handoff, und weiß in wenigen hundert Token, wie der Stand vom Issue ist. Ohne die vollständigen 60% Conversation-Historie, die dorthin geführt haben.

Das ist der eigentliche Trick. Der Handoff ist eine verlustbehaftete, aber gezielte Kompression, im Gegensatz zur automatischen Kontext-Komprimierung, die nicht weiß, was wichtig ist. Ich entscheide (bzw. das Modell entscheidet nach klaren Vorgaben), welche vier Kategorien überleben: Erledigtes, Offenes, Entscheidungen, Unsicherheiten. Alles andere darf weg.

Was eine "Session" in td überhaupt ist

Bisher war ständig von "Sessions" die Rede, ohne dass klar war, was das technisch bedeutet. Das lohnt einen eigenen Abschnitt, denn der ganze Review-Mechanismus hängt daran.

Eine td-Session ist kein Login und kein Benutzerkonto. Es ist eine ID, die td aus dem laufenden Agenten-Kontext und dem Git-Branch ableitet:

$ td session
SESSION: ses_9fd546 [claude-code_25695] on branch: default

Diese ID hängt am Kontextfenster. Beginnt Claude Code nach /clear von vorn, ist das für td eine andere Session:

$ td usage --new-session
NEW SESSION: ses_103923 on branch: default (previous: ses_36e0a3)
  You are a new context. Issues from the previous session may be awaiting review.

"Session" heißt also schlicht: ein Kontextfenster, ein Durchlauf. Und weil td jede Aktion mit dieser ID stempelt (wer hat gestartet, wer hat geloggt, wer hat den Handoff geschrieben), weiß es später, welches Kontextfenster ein Issue tatsächlich angefasst hat.

Genau das ermöglicht eine Buchführung, die ein normaler Issue-Tracker nicht hat: td kennt den Unterschied zwischen "diese Aufgabe wurde bearbeitet" und "diese Aufgabe wurde von diesem Kontext bearbeitet".

Der Review-Gate in der Praxis

Ein Issue durchläuft die üblichen Zustände:

open  ==>  in_progress  ==>  in_review  ==>  closed
                               |
                               +-- reject ==>  open

Interessant ist der Übergang von in_review nach closed. Angenommen, meine Session hat an td-3ced83 gearbeitet, einen Handoff geschrieben und das Issue eingereicht:

$ td review td-3ced83
REVIEW REQUESTED td-3ced83 (session: ses_9fd546)

Ein td show zeigt jetzt nicht nur den Inhalt, sondern auch die Beteiligungen:

td-3ced83: Demo Auth
Status: [in_review]

CURRENT HANDOFF (ses_9fd546, just now):
  Done:       - Callback-Endpoint
  Remaining:  - Refresh-Token-Rotation
  Decisions:  - JWT statt Sessions
  Uncertain:  - Mehrere aktive Sessions pro User?

AWAITING REVIEW - requires an implementation-independent session to approve/reject

SESSIONS INVOLVED:
  ses_9fd546 (implementer)

Die vorletzte Zeile ist der Kern: td verlangt eine Session ohne Implementierungsbeteiligung. Konkret gilt eine Session als unabhängig, wenn sie das Issue nie gestartet oder gestoppt hat und nicht als aktueller Bearbeiter eingetragen ist.

Versucht dieselbe Session, ihre eigene Arbeit freizugeben, kommt sie nicht durch:

$ td approve td-3ced83
ERROR: cannot approve: you implemented this issue, so approving it needs an
attribution. If a sub-agent or another party reviewed it, name them:
--reviewed-by "<who>". If you reviewed your own work, say so:
--self-review --reason "..."

Nach einem /clear ist es ein neues Kontextfenster und damit eine neue Session-ID. Dieselbe Freigabe geht jetzt durch:

$ td approve td-3ced83
APPROVED td-3ced83 (reviewer: ses_7b0be2)

Beides steht anschließend im Issue: wer implementiert und wer freigegeben hat.

Für den Reviewer selbst sind es zwei Kommandos:

td reviewable         # Was darf ich prüfen (Arbeit fremder Sessions)
td show <id>          # Handoff und Log lesen, bevor man urteilt

Warum das für Kontextmanagement relevant ist

Das klingt nach viel Bürokratie für eine Ein-Personen-Umgebung. Der Grund dahinter ist aber kein organisatorischer, sondern genau der, um den es in diesem Artikel die ganze Zeit geht.

Eine Session, die Code geschrieben hat, hat den gesamten Denkweg dorthin im Kontext, inklusive aller Annahmen, die sie unterwegs getroffen hat. Sie prüft deshalb nicht nur den Code, sondern auch ihre Erinnerung an den Code. Fehlt im Repo eine Datei, die nur in der Conversation existierte, fällt das dieser Session nicht auf: In ihrem Kontextfenster ist die Datei ja vorhanden. Die td-README nennt das treffend "works on my context"-Bugs.

Eine frische Session hat diesen vermeintlichen Vorteil nicht, und das ist hier ausdrücklich der Zweck. Sie sieht nur, was tatsächlich da ist: den Diff und den Handoff. Ihre Unwissenheit ist das Prüfinstrument. Auf diesem Wege wurden schon mehr als einmal echte Bugs gefunden.

Damit schließt sich der Kreis zum Handoff: Wenn eine unabhängige Session mit dem Handoff nichts anfangen kann, war der Handoff schlecht. Der Review ist also nebenbei ein Qualitätstest für die eigene Kontext-Kompression.

Der Mensch braucht eine andere Oberfläche als die AI

td ist allerdings konsequent für Agenten gebaut. Die Ausgaben sind kompakt, tokensparsam und maschinenfreundlich. Für die AI ist das genau richtig. Für mich als Mensch, der wissen will, was in fünf Projekten gerade offen ist, eher nicht.

Und hier liegt eine Falle, die direkt zum Thema des Artikels gehört: Jede Frage, die ich der AI über den Projektstand stelle, kostet Kontext. Ein "zeig mir mal alle offenen Issues" bedeutet Tool-Call, Ausgabe, Zusammenfassung. Das sind schnell ein paar tausend Token, die für die eigentliche Arbeit fehlen. Und es ist zusätzlich der Umweg über einen Vermittler, der mir Daten vorliest, die ich genauso gut selbst ansehen könnte.

Deshalb habe ich td-gui gebaut: eine lokale Weboberfläche für dieselbe td-Datenbank. Ein Blick in den Browser kostet null Token.

td-gui: Issue-Liste nach Status gruppiert, mit TDQ-Suchfeld und Statusfiltern

Die Issue-Liste, gruppiert nach Status und mit Anzahl pro Gruppe: zuerst das, was gerade läuft, dann das Offene, dann in_review als Arbeitsvorrat für mich als Reviewer. Das grüne "connected" oben rechts zeigt die offene Verbindung zur td-API, über die Änderungen aus dem Terminal sofort in der Ansicht landen.

td-gui                        # sucht sich einen Port, öffnet den Browser
td-gui --port 7777            # fester Port
td-gui --work-dir ../projekt  # anderes Projekt

Ein Go-Binary mit TypeScript-Frontend, ohne externe Abhängigkeiten, lauscht nur auf 127.0.0.1. Voraussetzung ist td ab v0.57.0 und ein Projekt, in dem td init gelaufen ist.

Architektonisch ist ein Punkt wichtig, gerade nach dem vorigen Abschnitt: td-gui schreibt nie direkt in die Datenbank. Es findet oder startet td's eigene HTTP-API (td serve) und leitet alles dorthin weiter. Jeder Schreibvorgang läuft damit durch dieselbe Validierung, dieselben Migrationen und dieselben Review-Regeln wie der CLI-Aufruf. Der Review-Gate von oben lässt sich also nicht dadurch aushebeln, dass man statt des Terminals den Browser benutzt, und die Attribution (unabhängig, delegiert oder Selbstprüfung) ist in der Oberfläche genauso abgebildet.

Praktisch nutze ich es für die Dinge, die im Terminal mühsam sind:

  • Backlog am Stück sehen, nach Status gruppiert, statt in Listenausgaben zu blättern
  • Reviews abarbeiten: Handoff lesen, dann genehmigen oder mit Begründung zurückweisen, ohne die laufende AI-Session dafür zu unterbrechen
  • Boards mit Swimlanes und Statuswechsel per Drag-and-drop
  • TDQ-Suche mit derselben Query-Sprache, die auch die Boards benutzen
  • Markdown-Rendering für Beschreibungen, Kriterien und Kommentare, was Handoffs deutlich lesbarer macht
  • Live-Aktualisierung, wenn die AI parallel im Terminal Issues anfasst

Die Arbeitsteilung ist am Ende dieselbe wie beim Rest dieses Artikels: Die AI bekommt die tokensparsame Textschnittstelle, ich bekomme die Ansicht, die für Menschen gemacht ist. Beide arbeiten auf denselben Daten.

Das Zusammenspiel

Alle drei Bausteine haben eine klar getrennte Zuständigkeit:

┌─────────────────────────────────────────────────────────┐
│ Globale CLAUDE.md    =>  td-Workflow, Arbeitsstil        │
│                          (klein, stabil, projektübergr.) │
├─────────────────────────────────────────────────────────┤
│ Memory               =>  Präferenzen über mich            │
│                          (minimal, regelmäßig ausgemistet)│
├─────────────────────────────────────────────────────────┤
│ Repo-Markdown        =>  Projektwissen, Entscheidungen    │
│                          (versioniert, on demand gelesen) │
├─────────────────────────────────────────────────────────┤
│ td                   =>  Arbeitsstand, Handoffs           │
│                          (pro Projekt, transient)         │
└─────────────────────────────────────────────────────────┘

Die Faustregeln:

  • Überdauert es das Projekt? ==> Memory (selten)
  • Gehört es zum Projekt? ==> Markdown im Repo
  • Beschreibt es den aktuellen Stand? ==> td
  • Ist es nur für diese eine Unterhaltung relevant? ==> Nirgends. Weg damit

Was das in der Praxis bringt

Der spürbarste Effekt ist nicht, dass die AI mehr weiß. Es ist, dass sie weniger Falsches weiß.

Ein Kontextfenster, das zu 20% aus eingespielten Memories besteht, von denen ein Viertel überholt ist, produziert selbstbewusst falsche Ergebnisse. Ein schlankes Fenster mit gezielt nachgeladenen, versionierten Markdown-Dateien produziert nachvollziehbare. Und ein Handoff bei ~55% statt einer Auto-Komprimierung bei 95% ist der Unterschied zwischen "wir machen weiter" und "wir fangen fast von vorne an".

Der Aufwand hält sich in Grenzen: eine globale Regel, ein td init pro Projekt, ein paar Markdown-Dateien, die man ohnehin schreiben sollte, und ein Aufräum-Durchlauf im Monat.

Was mich am meisten überrascht hat: Der größte Gewinn liegt nicht bei der AI, sondern bei mir. docs/decisions/ und td-Handoffs sind auch dann noch nützlich, wenn kein Modell im Spiel ist. Kontextmanagement für AI ist am Ende bloß Projektdokumentation, die endlich einen Grund hat, aktuell zu bleiben.


Links:

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert