Cuando monté mi flujo de Git entre Windows y Mac, la prioridad no era ahorrar dinero ni tener una sincronización más rápida. Había una condición desde el principio para un proyecto comercial que esperaba mantener durante años: el código fuente esencial no debía almacenarse en claro en ninguna plataforma cloud de terceros, aunque el repositorio fuera privado en GitHub.

No estoy acusando a GitHub de apropiarse de código privado. Es una decisión sobre confianza: no quería que el valor de años de trabajo dependiera de que nunca hubiera un acceso interno indebido, una cuenta comprometida, un fallo de seguridad o un ataque a la cadena de suministro. Un repositorio Private limita los accesos autorizados, pero no impide técnicamente que la plataforma procese los objetos Git originales. Mi objetivo era guardar las claves de descifrado y dejar en la nube solamente datos cifrados.

Por eso hice que un bare repo en Windows fuera el Central local, con el Mac conectado mediante SMB. Después añadí un mirror, archivos 7z cifrados sin conexión y copias en OneDrive / Google Drive bajo Cryptomator. Pero sincronizar copias cifradas en la nube no equivale a disponer de un remote Git cifrado para push y pull. Esa diferencia se volvió especialmente incómoda cuando empecé a querer colaborar con ChatGPT/Codex desde el teléfono.

Conservo los comandos, las notas de 2024–2026 y el experimento reproducible con Git 2.47.3. Esas pruebas describen refs, mirror y restauración; no prueban que haya implementado ya un remote Git cifrado en tiempo real ni un entorno de desarrollo móvil.

Por qué GitHub Private no resolvía mi requisito original

Hablo de un modelo de amenazas, no de una acusación contra un proveedor. Los términos de GitHub para repositorios privados incluyen compromisos de confidencialidad y excepciones de acceso por parte del servicio, pero eso no es lo mismo que cifrar todo el repositorio en el cliente y conservar las claves fuera del proveedor. Para muchos proyectos, Private basta; para mi código comercial más valioso, decidí imponer otra frontera.

ArquitecturaQué puede ver el proveedorLímite
GitHub Private convencionalObjetos Git, código y commits normales bajo controles de accesoIncumple mi regla de no subir código legible
Git local + Cryptomator en la nubeArchivos cifrados y algunos metadatos de tamaño y tiempoÚtil como backup remoto, no como colaboración Git nativa
Cifrar todo el remote Git antes de transmitirEl objetivo es que el servidor guarde solo texto cifradoCandidato para push/pull aún por validar
Archivos 7z cifrados sin conexiónInstantáneas históricas fuera de la sincronizaciónProtegen ante accidentes, no aportan colaboración inmediata

El cifrado en cliente no hace milagros: malware en el equipo, claves robadas o contexto de código descifrado enviado a una IA pueden romper el aislamiento. Cryptomator documenta ese alcance y los metadatos que siguen expuestos.

Arquitectura Git que evita almacenar código comercial en claro en la nube: Windows y Mac usan un bare Central local, con mirror hacia Cryptomator para copia cifrada en cloud y archivo 7z offline independiente.

Esquema: Git opera en equipos confiables; la nube solo conserva backups cifrados en el cliente, y los archivos offline retienen historia. El remote Git cifrado y el desarrollo móvil son propuestas aún no verificadas.

Sincronización Git en LAN Win/Mac: por qué usar un bare repo como Central

Primero, cómo se relacionaban en ejecución:

Repositorio de desarrollo en Windows ─── push / pull ──┐
                                                      ├── Central en Windows (bare repo)
Repositorio de desarrollo en Mac ─ SMB ─ push / pull ──┘              │
                                                                     │ actualización del mirror
                                                                     ▼
                                                        Mirror bare en el disco E
                                                                     │
                                                     archivado cifrado / copia offline
                                                                     ▼
                                                      disco externo, nube remota

De las cuatro capas, solo las tres últimas cumplen realmente una función de backup. Si las ponemos sobre la mesa, queda claro cuál puede fallar a la vez que las demás:

Capa y ubicaciónAcción claveQué cubreQué no resuelve
Central de colaboración: bare repo en disco D de Windowspush, pull en Win/MacEl código ya pusheado se puede recuperar aunque una máquina de desarrollo se estropeeWindows apagado, SMB caído, el propio Central dañado; no cuenta como backup en frío independiente
Mirror en la misma máquina: mirror en disco E de Windowsclone --mirror, remote updateRecuperar desde el mirror más reciente si el directorio del Central se dañaFallo de toda la máquina (D y E caen juntos); --prune puede borrar ramas sincronizadas
Archivo offline: disco externo desconectadoPaquete cifrado 7z, 7z tFallo del host, ransomware que afecte a las copias onlineLa actualización es manual; la versión más reciente depende de la fecha del archivo
Cifrado remoto: Cryptomator + OneDrive / Google Driverobocopy, verificación de manifiesto cifrado, verificación de descargaPérdida total de los dispositivos locales, recuperación remotaLa sincronización en la nube también puede propagar errores; no incluye código sin commitear ni datos externos

El RPO (hasta qué versión puedes perder como máximo) lo decide la hora del último commit, sincronización o archivado correctos de cada capa; el RTO (cuánto tardas en recuperar) depende además de la velocidad del medio, del descifrado y de los simulacros. Nunca registré de forma específica el tiempo real de recuperación, así que aquí solo comparo rutas de recuperación, sin disfrazar minutos supuestos de RTO/RPO. D y E están en el mismo Windows: dos letras de unidad no eliminan el riesgo común de que falle toda la máquina.

Lo clave del Central no es “tener una carpeta compartida”, sino que dentro hay un bare repository. Tiene los objetos, las referencias y la configuración del repositorio Git, pero ningún working tree normal. Las dos máquinas de desarrollo tienen cada una su propio árbol de trabajo, los cambios se hacen en local y solo se pushean al Central después del commit.

Según la documentación oficial de git init, la forma básica de crear un bare repo es:

# Ruta de ejemplo, no era el directorio de mi máquina original
git init --bare "D:/git-central/project.git"

# Configurar el Central en un repositorio de trabajo de Windows ya existente
git remote add central "D:/git-central/project.git"
git push -u central master

En el Mac primero se monta el recurso SMB en local y luego se pone la ruta montada como remote central. El master de abajo era el nombre de rama que usaba el repositorio en aquella época; si tu rama por defecto es main, no copies master tal cual.

git remote add central "/Volumes/git-central/project.git"
git fetch central
git pull --ff-only central master

En el manual de operaciones antiguo quedaban unas líneas muy concretas de diagnóstico en el Mac, que ahorraban la duda de “¿es que Git no se actualizó, o es que el SMB ni siquiera está montado?”:

# Montar en el Mac el recurso SMB de Windows (dirección de ejemplo)
open "smb://<windows-host>/<share-name>"

# Primero confirmar dónde está montado realmente el recurso
ls /Volumes
ls "/Volumes/<share-name>"

# Luego, desde el workspace del proyecto, confirmar a dónde apunta el remote de Git y en qué rama estás
git remote -v
git branch --show-current
git status --short

Las notas originales decían: primero comprobar que en el directorio compartido se ve de verdad el bare repo, y después operar el remote central; no hacer reset --hard a la primera que git pull da error. En Windows se seguía el mismo orden: mirar git status, commitear y luego git push central master. Estos comandos vienen de las notas de operación de entonces; en el artículo los nombres de host internos y las rutas del recurso se han sustituido por marcadores de ejemplo.

En la rutina diaria de entonces, antes de desarrollar hacía git pull central master, al terminar miraba git status, luego git add ., git commit, git push central master. La otra máquina tiraba de la misma entrada. El --ff-only del ejemplo es una protección que hoy prefiero conservar: si hay divergencia, se para y se resuelve, en vez de dejar que un pull normal genere silenciosamente un commit de merge.

Este esquema tenía una ventaja muy tangible: si el Central se rompía, el historial Git de las dos máquinas de desarrollo seguía ahí; si se rompía una máquina de desarrollo, el historial no desaparecía con ella. Siempre tomé “un repositorio completo puede servir de semilla para reconstruir” como principio de diseño. Pero esa frase tiene condiciones: ese repositorio tiene que conservar de verdad las ramas, etiquetas y objetos que necesitas. Un clone de una sola rama, un shallow clone, o archivos sin commitear dejados en el árbol de trabajo, no cuentan automáticamente como backup completo.

Otro límite fácil de pasar por alto: acceder al bare repo directamente por SMB depende en la práctica de los permisos del sistema de archivos compartido, los bloqueos y la estabilidad de la red. El esquema antiguo cubría mi costumbre de dos máquinas, pero eso no lo hace apto para varias regiones, varios usuarios o redes inestables. Para un servicio Git nuevo y duradero, yo preferiría hoy un transporte remoto como Git over SSH antes que tratar un disco compartido como servidor Git genérico.

Mirror de Git en frío: la sincronización de borrados y la instantánea histórica son dos cosas distintas

Lo que de verdad marcó distancia de seguridad fueron las capas detrás del Central. En el disco E original había un bare repo clonado con --mirror, que no participaba en el desarrollo y solo se actualizaba; el disco externo guardaba archivos cifrados, normalmente offline; y la nube guardaba otra copia remota del contenido cifrado. No eran cuatro git clone idénticos, sino respuestas a distintos alcances de fallo.

Merece la pena leer con atención la definición oficial de git clone --mirror: el mirror no solo incluye las ramas, sino que sincroniza otras referencias según el refspec de espejo. A diferencia de un clone normal de desarrollo, un git remote update posterior puede sobrescribir las refs correspondientes. La creación inicial y la actualización diaria podrían escribirse así:

# Ejecutar solo una vez, al inicializar el repositorio en frío
git clone --mirror "D:/git-central/project.git" "E:/git-cold/project.git"

# Actualizaciones posteriores del mirror. --prune puede eliminar referencias que ya no existen en el origen
git --git-dir="E:/git-cold/project.git" remote update --prune

# Solo comprueba los objetos de Git; no equivale a haber hecho un simulacro de recuperación
git --git-dir="E:/git-cold/project.git" fsck --full

El manual de operación del E en frío de entonces, además de git remote update, dejaba escrita una forma de confirmar el commit actual:

# Ejecutar dentro del bare repo del mirror; este ejemplo mantiene la histórica rama master
git remote update
git log origin/master -1 --oneline

# No basta con comparar el último log; una recuperación formal exige cotejar refs y objetos
git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags

Ese origin/master es la escritura de las notas históricas; si un bare mirror permite acceder a la referencia con ese nombre depende de la configuración real y del resultado de git show-ref. La práctica genérica más fiable es mirar directamente refs/heads/master y las demás refs que haya que conservar.

Con Git 2.47.3 creé un repositorio temporal y recorrí la cadena de borrado de --prune: pusheé la rama test-prune, sincronicé el mirror, luego borré esa rama del Central y actualicé el mirror. Las líneas clave de la salida son solo estas:

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

Fíjate: git fsck devuelve 0 y aún encuentra el dangling commit, pero el mirror ya no tiene la referencia de la rama test-prune. El objeto huérfano podría desaparecer en una recolección de basura posterior. El mirror no es una “máquina del tiempo”: sincroniza el estado más reciente, y también sincroniza los borrados. Este experimento verifica el comportamiento de las referencias de Git; la fiabilidad del 7z y de la nube sigue necesitando simulacros de restauración con archivos reales.

Según la documentación de git remote, remote update hace un fetch; --prune limpia según la configuración las referencias remotas que ya han caducado. Cada actualización normalmente solo transfiere los objetos nuevos necesarios, sin tener que volver a copiar todo el repositorio. Pero un mirror es una copia sincronizada, no una instantánea inmutable. Si en el origen se borra una rama por error, la siguiente sincronización con prune puede propagar ese borrado al mirror. Es ideal para recuperar rápido el estado actual, pero no puede cargar él solo con toda la responsabilidad de “anti-borrado y anti-ransomware”.

Por eso entonces mantuve dos ramales de backup distintos: uno empaquetaba el repositorio en frío en un archivo 7z cifrado con fecha y lo dejaba en un disco externo normalmente desconectado; el otro guardaba el mirror de Git dentro del vault cifrado de Cryptomator y copiaba el directorio cifrado del vault al directorio de sincronización de OneDrive / Google Drive. Podían proteger el mismo historial Git, pero no se puede dibujar sin más como “todas las copias en la nube pasan primero por 7z y luego por Cryptomator”. Lo que de verdad tiene valor es que un archivo histórico concreto no sea sobrescrito en la siguiente actualización del Central. La contraseña de cifrado no puede guardarse junto al archivo, y que la copia salga bien tampoco equivale a una recuperación exitosa.

El Central en D y el mirror en E, en el mismo Windows, amortiguan un mal uso del repositorio o el daño de un solo volumen, pero no paran un fallo de toda la máquina, un robo o un ransomware en la misma máquina. El disco externo solo tiene sentido de aislamiento offline si se desconecta de verdad cuando termina el backup; el almacenamiento en la nube, si sincroniza un borrado o una corrupción, tampoco tiene por qué conservar una versión antigua utilizable. Esa diferencia fue lo que me hizo dejar de conformarme con “ya tengo varias copias”.

Y un matiz de alcance: Git guarda los objetos y referencias Git ya commiteados, pero no te respalda automáticamente los datos grandes excluidos por .gitignore, los cambios sin commitear del working tree ni las bases de datos externas. En proyectos con Git LFS o submódulos hay que confirmar por separado si esos objetos y dependencias están respaldados. Que el mirror del repositorio esté completo no equivale a que todo el entorno de desarrollo lo esté. Datos en ejecución como los volúmenes de Docker necesitan otro esquema de backup; de eso ya escribí por separado en la práctica de disaster recovery con backup de volúmenes Docker a GitHub.

Cryptomator + nube: cómo verificar con SHA-256 el backup cifrado

En la capa de la nube lo más engorroso no era subir, sino saber después de subir que no faltaba ningún archivo. Entonces dejé dos notas de operación específicas para Cryptomator + OneDrive y Cryptomator + Google Drive, con el manifiesto de referencia cifrado, el orden de llamada de VaultIntegrityCheck.ps1 y cómo localizar MISSING, CHANGED y EXTRA tras la copia.

Cryptomator se encarga del cifrado de contenido y nombres en el cliente, el cliente de la nube se encarga de la transferencia: no son dos funciones del mismo software. La nota de seguridad oficial de Cryptomator también subraya que el cifrado no sustituye al backup. El flujo antiguo real se parecía más a esto:

E: mirror en frío / vault cifrado de Cryptomator (referencia)
     │
     ├─ Generar el manifiesto de huellas del directorio cifrado estable (Update)
     │
     ├─ robocopy al directorio local de sincronización de OneDrive o Google Drive
     │
     ├─ Esperar a que el cliente de la nube muestre la sincronización completa
     │
     └─ Comparación de solo lectura del directorio cifrado de destino (Verify)
          ├─ MISSING: existe en el origen, falta en el destino
          ├─ CHANGED: misma ruta, pero la huella del archivo cambió
          ├─ EXTRA: archivos de más en el destino, hay que distinguir conflictos o temporales
          └─ Coincide: el directorio local sincronizado corresponde al manifiesto

Mi propio VaultIntegrityCheck.ps1 distinguía Update y Verify: solo se actualizaba la referencia sobre la copia maestra cifrada E: confirmada como correcta; la verificación del destino nunca cambiaba esa referencia. Estos son ejemplos de las llamadas históricas. La comparación nativa de PowerShell que sigue funciona sin el script, pero no guarda un manifiesto de referencia persistente:

# Actualizar la referencia solo cuando el vault de origen en E: ha dejado de escribirse y su contenido es correcto
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
  -VaultPath "E:\BackupVault" `
  -ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Update

# Cuando OneDrive termine de subir, verificación de solo lectura del directorio cifrado de destino
powershell -NoProfile -File "D:\BackupTools\VaultIntegrityCheck.ps1" `
  -VaultPath "C:\CloudSync\OneDrive\BackupVault" `
  -ManifestPath "D:\BackupTools\vault-manifest.json" -Mode Verify

La versión de Google Drive conservaba además un comando robocopy de copia entre los dos pasos, con los parámetros /E /R:2 /W:2 /MT:8 /FFT /Z /NP. Con directorios neutros, el esqueleto reutilizable es:

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" }

La documentación de Microsoft sobre los códigos de salida de Robocopy confirma: solo un código 8 o superior significa que al menos una copia falló. Pero que sea 0–7 solo indica que la fase de copia no reportó ese fallo; jamás debe escribirse como “todos los SHA-256 de los archivos pasaron”. /E incluye subdirectorios, pero no se encarga de borrar archivos sobrantes que ya existan en el destino; yo no cambiaría sin más a /MIR, que sí borra en el destino, teniendo una única copia en la nube.

La retroalimentación de verificación que dejaron las notas históricas incluía tipos como [OK] Vault matches the manifest exactly., MISSING, CHANGED o EXTRA. Ante un MISSING o un CHANGED, primero hay que comprobar si la subida terminó y si el archivo se sigue escribiendo; no ejecutes Update sobre el directorio de la nube para tapar la anomalía con una referencia nueva. Las notas de Google Drive también registraban archivos temporales .tmp.driveupload; pueden ser un estado intermedio de sincronización, no se deben ignorar sin condiciones: lo prudente es volver a verificar cuando la sincronización termine.

Si el lector no tiene ese script privado, al menos puede usar Get-FileHash, incluido en Windows, para comprobar el SHA-256 de un archivo suelto. Es un cmdlet estándar de PowerShell; el algoritmo y el parámetro -LiteralPath están en la documentación oficial de Microsoft:

Get-FileHash -Algorithm SHA256 -LiteralPath "E:\BackupVault\vault.cryptomator"
Get-FileHash -Algorithm SHA256 -LiteralPath "C:\CloudSync\OneDrive\BackupVault\vault.cryptomator"

Pero comprobar un solo archivo de configuración no basta ni de lejos. El manifiesto de todo el vault debería cubrir como mínimo la ruta relativa, el tamaño y el SHA-256 de cada archivo cifrado, además de detectar en el destino archivos de más, de menos o cambiados con el mismo nombre; y tiene que tener una fecha de referencia clara, sin comparar mientras un extremo sigue escribiendo. Sin una herramienta de verificación hecha, también sirve un script PowerShell genérico de solo lectura: listar recursivamente la ruta relativa, los bytes y el SHA-256 de los archivos de origen y destino, y encontrar las diferencias con 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"

Este ejemplo nuevo solo cubre archivos, no directorios vacíos ni el estado en el servidor de la nube, y no tiene la capacidad de referencia persistente del histórico Update/Verify. A la verificación real en la nube aún le falta un paso: volver a descargar a una ubicación ajena al directorio de sincronización una versión concreta del archivo cifrado en la nube, y comparar el hash del archivo descargado con el registro fiable guardado antes de subir. Que el icono de sincronización de la nube diga “completo” más un directorio local coincidente, por sí solos, no prueban que el archivo remoto se pueda recuperar.

Para el disco externo offline, entonces opté por el paquete cifrado completo de 7z en lugar de depender siempre del mirror caliente. El -mhe=on de 7z cifra los nombres de archivo dentro del formato 7z, -p indica usar contraseña; no pongas la contraseña en texto plano dentro de scripts compartidos, el historial de comandos o los logs. La especificación oficial del formato 7z de 7-Zip confirma el soporte de AES-256. Al crear el archivo, recomiendo confirmar la contraseña y el “cifrado de nombres de archivo” en la GUI; abajo dejo el ejemplo de parámetros CLI, pero antes de operar comprueba con 7z -h en tu máquina la versión y la forma de introducir la contraseña, y no escribas la contraseña directamente en scripts ni en el historial:

# Solo cuando el repositorio en frío no se está escribiendo: crear un 7z con contraseña de su directorio
7z a -t7z -p -mhe=on "git-snapshot-YYYYMMDD.7z" "E:\GitCold\project.git"

# Probar si el archivo se puede leer entero: tras introducir la contraseña, comprobar el código de salida
7z t "git-snapshot-YYYYMMDD.7z"

Por ejemplo, si ese archivo se vuelve a descargar de la nube a D:\Downloads, así se verifica que los bytes coinciden antes y después de subir:

$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" }

Que los hashes coincidan solo demuestra que esos dos archivos son byte a byte iguales; no prueba que la referencia no haya sido manipulada, ni sustituye a 7z t, la extracción, git fsck, la comparación de refs y el clone real. El manifiesto de referencia conviene guardarlo por separado y protegido contra la misma falla que lo podría alterar; el backup del disco externo hay que desconectarlo de verdad cuando termine. Solo separando “la transferencia cifrada fue correcta” de “el historial de código es recuperable” en comprobaciones distintas evitarás tomar un [OK] por un seguro todoterreno cuando llegue el problema.

La pieza pendiente: hacer push del repositorio como texto cifrado

El sistema original tenía dos caminos diferentes. Los repositorios de Windows y Mac hacían git push y git pull contra el Central local en claro. Por separado, el mirror se copiaba dentro de Cryptomator y los archivos cifrados del vault viajaban a OneDrive / Google Drive. Eso no convierte el espacio en la nube en un remote Git que comprenda ramas, diffs y pull requests. No debo llamar «push Git cifrado en tiempo real» a lo que solo era sincronización de un backup cifrado.

Me falta poder transferir poco después de cada commit una representación cifrada del historial completo, con las claves bajo mi control. Tengo que probar el cifrado antes del envío, recuperación independiente de claves, escrituras concurrentes, interrupciones, sobrescrituras no deseadas y restauración real. Tampoco serviría para archivos del directorio de trabajo que aún no estén confirmados en Git.

Una opción que merece pruebas es git-remote-gcrypt, que utiliza GnuPG para un remote cifrado. Su README explica que el backend con URL Git genérica vuelve a subir el historial completo en cada push y presenta riesgos con los push forzados. Un backend tipo rsync puede ser incremental, pero no se comporta como un repositorio Private habitual de GitHub. Aún no he validado esta arquitectura en un proyecto comercial real.

Tampoco bastaría git-crypt: cifra contenido de archivos elegidos, pero no oculta nombres, mensajes de commit y otros metadatos Git. Automatizar copias cifradas frecuentes es más sencillo, aunque seguiría sin permitir que la web de GitHub o un agente cloud analicen directamente un historial cifrado.

Recuperación ante desastres en Git: desde qué copia restaurar el código y el historial

Entonces me dejé dos rutas de recuperación. Ante un incidente, primero juzgaría qué copia conserva todavía las refs objetivo, en vez de reconstruir de inmediato un proyecto vacío:

Hay que recuperar un proyecto Git
  ├─ La máquina de desarrollo conserva commits / ramas / etiquetas completos
  │    └─ Crear un Central bare vacío → comprobar refs → pushear ramas locales y tags
  ├─ La máquina de desarrollo está incompleta, pero el mirror del disco E sigue legible
  │    └─ fsck → revisar refs → clonar un workspace y comparar el historial
  └─ Ninguna copia online disponible
       └─ Buscar un archivo histórico offline o remoto → verificar SHA-256 → probar y extraer el 7z
                                     → git fsck → contrastar refs → clone

Esta decisión también me recordó algo: git push --all solo pushea las ramas locales. Que el workspace actual arranque no significa que todas las ramas de todas las máquinas de desarrollo estén en esa copia.

Una ruta es: se perdió el Central, pero queda un repositorio de desarrollo completo. Entonces se puede crear de nuevo un bare repo vacío y devolverle desde la máquina de desarrollo las ramas y etiquetas necesarias:

# Crear un bare repo vacío en una ubicación escribible del nuevo Central
git init --bare "/path/to/recovered/project.git"

# Ejecutar dentro del repositorio de desarrollo superviviente
git remote add recovered "/path/to/recovered/project.git"
git push recovered --all
git push recovered --tags

Aquí hay que decirlo claro: --all pushea las ramas locales de esta máquina de desarrollo, no todos los refs/remotes/*. Si otra máquina tuvo una vez una rama independiente y esta máquina no la guardó como rama local, no se puede pretender que git push --all la recupere de la nada. Antes de cambiar formalmente el Central, hay que contrastar el manifiesto de refs de los repositorios supervivientes y, si hace falta, completarlo desde otras copias supervivientes.

La otra, más extrema: ni la máquina de desarrollo ni el Central están disponibles. La entrada de recuperación pasa a ser el archivo del disco externo o de la nube. El orden debería ser: tomar un backup con fecha clara, completar el descifrado y la extracción, comprobar los objetos del bare repo, cotejar las refs esperadas y por último clonar el workspace. No sobrescribas todavía el backup recién extraído, y no ejecutes como primer paso un push de mirror con --force o --prune.

# Ejecutar sobre el bare repo obtenido de la extracción
git --git-dir="/restore/project.git" fsck --full
git --git-dir="/restore/project.git" for-each-ref \
  --format='%(refname) %(objectname)' refs/heads refs/tags

# Cuando el repositorio sea legible, crear el workspace
git clone "/restore/project.git" recovered-worktree

git fsck comprueba la conectividad y validez de los objetos Git; git for-each-ref lista los object ID correspondientes a ramas y etiquetas. Juntos, al menos responden a “¿está dañado el paquete?” y “¿están las ramas y etiquetas esperadas?”. Pero para probar que la recuperación es real, aún hay que revisar el contenido de los archivos, las dependencias, los datos externos, y si se puede seguir compilando y trabajando en una máquina nueva.

Cuando más tarde ordené verify-coldbackup.ps1, fijé el orden de comprobación en una cadena: prueba de integridad del 7z → descifrado y extracción → clonar el workspace → git fsck de objetos → cotejar el SHA de todas las ramas y etiquetas. Mirar solo “el comprimido se abre” no basta ni de lejos; para confirmar que el código está de verdad respaldado, hay que recorrer la restauración completa en un directorio independiente.

Dicho sea de paso, la otra ruta de “hacer mirror de proyectos públicos upstream a mi propio repo privado de GitHub” es una cosa distinta de este Central en LAN. Esa ruta tiene que lidiar con las refs restringidas de GitHub, la cobertura de ramas y el riesgo de borrado en destino de --force --prune, y no debería mezclar ramas personales de desarrollo en el destino del mirror. Los problemas concretos del mirror ya los dejé por separado en la sincronización de repositorios públicos de GitHub a mirrors privados. La documentación oficial de git push también explica con claridad las diferencias entre --mirror, --prune y los refspec.

La paradoja al desarrollar desde el móvil

Con Windows y Mac delante podía trabajar con copias locales completas. Ahora, cuando solo tengo el teléfono y quiero pedir a ChatGPT/Codex un ajuste o una prueba, descubro el coste de aquella decisión. Si Windows está apagado o no se puede acceder a SMB, el Central local deja de ser utilizable desde fuera.

Reducir el tamaño del clon en Mac con partial clone o sparse checkout solo ahorra almacenamiento. No responde a quién descifra el código. Los agentes de programación en la nube normalmente necesitan leer, buscar y ejecutar el código fuente: si el servidor solo conserva texto cifrado, los PR, diffs, la búsqueda y los agentes de GitHub no pueden interpretarlo. Descifrarlo en un entorno cloud que no controlo rompería la premisa original.

La opción que mejor preserva esa frontera es usar el móvil como mando a distancia y ejecutar Git, pruebas y herramientas en un Mac/Linux propio y confiable, mediante red privada y SSH con autenticación fuerte. Lo considero una dirección, no un sistema ya verificado. Harían falta alta disponibilidad, mínimo privilegio y una gestión cuidadosa de claves. Además, si el modelo remoto recibe fragmentos descifrados del código desde esa máquina, eso sigue siendo una salida de datos en claro y merece evaluación propia.

Un remote Git cifrado puede ayudar a mantener la historia fuera de casa, pero no vuelve legible el código para un agente cloud. Falta verificar quién descifra, dónde se ejecuta el código y cómo continúan las tareas si se desconecta el teléfono. Hasta resolverlo, reemplazar el Central por un GitHub Private normal no es una migración que cumpla mis objetivos.

Preguntas frecuentes sobre bare repos de Git, SMB y recuperación de backups

¿Por qué no compartir directamente por SMB un workspace normal de Git?

Git rechaza por defecto hacer push a la rama activa de un repositorio no bare (receive.denyCurrentBranch). Un bare repo no tiene árbol de trabajo, por lo que sirve como Central mientras Win/Mac conservan copias de desarrollo separadas.

El Mac monta el SMB pero git pull falla: ¿qué comprobar primero?

Primero comprueba ls /Volumes, git remote -v y los permisos SMB. No borres archivos .lock sin confirmar antes que no quede ningún proceso Git activo; los bloqueos de un bare repo pueden aparecer en refs. Para uso frecuente por red, considera Git over SSH.

¿Es git clone --mirror más seguro que un git clone normal?

--mirror sincroniza refs y puede propagar los borrados del origen. Para evitar perder historia hacen falta archivos fechados que no se actualicen junto con el mirror.

OneDrive / Google Drive dice “sincronizado”, ¿eso ya es pasar la verificación de hash?

No. Estado de sincronización, comparación local del manifiesto cifrado, SHA-256 de una nueva descarga y la prueba de descifrado, git fsck, refs y clone son verificaciones distintas. El antiguo [OK] solo cubría el destino local y su referencia.

¿Por qué no usar GitHub Private? ¿Cada cuánto probar la recuperación?

GitHub Private controla accesos pero no cumple mi requisito de que el proveedor solo tenga contenido cifrado. Tras configurar o cambiar el backup cifrado, prueba una recuperación independiente con claves, objetos Git, refs y clone; sin las claves, la copia cifrada no sirve.

Referencias: Git init · Git clone · Git remote · Git push · Git fsck · Robocopy · Get-FileHash · Límites de seguridad de Cryptomator · Formato 7z oficial de 7-Zip