DoMe Dynamics
AI Agent Systems
Zurueck zum Log
pythonsubprocesswindowsutf-8

Python subprocess unter Windows: PATH-Auflösung, cp1252-Encoding und Session-Kontext beim CLI-Aufruf

Erste Woche, drei Subprocess-Fehler — wie der Bot lernte, auf Windows zu sprechen

Der DCO begann als Telegram-Bot, der die Claude-CLI aufruft. In der ersten Woche scheiterte jeder Aufruf auf eine neue Art: erst fand Windows das Programm nicht, dann zerfielen die Umlaute, dann war der Gesprächskontext kaputt.

Der Dynamic Central Orchestrator fing klein an: ein Telegram-Bot, der eingehende Nachrichten an die Claude-CLI weiterreicht und die Antwort zurückschickt. Im Prinzip ein Einzeiler — subprocess.run(["claude", "-p", prompt]). In der Praxis war es drei Tage Lehrgeld, verteilt auf drei Fehler, die nichts miteinander zu tun hatten außer dem Ort, an dem sie passierten: der Grenze zwischen meinem Python-Prozess und einem anderen Programm auf Windows.

Fehler 1: Windows findet das Programm nicht

Der erste Lauf endete mit FileNotFoundError: [WinError 2]. Im Terminal lief claude problemlos — also musste es ja installiert sein. Der Bot sah es trotzdem nicht.

Der Grund: Ein Service- oder Hintergrundprozess erbt nicht denselben PATH wie meine interaktive Shell. Wenn ich claude in die Konsole tippe, durchsucht die Shell ihren PATH und findet den npm-Wrapper. Mein Python-Prozess, gestartet aus einem anderen Kontext, hatte diesen PATH-Eintrag nicht. Für mich war das Programm „da". Für den Subprozess existierte es nicht.

Die Lösung war nicht, den Befehl anders zu schreiben, sondern ihn vorher aufzulösen: shutil.which("claude") durchsucht den PATH des laufenden Prozesses und gibt den absoluten Pfad zurück — oder None, wenn nichts gefunden wird. Diesen absoluten Pfad übergebe ich dann an subprocess.run. Aus „verlass dich darauf, dass das Programm im PATH ist" wird „finde es einmal, sauber, und scheitere laut, wenn es fehlt".

Fehler 2: aus „für" wird „für"

Der nächste Lauf funktionierte — fast. Die Antworten kamen zurück, aber jeder Umlaut war zerschossen. „für" wurde zu „für", „schön" zu „schön". Klassisches Mojibake: UTF-8-Bytes, durch eine Latin-1-Brille gelesen.

Windows ist hier eine besondere Falle, weil die Standard-Konsolen-Codepage nicht UTF-8 ist. Wenn Text als Kommandozeilen-Argument durch die Prozessgrenze geht, hängt die Kodierung von Dingen ab, die ich nicht kontrolliere — der Codepage des Eltern- und des Kindprozesses. Ein Prompt mit deutschen Umlauten wird unterwegs umkodiert, und niemand fragt mich, ob das so gewollt war.

Der Fix war eine Verschiebung des Übergabewegs: nicht den Prompt als Argument anhängen, sondern über stdin einspeisen — subprocess.run(cmd, input=prompt, encoding="utf-8"). Über stdin ist der Text ein Datenstrom mit expliziter Kodierung, kein von der Shell interpretiertes Argument. Das löst gleich zwei Probleme auf einmal: die Umlaute bleiben heil, und lange oder sonderzeichen-haltige Prompts werden nicht vom Quoting der Shell zerlegt. Letzteres wurde später, bei den .cmd-Wrappern, noch zu einer eigenen Geschichte.

Fehler 3: der Bot vergisst, worüber wir reden

Jetzt liefen die Aufrufe sauber durch — aber der Bot hatte kein Gedächtnis. Jede Nachricht war für ihn die erste. „Und dann?" war eine sinnlose Frage, weil es kein „davor" gab.

Das ist kein Subprocess-Bug im engeren Sinn, aber es ist dieselbe Klasse: Ein frisch gestarteter Prozess hat genau den Kontext, den ich ihm mitgebe — und keinen Zeichen mehr. Der CLI-Aufruf ist zustandslos. Wenn ich Gesprächsverlauf will, muss ich ihn selbst zusammenbauen und mitschicken.

Die naive Variante — einfach alle bisherigen Nachrichten aneinanderhängen — produziert schnell Matsch, in dem das Modell nicht mehr unterscheiden kann, was meine Eingabe war und was seine eigene frühere Antwort. Es braucht klare Trennmarken: wer hat was gesagt, wo endet ein Beitrag, wo beginnt der nächste. Erst mit strukturiertem Verlauf statt einem Textbrei wurde aus den Einzelaufrufen ein Gespräch.

Drei Fehler, ein Muster

Im Rückblick war keiner dieser Fehler schwer. Jeder einzelne war eine Stunde Recherche und eine Zeile Code. Was sie verbindet, ist die Stelle, an der sie auftraten — und die Annahme, die sie alle teilten: dass ein anderes Programm aufzurufen „einfach" ist.

Ist es nicht. Auf Windows zerfällt „ein Programm aufrufen" in mindestens drei getrennte Probleme: Wird das Programm überhaupt gefunden? Kommt der Text unverfälscht an? Hat der Aufruf genug Kontext, um sinnvoll zu antworten? Jedes davon ist unsichtbar, bis man es trifft, und keines verrät sich vorher.

Die teuerste Annahme war „funktioniert in meinem Terminal". Mein Terminal hat den richtigen PATH, eine UTF-8-fähige Konsole und einen Menschen, der den Kontext im Kopf hat. Ein Bot-Prozess hat nichts davon automatisch. Die Lücke zwischen „läuft bei mir interaktiv" und „läuft als Dienst" ist genau die Lücke, in der diese drei Fehler wohnen.

Was offen ist

Die hier beschriebenen Fixes sind robust, aber sie sind projektspezifisch zusammengesucht. Was fehlt, ist eine saubere Abstraktion: eine kleine Wrapper-Funktion, die shutil.which auflöst, stdin mit UTF-8 erzwingt und Fehler einheitlich behandelt — einmal richtig gebaut, überall wiederverwendet. Stattdessen sind die Lehren über mehrere Projekte verteilt als Konventionen dokumentiert und werden bei jedem neuen Subprozess-Aufruf einzeln angewendet. Das funktioniert, ist aber fehleranfällig: Wer die Konvention nicht kennt, baut Fehler 1 bis 3 frisch nach.

Die ehrliche Bilanz nach der ersten Woche: Der Bot konnte sprechen. Aber dass „mit einem anderen Programm reden" so viel Reibung erzeugt, war die erste echte Lektion des Projekts — und eine, die sich danach in jedem weiteren Windows-Subprozess wiederholt hat.


Solche Subprocess-Fehler kosten oft Stunden, bevor die eigentliche Ursache feststeht. Wenn du an ähnlichen Integrationen arbeitest, schreib mir — ich sammle solche Fälle.