Blog
Der Deploy-Befehl der mich aus meinem eigenen Server ausgesperrt hat
Ein Flag zu viel und mein SSH-Zugang war tot. Warum ich rsync --delete aus jedem Deploy geworfen hab und was seither an seiner Stelle läuft.
10. Juli 2026
Ein Flag. Mehr war es nicht. `--delete`. Danach kam ich nicht mehr auf meinen eigenen Server.
Ich wollte eine kleine Site deployen. Der Befehl war Muskelgedächtnis, so tippe ich das seit Jahren:
```bash
rsync -av --delete ~/projekte/mein-projekt/ deploy@server:~/
```
Sieht harmlos aus. `-av` ist archive plus verbose, `--delete` räumt auf dem Ziel alles weg was in der Quelle nicht mehr existiert. Klingt sauber, und ist es auch, solange Quelle und Ziel dieselbe Struktur haben. Meine hatten das nicht. Ich hatte aus Reflex ins Home-Verzeichnis synct statt in den Projektordner darunter. Und ein Home-Verzeichnis auf einem Server enthält Dinge die in meinem lokalen Projektordner nun mal nicht liegen. Zum Beispiel den Ordner `.ssh` mit der `authorized_keys`.
`--delete` hat exakt das gemacht was in der Manpage steht. Alles was auf dem Ziel liegt und in der Quelle fehlt, gelöscht. Nach ungefähr 30 Sekunden war der Befehl durch. Meine `authorized_keys`: leer. Der SSH-Login mit meinem Key: abgelehnt. Kein Rein mehr, kein Deploy mehr, kein Rollback von außen.
## Was der Schaden wirklich war
Das Dumme an einem toten SSH-Zugang ist, dass du ihn nicht per SSH reparieren kannst. Ich hatte keine zweite offene Session mehr, die Verbindung war schon zu. Also blieb nur der Weg über die Recovery-Konsole des Hosters, Passwort neu setzen, Key manuell wieder in die `authorized_keys` schreiben, Fingerprints prüfen. Bis alles wieder stand, war ein ganzer Arbeitstag weg. Ungefähr acht Stunden, in denen ich nichts gebaut habe, sondern nur einen Fehler rückgängig gemacht der zwei Sekunden Tippen war.
Und es blieb nicht bei dem einen Server. Ein Projekt lief zu dem Zeitpunkt auf einer Kiste die von genau diesem Zugang abhing. Ich musste es noch am selben Tag notfallmäßig auf eine andere Dev-Box umziehen, damit es online blieb. Ein Flag, zwei Baustellen.
## Warum rsync für Deploys von Anfang an die falsche Idee war
Der ehrliche Teil: der Fehler war nicht `--delete`. Der Fehler war, `rsync` überhaupt als Deploy-Werkzeug zu benutzen. `rsync` gleicht zwei Verzeichnisse ab. Es hat keine Ahnung was dein Projekt ist, was Server-State ist, was heilig ist und was Müll. Es kennt nur links und rechts, und `--delete` sagt ihm, rechts soll aussehen wie links. Ein Deploy will aber genau das nicht. Ein Deploy will deinen Code aktualisieren und den Rest des Servers in Ruhe lassen.
Der Server hat immer Dinge die dein Repo nicht hat. SSH-Keys, Container-State, Logs, Tokens, Shell-Historie. Sobald `--delete` im Spiel ist, entscheidet dein lokaler Ordner darüber ob die weiterleben. Das ist eine Verantwortung die ein Sync-Tool nicht tragen sollte.
## Wie ich jetzt deploye
Ich habe `rsync` komplett aus meinen Deploys geworfen. Drei Wege haben es ersetzt, je nachdem was ich bewege.
Code geht über Git. `git push` von lokal, `git pull` auf dem Server. Eine einzige Quelle der Wahrheit, versioniert, mit Historie, und Git fasst nichts an das nicht im Repo ist. Wenn ich mich vertippe, ist der schlimmste Fall ein Reset auf den letzten Commit, kein gelöschter Key.
Container gehen über Docker. Auf dem Server ein `docker compose up -d --build`, fertig. Wenn ich ein fertiges Image ohne Registry rüberschieben will, nehme ich
```bash
docker save mein-image:latest | gzip | ssh deploy@server 'docker load'
```
Das schiebt ein Tar durch die SSH-Pipe und lädt es drüben. Kein Verzeichnis-Abgleich, keine Lösch-Logik, nur ein Artefakt das ankommt oder nicht. Details zum `docker save` Format stehen in der [Docker CLI Reference](https://docs.docker.com/reference/cli/docker/image/save/).
Einzelne Files gehen über `scp`. Additiv, ohne `--delete`, ohne Verzeichnis-Spiegelung. `scp` kann nichts löschen was ich nicht explizit nenne. Genau das will ich bei einem manuellen Handgriff.
Wenn du selbst gerade zum ersten Mal etwas auf einen eigenen VPS bringst, ist das Recipe [Docker Compose, direct hosting on a VPS you own](/recipes/8.2-docker-compose) der ruhige Einstieg. Und wenn Deploys später automatisch aus einer Pipeline laufen sollen, zeigt das Playbook [Claude Code headless in CI/CD](/playbooks/claude-code-headless-in-ci-cd) wie das ohne manuelles Kommando geht.
## Der Hook der es nie wieder zulässt
Auf Willenskraft verlasse ich mich bei sowas nicht mehr. Muskelgedächtnis war ja gerade das Problem. Also blockt jetzt ein PreToolUse-Hook jeden `rsync`-Aufruf, bevor er überhaupt startet:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash ~/.claude/hooks/rsync-block.sh" }
]
}
]
}
}
```
Das Script liest den Befehl aus dem Tool-Input, greppt nach `rsync` und beendet sich mit Exit-Code 2 wenn es fündig wird. Exit-Code 2 blockt den Aufruf in Claude Code, der Befehl läuft nie los. Wenn ich in einem seltenen Fall doch mal `rsync` brauche, muss ich es bewusst mit einer Env-Variable freischalten. Genau diese eine Sekunde Reibung reicht, damit ich nachdenke statt nur zu tippen.
Wie Hooks grundsätzlich funktionieren, steht in der Lesson [Hooks und Skills](/levels/4/04-hooks-und-skills), und die offiziellen Event-Typen plus das Deny-Verhalten findest du in der [Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks).
## Was du daraus mitnimmst
Wenn du Sites oder Container auf einen Server bringst den du selbst besitzt, nimm kein Werkzeug das löschen kann für eine Aufgabe die nur hinzufügen soll. Code gehört in Git. Container gehören in ein Image. Und ein destruktives Flag gehört hinter eine Reibung die dich zum Nachdenken zwingt, nicht in dein Muskelgedächtnis. Der teuerste Fehler ist selten der komplizierte. Meiner war acht Zeichen lang.