Am Samstagabend wollte ich vom iPad aus kurz etwas auf dem Mac nachsehen. Prompt 3 auf, SSH-Verbindung zum Mac, wie schon hundertmal. Statt eines Prompts bekam ich diesmal eine längere Warnung von Powerlevel10k. Darunter: nichts. Kein Prompt, und als ich ls eintippte, passierte ebenfalls nichts.
Klingt nach einer hängenden Shell. War es nicht.
Die erste Spur: Powerlevel10k

Powerlevel10k ist das Prompt-Theme, das ich in der zsh verwende. Es hat ein Feature namens Instant Prompt. Ganz oben in der .zshrc steht ein Block, der den Prompt sofort anzeigt, während der Rest der Konfiguration im Hintergrund weiterlädt. Damit das funktioniert, darf nach diesem Block nichts mehr auf die Konsole schreiben. Tut es doch etwas, meldet sich Powerlevel10k mit genau dieser Warnung und zeigt unter der letzten Zeile an, was ausgegeben wurde.
Auffällig war, dass unter -- console output produced during zsh initialization follows -- nichts zu sehen war. Irgendetwas wurde also ausgegeben, nur nichts Sichtbares. Das war der erste Hinweis, den ich zu dem Zeitpunkt noch nicht richtig eingeordnet habe.
Ctrl-C hilft nicht
Meine erste Vermutung: Ein Befehl in der .zshrc wartet auf eine Eingabe. Ein ssh-add, das nach der Passphrase fragt, oder der Update-Dialog von Oh My Zsh. Weil Powerlevel10k während des Instant Prompts die Ausgaben umleitet, sieht man so eine Frage nicht. Die Shell würde dann unsichtbar warten.
Also Ctrl-C. Das ist auf dem iPad ohne Hardware-Tastatur schon die erste Hürde, weil die Control-Taste in der Zusatzleiste von Prompt 3 nicht auf den ersten Blick zu finden ist. Als ich sie gefunden hatte, passierte trotzdem nichts.
Statt weiter in der hängenden Sitzung herumzuprobieren, habe ich von außen nachgesehen. In einer zweiten Sitzung auf dem Mac lassen sich alle Prozesse mit ihrem Terminal und dem Elternprozess anzeigen:
ps -A -o pid,ppid,stat,tty,command | grep -v grep | grep -E 'ttys|zsh'Für die SSH-Sitzung vom iPad sah das Ergebnis so aus (gekürzt):
58738 58732 S ?? sshd-session: staude@ttys001
58739 58738 Ss+ ttys001 -zsh
59148 58752 S ttys001 /Users/staude/.cache/gitstatus/gitstatusd-darwin-arm64 ...Die zsh hatte keinen Kindprozess, auf den sie hätte warten können. Und gitstatusd lief, ein Hilfsprozess, den Powerlevel10k erst am Ende der Initialisierung startet. Die .zshrc war also komplett durchgelaufen, die Shell saß am Prompt und wartete auf Eingaben.
Was ich bis dahin nicht auf dem Schirm hatte: Das Problem lag gar nicht auf dem Mac. Es lag in der Anzeige auf dem iPad.
Die unsichtbare Ausgabe sichtbar machen
Wenn ein Terminal plötzlich alles verschluckt, steckt meist eine Escape-Sequenz dahinter, mit der es nichts anfangen kann. Um zu sehen, was die .zshrc tatsächlich ausgibt, startet man eine interaktive zsh und leitet die Ausgabe durch cat -v. Das macht Steuerzeichen sichtbar, ein Escape erscheint dann als ^[:
zsh -i -c exit 2>&1 | cat -v | tail -20Heraus kam eine einzige Zeile:
^[P$f{"hook": "SourcedRcFileForWarp", "value": { "shell": "zsh", "uname": "Darwin" }}M-^\Das ist kein Fehler im eigentlichen Sinn, sondern eine Nachricht an das Terminal Warp. Warp schreibt bei der Einrichtung ein kleines Snippet in die .zshrc, damit es erkennt, dass die Shell fertig geladen ist. Ich hatte Warp vor einiger Zeit ausprobiert, das Snippet war geblieben.
Technisch ist das eine DCS-Sequenz (Device Control String). Sie beginnt mit ESC P und endet mit einem String Terminator. Warp verwendet dafür das einzelne 8-Bit-Zeichen 0x9C, in der Ausgabe von cat -v als M-^\ zu sehen. In einem Terminal, das UTF-8 erwartet, ist ein einzelnes Byte 0x9C aber kein gültiges Zeichen. Prompt 3 erkennt das Ende der Sequenz deshalb nicht. Für das Terminal ist die Steuersequenz noch offen, und alles, was danach kommt, gilt als Teil davon: der Prompt, meine Eingaben, die Ausgabe von ls.
Gleichzeitig ist diese Zeile genau die Konsolenausgabe, über die sich Powerlevel10k beschwert hat. Beide Symptome hatten also dieselbe Ursache.
Der Fix
Die Stelle in den Konfigurationsdateien findest du so:
grep -n SourcedRcFileForWarp ~/.zshrc ~/.zprofile ~/.zshenv 2>/dev/nullWenn du Warp nicht mehr verwendest, löschst du die Zeile einfach. Wenn du Warp weiter nutzen willst, sorgst du dafür, dass die Sequenz nur dann ausgegeben wird, wenn die Shell tatsächlich in Warp läuft. Warp setzt dafür die Variable TERM_PROGRAM auf WarpTerminal:
[[ "$-" == *i* && $TERM_PROGRAM == WarpTerminal ]] && printf '\eP$f{"hook": "SourcedRcFileForWarp", "value": { "shell": "zsh", "uname": "Darwin" }}\x9c'Danach eine neue SSH-Verbindung vom iPad: Der Prompt war da, und die Warnung von Powerlevel10k war ebenfalls verschwunden.
Was ich mitnehme
Ich habe eine ganze Weile in der falschen Richtung gesucht, weil alles nach einer hängenden Shell aussah. Der Blick von außen mit ps hat das in einer Minute widerlegt. Und cat -v hat gezeigt, was das Terminal mir verschwiegen hat.
Dazu kommt eine zweite Lehre: Terminal-Programme schreiben gern Integrations-Snippets in die .zshrc. Das gilt für Warp, aber auch für iTerm2 und andere. Solange man nur mit diesem einen Terminal arbeitet, fällt das nicht auf. Sobald man sich mit einem anderen Programm verbindet, kann so ein Snippet stören. Es lohnt sich, solche Zeilen an das jeweilige Terminal zu binden.
Kurz gesagt: Wenn das Terminal schweigt, frag zuerst ps, ob die Shell überhaupt hängt.
Links
- Powerlevel10k, Abschnitt Instant Prompt: github.com/romkatv/powerlevel10k#instant-prompt

