Warum ich Quellcode nicht unverschlüsselt auf GitHub lege: Privates Git auf Win/Mac, verschlüsselte Cloud-Backups und die Grenzen mobiler Entwicklung
Als ich Git zwischen Windows und Mac eingerichtet habe, stand nicht die Geschwindigkeit im Vordergrund. Ich hatte für langfristige kommerzielle Projekte eine klare Voraussetzung: Den Kern-Quellcode möchte ich keinem Cloud-Anbieter im Klartext überlassen – auch nicht in einem GitHub-Private-Repository.
Ich behaupte damit nicht, GitHub würde heimlich Code entwenden. Vielmehr will ich den Wert jahrelanger Arbeit nicht davon abhängig machen, dass es nie zu einem internen Zugriffsfehler, Account-Diebstahl, einem Angriff auf die Lieferkette oder einer Sicherheitslücke beim Anbieter kommt. „Private“ bezeichnet eine Zugriffskontrolle, keine technische Unmöglichkeit für den Plattformbetreiber, die eigentlichen Git-Objekte zu lesen. Die Entschlüsselungsschlüssel sollen bei mir bleiben; die Cloud soll Chiffretext speichern.
Darum lief der Central als Bare-Repo auf meinem Windows-Rechner, der Mac griff per SMB darauf zu. Später kamen ein Mirror auf Laufwerk E:, getrennt aufbewahrte verschlüsselte 7z-Archive und Cryptomator-Kopien in OneDrive / Google Drive hinzu. Was ich damals noch nicht gelöst hatte: Eine verschlüsselte Kopie in einer Cloud zu speichern ist nicht gleichbedeutend mit Git-push/pull zu einem verschlüsselten Remote. Als ich häufiger vom Smartphone aus ChatGPT/Codex für Entwicklungsaufgaben einsetzen wollte, wurde genau diese Lücke zum Problem.
Die folgenden Git-Befehle, Wiederherstellungshinweise und Ergebnisse des isolierten Git-2.47.3-Test-Repositories bleiben aus der ursprünglichen Fassung erhalten. Der Test belegt Git-Mirror- und Ref-Verhalten, nicht die erfolgreiche Einführung eines verschlüsselten Cloud-Git-Remotes.
Warum GitHub Private nicht meine Ausgangslösung war
Das ist ein bewusst gewähltes Bedrohungsmodell, kein Vorwurf gegenüber GitHub oder anderen Cloud-Diensten. Die GitHub-Bedingungen für private Repositories beschreiben Vertraulichkeit und Ausnahmen beim dienstseitigen Zugriff. Sie sind etwas anderes als eine vollständige clientseitige Verschlüsselung mit ausschließlich selbst verwalteten Schlüsseln. Für viele Projekte reicht ein Private-Repo vollkommen; für meinen hochwertigen kommerziellen Kerncode habe ich anders entschieden.
| Variante | Was der Anbieter potenziell sieht | Einordnung |
|---|---|---|
| Normales GitHub Private | Gewöhnliche Git-Objekte, Quellcode und Commit-Historie hinter der Zugriffskontrolle | Verletzt meine Klartext-Grenze |
| Lokales Git + Cryptomator-Cloud-Sync | Verschlüsselte Dateien; Größen- und Zeitinformationen können sichtbar bleiben | Sinnvoll für Offsite-Backups, nicht für native Git-Zusammenarbeit |
| Gesamten Git-Remote auf dem Client verschlüsseln | Ziel: Remote hält nur Chiffretext, abhängig von der Implementierung | Kandidat für Push/Pull, muss gesondert getestet werden |
| Offline-Archiv mit 7z-Verschlüsselung | Getrennter verschlüsselter historischer Stand | Katastrophenvorsorge, keine Echtzeit-Synchronisierung |
Auch das ist keine absolute Sicherheit. Malware auf dem Entwicklerrechner, verlorene Schlüssel oder an einen KI-Dienst gesendete Klartext-Dateien umgehen die Grenze. Cryptomator dokumentiert entsprechende Sicherheitsgrenzen und die verbleibenden Metadaten.
Architekturskizze: Git-Arbeit bleibt lokal, die Cloud hält clientseitig verschlüsselte Backups und Offlinearchive sichern die Historie. Verschlüsselter Git-Remote-Push und mobile Entwicklung sind weiterhin Konzepte.
Win/Mac-Git-Sync im LAN: Warum das Bare-Repo als Central
So sah die Laufzeitbeziehung aus:
Windows-Arbeitsrepo ─── push / pull ──┐
├── Windows-Central (bare repo)
Mac-Arbeitsrepo ─ SMB ─ push / pull ──┘ │
│ Mirror-Aktualisierung
▼
E:-Mirror-Bare-Repo
│
Verschlüsseltes Archiv / Offline-Kopie
▼
Externe Festplatte, Offsite-Cloud
Von den vier Schichten tragen eigentlich nur die letzten drei die Backup-Verantwortung. Legt man sie nebeneinander, sieht man schnell, welche Schicht bei welchen Fehlern mit ausfällt:
| Schicht & Position | Kernaktion | Schützt vor | Kann nicht lösen |
|---|---|---|---|
| Kooperations-Central: Bare-Repo auf Windows-D: | Win/Mac push, pull | Bereits gepushter Code bleibt abrufbar, wenn ein Entwicklungsrechner ausfällt | Windows aus, SMB-Verbindung tot, Central selbst beschädigt; das ist kein unabhängiges Cold Backup |
| Gleich-Maschinen-Mirror: Mirror auf Windows-E: | clone --mirror, remote update | Wiederherstellung aus dem aktuellsten Mirror bei Verzeichnisschäden an der Central | D: und E: fallen gemeinsam aus; --prune kann Branches mitlöschen |
| Offline-Archiv: getrennte externe Festplatte | Verschlüsseltes 7z-Paket, 7z t | Rechnerausfall, Ransomware trifft Online-Kopien | Updates sind Handarbeit; Aktualität hängt vom Archivierungszeitpunkt ab |
| Offsite-verschlüsselt: Cryptomator + OneDrive / Google Drive | robocopy, Manifest-Prüfung der Ciphertexte, Download-Verifikation | Totalverlust der lokalen Geräte, Abruf aus der Ferne | Cloud-Sync kann Fehler weitertragen; kein uncommitteter Code, keine externen Daten |
Die RPO (bis zu welcher Version maximal Verlust droht) wird von der jeweils letzten erfolgreichen Übertragung, Synchronisation oder Archivierung jeder Schicht bestimmt; die RTO (wie lange eine Wiederherstellung dauert) hängt zusätzlich von Mediengeschwindigkeit, Entschlüsselung und Übungsstand ab. Ich habe damals die tatsächlichen Wiederherstellungszeiten nicht dokumentiert, deshalb vergleiche ich hier nur Wiederherstellungspfade – ohne erfundene Minuten als RPO/RTO auszugeben. D: und E: gehören zum selben Windows-Rechner; zwei Laufwerksbuchstaben eliminieren das gemeinsame Risiko eines Totalausfalls nicht.
Der entscheidende Punkt an der Central ist nicht „irgendein geteilter Ordner”, sondern dass darin ein bare repository (nacktes Repository) liegt. Es enthält Git-Objekte, Referenzen und die Repository-Konfiguration – aber kein normales Arbeitsverzeichnis. Beide Entwicklungsrechner haben jeweils ihren eigenen Arbeitsbaum, Änderungen entstehen lokal und werden erst nach dem Commit in die Central gepusht.
Laut offizieller git init-Dokumentation von Git ist die Grundform für ein neues Bare-Repo:
# 示例路径,不是我原机器的目录
git init --bare "D:/git-central/project.git"
# 已有 Windows 工作仓库中配置 Central
git remote add central "D:/git-central/project.git"
git push -u central master
Auf Mac-Seite hängt man zuerst die SMB-Freigabe lokal ein und setzt dann den Einhängepfad als central-Remote. Das unten verwendete master ist der damalige Branchname dieses Repositorys; wer als Standardbranch main nutzt, sollte master nicht blind übernehmen.
git remote add central "/Volumes/git-central/project.git"
git fetch central
git pull --ff-only central master
Aus dem alten Betriebshandbuch stammen noch ein paar sehr konkrete Mac-seitige Diagnosebefehle, die die Frage „Hat Git nichts geholt, oder hängt SMB überhaupt nicht?” überflüssig machen:
# 在 Mac 上挂载 Windows 的 SMB 共享(示例地址)
open "smb://<windows-host>/<share-name>"
# 先确定共享真正挂载到了哪里
ls /Volumes
ls "/Volumes/<share-name>"
# 再从项目工作区确认 Git remote 指向的位置和当前分支
git remote -v
git branch --show-current
git status --short
Die alten Notizen sahen vor, zuerst zu prüfen, ob das Bare-Repo im Freigabeverzeichnis wirklich sichtbar ist, und erst dann den central-Remote zu bedienen – nicht beim ersten git pull-Fehler panisch reset --hard auszuführen. Auf Windows-Seite galt dieselbe Reihenfolge: erst git status prüfen, committen, dann git push central master. Diese Befehle stammen aus den damaligen Betriebsnotizen; interne Hostnamen und Freigabepfade sind im Text durch Beispielplatzhalter ersetzt.
Zum Tagesablauf damals: Vor Arbeitsbeginn git pull central master, danach git status prüfen, dann git add ., git commit, git push central master. Der andere Rechner holt sich alles über denselben Zugang. Das --ff-only im Beispiel ist eine Absicherung, die ich heute bewusst beibehalte: Bei divergierenden Histories bleibt der Befehl stehen und zwingt zur Klärung, statt dass ein normaler pull still Merge-Commits erzeugt.
Dieses Arrangement hat einen sehr handfesten Vorteil: Fällt die Central aus, existiert die Git-Historie auf beiden Entwicklungsrechnern weiter; fällt ein Entwicklungsrechner aus, verschwindet die Historie nicht mit ihm. Ich habe „ein vollständiges Repository kann als Seed für den Wiederaufbau dienen” stets als Entwurfsprinzip behandelt. Allerdings gilt das nur unter Bedingungen: Das betreffende Repository muss die benötigten Branches, Tags und Objekte tatsächlich enthalten. Ein Single-Branch-Clone, ein Shallow Clone oder uncommittete Dateien im Arbeitsbaum zählen nicht automatisch als vollständiges Backup.
Eine leicht übersehene Grenze: Wer ein Bare-Repo direkt per SMB anspricht, hängt von Berechtigungen, Locks und Netzwerkstabilität des Dateisystems ab. Das alte Setup bediente meine Zweirechner-Gewohnheit gut – das heißt nicht, dass es für verteilte Standorte, mehrere Nutzer oder wackelige Netze taugt. Für einen dauerhaften Git-Dienst würde ich heute eher einen echten Remote-Transport wie Git over SSH nehmen, statt ein Netzlaufwerk als Universal-Git-Server zu missbrauchen.
Git-Mirror als Cold Backup: Synchronisiertes Löschen ist nicht dasselbe wie ein historischer Snapshot
Echte Sicherheitsabstände entstehen erst durch die Schichten hinter der Central. Ursprünglich lag auf Laufwerk E: ein per --mirror geklontes Bare-Repo – ohne Entwicklung, nur zur Aktualisierung; die externe Festplatte hielt verschlüsselte Archive, meist offline; die Cloud bekam noch eine verschlüsselte Offsite-Kopie. Das sind keine vier identischen git clone, sondern Antworten auf unterschiedliche Ausfallradien.
Die offizielle Definition von git clone --mirror verdient einen genauen Blick: Ein Mirror enthält nicht nur Branches, sondern synchronisiert auch weitere Referenzen gemäß dem Mirror-Refspec. Anders als ein normaler Entwicklungs-Clone kann ein späteres git remote update die entsprechenden Refs überschreiben. Beispiel für Erst-Erstellung und laufende Aktualisierung:
# 只在初始化冷备仓库时执行一次
git clone --mirror "D:/git-central/project.git" "E:/git-cold/project.git"
# 后续更新镜像。--prune 可能删除源端已经不存在的引用
git --git-dir="E:/git-cold/project.git" remote update --prune
# 只检查 Git 对象,并不等于完成一次恢复演练
git --git-dir="E:/git-cold/project.git" fsck --full
Das damalige Betriebshandbuch für das E:-Cold-Backup enthielt neben git remote update auch eine festgeschriebene Methode zur Commit-Prüfung:
# 在 mirror 裸仓库内执行;本例沿用历史 master 分支
git remote update
git log origin/master -1 --oneline
# 不只比最近一条 log;正式恢复还需要核对 refs 和对象
git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags
Das origin/master hier ist die Schreibweise aus den historischen Notizen; ob ein nackter Mirror seine Referenzen unter diesem Namen zugänglich macht, hängt von der tatsächlichen Konfiguration und dem git show-ref-Ergebnis ab. Die verlässlichere, allgemeingültige Vorgehensweise ist, direkt refs/heads/master und alle anderen zu sichernden Refs zu prüfen.
Ich habe mit Git 2.47.3 ein temporäres Repository aufgebaut und die --prune-Löschkette einmal durchgespielt: Branch test-prune pushen, Mirror synchronisieren, Branch aus der Central löschen, Mirror erneut aktualisieren. Die entscheidende Ausgabe sind nur wenige Zeilen:
git_version=git version 2.47.3
feature_after_sync=present 733c4dc75735672f2db8b4ed8185058ceddaf8c7
feature_after_prune=absent
git_fsck_exit=0
git_fsck_output=dangling commit 733c4dc75735672f2db8b4ed8185058ceddaf8c7
restored_master_sha=c7b763f667cbbba6c1bb39a6719236a554257a8c
Beachte: git fsck liefert Exit 0, der dangling commit ist noch auffindbar – aber der Mirror kennt die Branch-Referenz test-prune nicht mehr. Hängende Objekte können bei späterer Garbage Collection verschwinden. Ein Mirror ist keine „Zeitmaschine” – er synchronisiert den neuesten Stand, und er synchronisiert auch Löschungen. Dieses Experiment belegt Git-Referenzverhalten; die Verlässlichkeit von 7z und Cloud erfordert weiterhin echte Wiederherstellungsübungen mit realen Archiven.
Laut git remote-Dokumentation führt remote update einen Fetch aus; --prune räumt abgelaufene Remote-Referenzen gemäß Konfiguration auf. Ein Update überträgt in der Regel nur die benötigten neuen Objekte – kein vollständiges Neukopieren. Aber: Ein Mirror ist eine synchronisierte Kopie, kein unveränderlicher Snapshot. Löscht man upstream versehentlich einen Branch, kann das nächste Update mit Prune diese Löschung in den Mirror weitertragen. Er eignet sich hervorragend für die schnelle Wiederherstellung des aktuellen Stands, trägt aber die Verantwortung für „Schutz vor versehentlichem Löschen und Ransomware” nicht allein.
Deshalb hielt ich damals zwei getrennte Backup-Zweige bereit: einen, der das Cold-Repo als datiertes, verschlüsseltes 7z-Archiv auf einer normalerweise getrennten externen Festplatte ablegt; und einen, der das Git-Mirror-Repo in einem Cryptomator-Vault speichert und dessen Ciphertext-Verzeichnis in das lokale OneDrive- / Google-Drive-Sync-Verzeichnis kopiert. Beide können dieselbe Git-Historie schützen, aber man darf sich nicht einfach vorstellen, „alle Cloud-Kopien gehen zuerst durch 7z und dann durch Cryptomator”. Wertvoll ist, dass ein historisches Archiv beim nächsten Central-Update nicht überschrieben wird. Das Verschlüsselungspasswort gehört nicht zum Archiv dazu, und ein erfolgreicher Kopiervorgang ist noch kein gelungener Restore.
Central auf D: und Mirror auf E: – beide auf demselben Windows-Rechner – mildern Fehlbedienung am Repository oder einen defekten Datenträger, stoppen aber keinen Totalausfall, keinen Diebstahl und keine Ransomware auf derselben Maschine. Eine externe Festplatte erfüllt ihren Offline-Zweck nur, wenn sie nach dem Backup wirklich getrennt wird; Cloud-Speicher, der Löschungen oder Korruption synchronisiert hat, hält nicht unbedingt eine brauchbare alte Version bereit. Genau diese Unterschiede haben mich später davon abgebracht, mich mit „ich habe doch mehrere Backups” zufriedenzugeben.
Noch eine Scope-Klarstellung: Git sichert committete Git-Objekte und Referenzen – nicht automatisch die per .gitignore ausgeschlossenen großen Daten, uncommittete Änderungen im Arbeitsbaum oder externe Datenbanken. Wer Git LFS oder Submodule nutzt, muss separat prüfen, ob die jeweiligen Objekte und Abhängigkeiten gesichert sind. Ein vollständiges Repository-Mirror bedeutet nicht, dass die gesamte Entwicklungsumgebung vollständig gesichert ist. Laufzeitdaten wie Docker-Volumes brauchen ein eigenes Backup-Konzept; dazu habe ich separat über meine Docker-Volume-Backup-Praxis mit GitHub geschrieben.
Cryptomator + Cloud: SHA-256-Prüfung verschlüsselter Backups
An der Cloud-Schicht ist nicht der Upload das Komplizierte, sondern die Frage danach: Wie stellt man fest, dass keine Datei fehlt? Für Cryptomator + OneDrive und Cryptomator + Google Drive hatte ich jeweils eigene Betriebsnotizen – mit Ciphertext-Basismanifest, der Aufrufreihenfolge von VaultIntegrityCheck.ps1 und einer Anleitung, wie man nach dem Kopieren MISSING, CHANGED und EXTRA lokalisiert.
Cryptomator übernimmt die clientseitige Verschlüsselung von Dateiinhalten und -namen, der Cloud-Client übernimmt die Übertragung – das sind nicht zwei Funktionen derselben Software. Auch die offiziellen Cryptomator-Sicherheitshinweise betonen, dass Verschlüsselung kein Backup-Ersatz ist. Der tatsächliche alte Ablauf sah eher so aus:
E: Cold-Backup-Mirror / Cryptomator-Verschlüsselungsvault (Basis)
│
├─ Fingerprint-Manifest des stabilen Ciphertext-Verzeichnisses erzeugen (Update)
│
├─ robocopy in das lokale OneDrive- oder Google-Drive-Sync-Verzeichnis
│
├─ Warten, bis der Cloud-Client die Synchronisierung als abgeschlossen meldet
│
└─ Nur-lesender Abgleich des Ziel-Ciphertext-Verzeichnisses (Verify)
├─ MISSING: Quelle hat sie, Ziel nicht
├─ CHANGED: gleicher Pfad, geänderter Datei-Fingerprint
├─ EXTRA: Ziel hat mehr Dateien, Konflikt-/Temp-Dateien prüfen
└─ Treffer: lokales Sync-Verzeichnis stimmt mit Manifest überein
Mein damaliges VaultIntegrityCheck.ps1 trennte Update und Verify: Nur bei einer bestätigten korrekten E:-Ciphertext-Quelle durfte die Basis aktualisiert werden; ein Zielvergleich veränderte sie nicht. Das sind historische Beispielaufrufe dieses privaten Skripts. Der native PowerShell-Vergleich weiter unten funktioniert ohne es, führt aber kein dauerhaftes Basismanifest:
# 只在确认 E: 源 vault 停止写入并且内容正确时更新基准
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
-VaultPath "E:\BackupVault" `
-ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Update
# 等 OneDrive 上传完成,再对目标密文目录做只读校验
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
-VaultPath "C:\CloudSync\OneDrive\BackupVault" `
-ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Verify
Die Google-Drive-Variante behielt zwischen den beiden Schritten einen robocopy-Kopierbefehl mit den Parametern /E /R:2 /W:2 /MT:8 /FFT /Z /NP. Das neutrale, übernehmbare Grundgerüst sieht so aus:
robocopy "E:\BackupVault" "G:\MyDrive\BackupVault" /E /R:2 /W:2 /MT:8 /FFT /Z /NP
if ($LASTEXITCODE -ge 8) { throw "Robocopy failed with code $LASTEXITCODE" }
Die Exit-Code-Dokumentation von Microsoft zu Robocopy bestätigt: Erst Rückgabecodes ab 8 bedeuten mindestens einen fehlgeschlagenen Kopiervorgang. Aber 0–7 heißt nur, dass in der Kopierphase kein solcher Fehler gemeldet wurde – daraus darf man keinesfalls „alle Datei-SHA-256 sind bestanden” machen. /E schließt Unterverzeichnisse ein, löscht aber keine überzähligen Zieldateien; und /MIR, das Zieldateien löschen würde, käme mir auf der einzigen Cloud-Kopie nicht leichtfertig in die Befehlszeile.
Die historischen Notizen kannten als Prüfergebnis die Typen [OK] Vault matches the manifest exactly., MISSING, CHANGED, EXTRA. Bei MISSING oder CHANGED gilt: erst prüfen, ob der Upload fertig ist und keine Datei mehr beschrieben wird – niemals Update auf das Cloud-Verzeichnis loslassen, um die Anomalie als neue Basis zu übermalen. Die Google-Drive-Notizen vermerkten auch .tmp.driveupload-Temporärdateien; das kann ein Sync-Zwischenzustand sein, also nicht pauschal ignorieren – nach Abschluss der Synchronisierung erneut verifizieren ist die sicherere Variante.
Wer dieses private Skript nicht hat, kann zumindest mit dem Windows-eigenen Get-FileHash den SHA-256 einzelner Dateien prüfen. Es ist ein Standard-Cmdlet von PowerShell; Algorithmus und -LiteralPath-Parameter stehen in der offiziellen Microsoft-Dokumentation:
Get-FileHash -Algorithm SHA256 -LiteralPath "E:\BackupVault\vault.cryptomator"
Get-FileHash -Algorithm SHA256 -LiteralPath "C:\CloudSync\OneDrive\BackupVault\vault.cryptomator"
Aber eine einzelne Konfigurationsdatei zu prüfen reicht bei Weitem nicht. Ein Manifest des gesamten Vaults muss mindestens relative Pfade, Dateigrößen und den SHA-256 jeder Ciphertext-Datei abdecken und am Ziel Überzahl, Unterzahl sowie gleichnamige Änderungen erkennen; dazu braucht es ein klar datiertes Quellen-Basisdatum – man vergleicht nicht, während auf beiden Seiten noch geschrieben wird. Ohne fertiges Prüfwerkzeug tut es auch ein generisches, nur-lesendes PowerShell-Skript: Es listet rekursiv relative Pfade, Bytezahlen und SHA-256 von Quelle und Ziel auf und findet Unterschiede mit Compare-Object:
function Get-Inventory([string]$root) {
$base = (Resolve-Path -LiteralPath $root -ErrorAction Stop).ProviderPath.TrimEnd([char]'\')
Get-ChildItem -LiteralPath $base -File -Recurse -Force -ErrorAction Stop |
ForEach-Object {
[pscustomobject]@{
Path = $_.FullName.Substring($base.Length).TrimStart([char]'\')
Bytes = $_.Length
SHA256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
}
}
}
$original = @(Get-Inventory "E:\BackupVault")
$replica = @(Get-Inventory "C:\CloudSync\OneDrive\BackupVault")
if (!$original.Count -or !$replica.Count) { throw "Empty or unavailable vault" }
$diff = Compare-Object $original $replica -Property Path,Bytes,SHA256
if ($diff) {
$diff | Format-Table -AutoSize
throw "Local encrypted vaults differ"
}
Write-Host "[OK] Local encrypted vault file hashes match"
Dieses neue Beispiel deckt nur Dateien ab, keine leeren Verzeichnisse und keinen serverseitigen Cloud-Status, und es hat nicht die persistente Basis-Fähigkeit des historischen Update/Verify. Für echte Cloud-Prüfung fehlt noch ein Schritt: eine bestimmte Version des Cloud-Archivs an einen Ort außerhalb des Sync-Verzeichnisses erneut herunterladen und den Hash der heruntergeladenen Datei gegen den vertrauenswürdigen, vor dem Upload festgehaltenen Wert prüfen. „Sync-Symbol zeigt fertig + lokales Verzeichnis stimmt” allein beweist nicht, dass die entfernten Dateien auch wirklich abrufbar sind.
Für die Offline-Festplatte setzte ich damals auf verschlüsselte 7z-Vollarchive statt auf einen dauerhaft heißen Mirror. -mhe=on verschlüsselt auch die Dateinamen innerhalb des 7z-Formats, -p steht für die Passwortnutzung; das Passwort gehört nicht im Klartext in geteilte Skripte, Befehlsverläufe oder Logs. Die offizielle 7z-Format-Dokumentation von 7-Zip bestätigt AES-256-Unterstützung. Beim Erstellen eines Archivs empfehle ich, Passwort und „Dateinamen verschlüsseln” in der GUI zu bestätigen; hier bleibt das CLI-Parameterbeispiel erhalten – vorher mit lokalem 7z -h Version und Passworteingabeweg prüfen und das Passwort nicht direkt in Skripte oder Verlaufsdateien schreiben:
# 只在冷备仓库没有写入时,对其目录创建带口令的 7z 归档
7z a -t7z -p -mhe=on "git-snapshot-YYYYMMDD.7z" "E:\GitCold\project.git"
# 测试归档是否能读完:输入口令后检查退出码
7z t "git-snapshot-YYYYMMDD.7z"
Wird dieses Archiv etwa aus der Cloud nach D:\Downloads erneut heruntergeladen, lässt sich die Byte-Übereinstimmung vor und nach dem Upload so prüfen:
$before = (Get-FileHash "D:\GitArchive\git-snapshot-YYYYMMDD.7z" -Algorithm SHA256).Hash
$after = (Get-FileHash "D:\Downloads\git-snapshot-YYYYMMDD.7z" -Algorithm SHA256).Hash
if ($before -ne $after) { throw "Downloaded archive SHA-256 mismatch" }
Ein übereinstimmender Hash besagt nur, dass diese beiden Archive byte-identisch sind – nicht, dass die Basis unmanipuliert ist, und er ersetzt weder 7z t, Entpacken, git fsck, den Refs-Abgleich noch einen echten Clone. Das Basis-Manifest bewahrt man am besten separat auf und schützt es davor, demselben Ausfall zum Opfer zu fallen wie die Daten; die externe Festplatte wird nach dem Backup wirklich getrennt. Wer „Ciphertext korrekt übertragen” und „Code-Historie wiederherstellbar” als getrennte Prüfungen behandelt, verwechselt im Ernstfall kein einzelnes [OK] mit einer Allzweck-Versicherung.
Was noch fehlt: verschlüsselten Git-Verlauf per Push übertragen
Bisher gab es zwei getrennte Wege: Windows und Mac nutzten git push und git pull gegen den lokalen unverschlüsselten Bare-Central. Parallel kam eine Mirror-Kopie in einen Cryptomator-Vault und anschließend als verschlüsselte Dateiablage in die Cloud. Auch häufige Vault-Synchronisierung macht daraus keinen Git-Remote, dessen Branches, Diffs und PRs GitHub lesen kann. Der alte Aufbau hatte keine nachgewiesenen Echtzeit-Pushes verschlüsselter Git-Repositories.
Gewünscht ist, nach einem Commit möglichst zeitnah einen verschlüsselten Stand des gesamten Repository-Verlaufs extern abzulegen. Dazu muss die Verschlüsselung vor dem Transfer erfolgen. Der Schlüssel muss getrennt wiederherstellbar sein; konkurrierende Pushes, abgebrochene Transfers, versehentliches Überschreiben und eine echte Wiederherstellung müssen geprüft werden. Nicht commitete Arbeitsdateien sind davon weiterhin ausgeschlossen.
Ein Kandidat ist git-remote-gcrypt, das Git-Remotes mit GnuPG verschlüsselt. Laut Projekt-README muss das Backend über eine gewöhnliche Git-URL bei jedem Push die ganze Historie erneut übertragen. Außerdem sind gefährliche Force-Push-Semantiken dokumentiert. Das rsync-Backend kann inkrementell arbeiten, entspricht aber nicht dem normalen GitHub-Private-Workflow. Ich habe diese Lösung nicht auf meinem kommerziellen Repository produktiv abgenommen.
git-crypt ist auch kein Ersatz für die Ganzrepo-Verschlüsselung: Dateinamen, Commit-Messages und weitere Git-Metadaten bleiben sichtbar. Häufige automatische verschlüsselte Offsite-Backups sind ein anderer vernünftiger Weg, liefern aber keine lesbaren GitHub-Diffs oder Cloud-Agenten, die direkt mit Chiffretext entwickeln können.
Git-Disaster-Recovery: Aus welcher Kopie Code und Historie wiederherstellen
Ursprünglich hatte ich mir zwei Wiederherstellungsrouten bereitgelegt. Im Ernstfall prüfe ich zuerst, welches vorhandene Repository die Ziel-Refs noch hält, statt sofort ein leeres Projekt neu aufzubauen:
Git-Projekt muss wiederhergestellt werden
├─ Entwicklungsrechner hat noch vollständige Commits / Branches / Tags
│ └─ Leere Bare-Central neu anlegen → Refs prüfen → lokale Branches und Tags pushen
├─ Entwicklungsrechner unvollständig, aber E:-Mirror noch lesbar
│ └─ fsck → Refs sichten → Arbeitsbaum clonen, Historie abgleichen
└─ Keine Online-Kopie verfügbar
└─ Offline- oder Offsite-Historienarchiv suchen → SHA-256 prüfen → 7z-Test und Entpacken
→ git fsck → Refs-Abgleich → clone
Diese Prüfung erinnert auch an einen wichtigen Punkt: git push --all pusht nur lokale Branches. Nur weil der aktuelle Arbeitsbaum startet, stehen nicht automatisch alle Branches aller früheren Entwicklungsrechner in dieser einen Kopie.
Fall eins: Die Central ist weg, aber ein vollständiges Entwicklungs-Repository existiert noch. Dann legt man ein neues Bare-Repo an und pusht die benötigten Branches und Tags vom Entwicklungsrechner zurück:
# 在新 Central 的可写位置创建空裸仓库
git init --bare "/path/to/recovered/project.git"
# 在幸存的开发仓库内执行
git remote add recovered "/path/to/recovered/project.git"
git push recovered --all
git push recovered --tags
Hier muss man klar sagen: --all pusht die lokalen Branches dieses Rechners, nicht alle refs/remotes/*. Hatte ein anderer Rechner eigene Branches, die dieser hier nie als lokale Branches gespeichert hat, kann man nicht behaupten, git push --all hole sie wie von Zauberhand zurück. Vor dem offiziellen Wechsel der Central gehört ein Abgleich gegen das Refs-Manifest der überlebenden Repositories – bei Bedarf aus anderen überlebenden Kopien ergänzen.
Der extremere Fall: Weder der lokale Entwicklungsrechner noch die Central sind verfügbar. Dann beginnt die Wiederherstellung bei einem Archiv auf der externen Festplatte oder in der Cloud. Die Reihenfolge: ein klar datiertes Backup nehmen, entschlüsseln und entpacken, die Objekte des Bare-Repos prüfen, die erwarteten Refs abgleichen und erst dann einen Arbeitsbaum clonen. Das frisch entpackte Backup nicht gleich überschreiben – und schon gar nicht als Erstes einen Mirror-Push mit --force oder --prune ausführen.
# 在解压得到的裸仓库上执行
git --git-dir="/restore/project.git" fsck --full
git --git-dir="/restore/project.git" for-each-ref \
--format='%(refname) %(objectname)' refs/heads refs/tags
# 确认仓库可读后,再建工作区
git clone "/restore/project.git" recovered-worktree
git fsck prüft Konnektivität und Gültigkeit der Git-Objekte; git for-each-ref listet die Objekt-IDs zu Branches und Tags auf. Zusammen beantworten sie mindestens „Ist das Paket beschädigt?” und „Sind die erwarteten Branches und Tags da?”. Um echte Wiederherstellbarkeit zu belegen, muss man zusätzlich Dateiinhalte, Abhängigkeiten und externe Daten prüfen – und ob sich auf einer neuen Maschine weiterbauen und -arbeiten lässt.
Beim späteren Aufräumen von verify-coldbackup.ps1 habe ich die Prüfreihenfolge als Kette festgeschrieben: 7z-Integritätstest → Entschlüsseln und Entpacken → Arbeitsbaum clonen → git fsck auf die Objekte → SHA-Abgleich aller Branches und Tags. „Das Archiv lässt sich öffnen” allein reicht bei Weitem nicht; um sicherzugehen, dass der Code wirklich gesichert ist, muss man den Restore einmal vollständig in einem separaten Verzeichnis durchspielen.
Nebenbei: Die andere Route – „Upstream-Open-Source-Projekte in ein eigenes privates GitHub-Repo spiegeln” – ist ein anderes Thema als diese LAN-Central. Dort geht es um GitHubs eingeschränkte Refs, Branch-Überschreibungen und das Löschrisiko von --force --prune am Ziel; eigene Entwicklungsbranches gehören nicht ins Mirror-Ziel. Die konkreten Mirror-Fragen habe ich separat im dem automatischen Spiegeln öffentlicher GitHub-Repositories in private Backups behandelt, das wird hier nicht wiederholt. Die offizielle git push-Dokumentation erklärt die Unterschiede zwischen --mirror, --prune und Refspecs ebenfalls sehr klar.
Die Hürde bei der Arbeit vom Smartphone aus
Solange ich vor Windows oder Mac saß, war das System praktikabel. Beide Rechner besitzen lokale Git-Arbeitskopien. Wenn ich jedoch unterwegs vom Handy aus ChatGPT/Codex nach einem Fix oder einem Test fragen möchte, ist Windows als einziger Central ein Verfügbarkeitsproblem: Ist der Rechner ausgeschaltet oder SMB nicht erreichbar, stockt die Arbeit.
Partial Clone und Sparse Checkout entlasten den Mac-Speicher, beantworten aber nicht die Frage, wer entschlüsseln darf. Cloudbasierte Coding-Agenten müssen den Quellcode in der Regel lesen, durchsuchen und ausführen. Besteht der Cloud-Remote nur aus Chiffretext, sind GitHub-Diffs, Suche, PRs und Agenten ohne Klartext nicht nutzbar. Würde ich den Code für den Agenten in einer nicht vertrauenswürdigen Cloud-Umgebung entschlüsseln, wäre mein ursprüngliches Ziel verfehlt.
Ein besser passender Ansatz ist deshalb, das Smartphone als Fernbedienung zu verwenden, nicht als Speicherort für den Code. Eine vertrauenswürdige, dauerhaft erreichbare Mac-/Linux-Maschine könnte über ein abgesichertes privates Netz und SSH Git und Entwicklungstools ausführen. Das ist eine noch nicht produktiv verifizierte Architekturidee. Verfügbarkeit, starke Authentisierung, minimale Rechte und Schlüsselverwaltung bleiben nötig. Auch wenn ein lokaler Agent Quellcode an ein externes KI-Modell sendet, ist das eine separate Klartext-Datenübertragung und muss geprüft werden.
Ein verschlüsselter Git-Remote verbessert möglicherweise die Speicherung der Historie, macht sie für Cloud-Agenten aber nicht automatisch lesbar. Ich muss klären: Wer entschlüsselt, wo wird Klartext ausgeführt, und was passiert nach einem Verbindungsabbruch des Telefons? Bis dahin ist „GitHub Private wird der einzige Central“ für dieses Bedrohungsmodell keine fertige Lösung.
Häufige Fragen zu Git-Bare-Repos, SMB und Backup-Wiederherstellung
Warum nicht einfach einen normalen Git-Arbeitsbaum per SMB freigeben?
Git verweigert standardmäßig Pushes auf den ausgecheckten Branch eines Nicht-Bare-Repos (receive.denyCurrentBranch). Ein Bare-Repo hat keinen Arbeitsbaum und eignet sich als Central, während Win/Mac eigene Arbeitskopien behalten.
Was zuerst prüfen, wenn git pull nach dem SMB-Mount auf dem Mac fehlschlägt?
Zuerst ls /Volumes, git remote -v und SMB-Rechte prüfen. Eine .lock-Datei nicht löschen, bevor sicher keine Git-Prozesse mehr laufen; Locks im Arbeits- und Bare-Repo können sich unterscheiden. Für häufigen Netzwerkzugriff ist Git over SSH eine Alternative.
Ist git clone --mirror sicherer als ein normaler git clone?
--mirror synchronisiert Refs einschließlich späterer Löschungen und ersetzt kein unveränderliches Backup. Gegen versehentliches Löschen helfen datierte Archive, die nicht mit dem Ursprung synchronisiert werden.
OneDrive / Google Drive zeigt „Synchronisiert” – gilt das als bestandene Hash-Prüfung?
Nein. Client-Status, lokaler Ciphertext-Manifestvergleich, SHA-256 einer neu heruntergeladenen Cloud-Kopie und tatsächliche Entschlüsselung mit git fsck, Refs-Prüfung und Clone sind getrennte Schritte. Das frühere [OK] galt nur für die lokal geprüfte Kopie und ihre Basis.
Warum nicht GitHub Private? Wie oft sollte man eine Wiederherstellung testen?
GitHub Private schützt Zugriffe, erfüllt aber nicht meine Grenze gegen Klartext bei Cloud-Speicherung. Nach dem Einrichten oder Ändern der Verschlüsselungskette einmal unabhängig mit Schlüssel, Git-Objekten, Refs und Clone wiederherstellen; ohne Schlüssel ist Chiffretext unbrauchbar.
Referenzen:Git init · Git clone · Git remote · Git push · Git fsck · Robocopy · Get-FileHash · Cryptomator-Sicherheitsziel · offizielles 7-Zip-7z-Format