# 핵심 소스코드를 GitHub에 평문으로 올리지 않는 이유: Win/Mac 개인 Git과 암호화 백업, 모바일 개발의 한계

> Canonical: https://vxi.me/ko/posts/git-backup-win-mac-bare-repo-disaster-recovery/
> Locale: ko-kr
> Published: 2026-10-15
> Updated: 2026-10-16
> Markdown: https://vxi.me/ko/posts/git-backup-win-mac-bare-repo-disaster-recovery.md

Windows와 Mac 사이에 Git 환경을 만든 이유를 다시 생각해 보면, 처음부터 중요했던 것은 속도나 비용이 아니었습니다. 몇 년씩 투자할 상용 프로젝트의 **핵심 소스코드를 어떤 외부 클라우드에도 평문으로 맡기지 않는다**는 원칙이었습니다. GitHub Private 저장소라 해도 예외는 아니었습니다.

GitHub가 몰래 코드를 가져간다는 뜻은 아닙니다. 내부 권한 오남용이나 계정 탈취, 공급망 침해, 플랫폼 사고가 앞으로도 절대 없을 것이라고 가정하고 장기 자산을 맡기기 싫었던 겁니다. Private는 접근을 통제할 뿐, **호스팅 서비스가 원본 Git 객체를 기술적으로 읽을 수 없다는 뜻은 아닙니다.** 복호화 키는 제가 보관하고 클라우드에는 암호문만 남기고 싶었습니다.

그래서 Windows의 bare repo를 로컬 Central로 두고 Mac에서는 SMB로 push/pull했습니다. 나중에는 E 드라이브 mirror, 분리 보관하는 7z 암호화 아카이브, Cryptomator로 암호화한 OneDrive / Google Drive 사본을 추가했습니다. 그러나 **암호화 백업의 클라우드 동기화와 Git remote 자체의 암호화 push는 다릅니다.** 휴대전화에서 ChatGPT/Codex로 개발을 이어가려 하면서 그 차이가 큰 병목으로 드러났습니다.

아래의 2024~2026년 작업 기록과 Git 2.47.3 합성 저장소 실험은 그대로 보존했습니다. 기존 실험은 mirror, refs, 복원 동작만 검증하며 암호화 원격 Git이나 모바일 개발의 성공을 입증하지 않습니다.

## 왜 GitHub Private를 기본 저장소로 선택하지 않았나

이는 특정 서비스가 코드를 훔친다는 주장이 아니라 신뢰 경계를 정하는 문제입니다. [GitHub의 Private 저장소 약관](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#e-private-repositories)은 비공개 유지와 서비스 측 접근 예외를 설명합니다. 하지만 사용자가 단독으로 키를 관리하는 전체 저장소 클라이언트 암호화와는 보장이 다릅니다. 대부분의 개발에는 Private가 충분할 수 있지만, 제 장기 상용 코드에선 우선순위를 달리했습니다.

| 방식 | 서버에 남는 정보 | 제 요구사항과의 관계 |
|---|---|---|
| 일반 GitHub Private | 플랫폼 접근 제어로 보호하는 일반 Git 객체와 커밋 기록 | 평문 클라우드 금지 원칙에 맞지 않음 |
| 로컬 Git + Cryptomator | 암호화 파일과 일부 크기·시각 메타데이터 | 원격 재해복구에는 유용, Git 협업과는 별개 |
| 클라이언트에서 Git remote 전체 암호화 | 구현에 따라 암호문만 보관하도록 설계 | 별도 검증이 필요한 push/pull 후보 |
| 오프라인 7z 암호화 파일 | 연결이 끊긴 암호화 스냅샷 | 사고 대비책이지 실시간 협업 수단은 아님 |

로컬 PC가 악성코드에 감염되거나 키가 유출되면 보호가 깨질 수 있습니다. 복호화된 소스를 AI 서비스에 전달해도 별도의 유출 경로가 생깁니다. [Cryptomator의 보안 범위](https://docs.cryptomator.org/security/security-target/)는 파일 크기와 시각 등의 메타데이터 한계도 다룹니다.

![상용 소스코드를 평문으로 클라우드에 두지 않는 Git 백업 구조. Windows와 Mac의 로컬 bare Central, E 드라이브 미러, Cryptomator 암호문 클라우드 백업 및 독립 7z 오프라인 아카이브.](/images/posts/git-backup-win-mac-bare-repo-disaster-recovery/zero-trust-git-backup.ko-kr.svg)

*구조도: Git 협업은 신뢰하는 로컬 장치에서, 클라우드에는 클라이언트 암호화된 백업만 보관합니다. 암호화 Git remote push와 휴대전화 개발은 아직 검증이 필요한 설계 방향입니다.*

## Win/Mac 로컬 네트워크 Git 동기화: bare repo를 Central로 쓰는 이유

먼저 실행 시점의 관계부터 봅니다:

```text
Windows 개발 저장소 ─── push / pull ──┐
                                     ├── Windows Central (bare repo)
Mac 개발 저장소 ─ SMB ─ push / pull ─┘           │
                                                │ mirror 업데이트
                                                ▼
                                     E 드라이브 미러 베어 저장소
                                                │
                                    암호화 아카이브 / 오프라인 복사
                                                ▼
                                     외장 하드, 오프사이트 클라우드
```


4계층 중 실질적으로 백업 역할을 하는 건 뒤의 3계층뿐입니다. 이를 펼쳐 보면 어느 계층이 함께 망가질지 분명해집니다:

| 계층과 위치 | 주요 동작 | 주요 대응 | 해결할 수 없는 것 |
|---|---|---|---|
| 협업 Central: Windows D 드라이브 베어 저장소 | Win/Mac `push`, `pull` | 한 대의 개발 머신이 손상돼도 푸시된 코드 복구 가능 | Windows 종료, SMB 연결 끊김, Central 자체 손상; 독립 콜드 백업으로 볼 수 없음 |
| 동일 머신 미러: Windows E 드라이브 mirror | `clone --mirror`, `remote update` | Central 디렉터리 손상 시 최근 미러에서 복원 | D/E 동일 머신 장애; `--prune`이 브랜치를 함께 삭제할 수 있음 |
| 오프라인 아카이브: 연결이 끊긴 외장 하드 | 7z 암호화 패키지, `7z t` | 호스트 장애, 온라인 사본에 영향을 준 랜섬웨어 | 업데이트는 수동; 최신 버전은 아카이브 시점에 따라 결정 |
| 오프사이트 암호문: Cryptomator + OneDrive / Google Drive | `robocopy`, 암호문 목록 검증, 다운로드 검증 | 로컬 장비 전부 분실, 오프사이트에서 회수 | 클라우드 동기화도 오류를 전파할 수 있음; 미커밋 코드와 외부 데이터는 포함하지 않음 |

RPO(어느 버전까지 잃을 수 있는지)는 각 계층의 **마지막 성공 커밋, 동기화 또는 아카이브 시점**에 의해 결정됩니다. RTO(복구에 얼마나 걸리는지)는 매체 속도, 복호화, 연습 상황에 따라 달라집니다. 예전에는 실제 복구 소요 시간을 따로 기록하지 않았기 때문에, 여기서는 복구 경로만 비교하고 가상의 분 단위 숫자로 RPO/RTO인 척하지 않겠습니다. D/E가 같은 Windows 머신에 있으니, 드라이브 문자 두 개로는 전체 머신 장애라는 공통 위험을 없앨 수 없습니다.

Central의 핵심은 "공유 폴더가 하나 있다"가 아니라, 그 안에 들어 있는 것이 **bare repository(베어 저장소)**라는 점입니다. Git 오브젝트, ref, 저장소 설정은 있지만 일반 워킹 디렉터리는 없습니다. 두 대의 개발 머신은 각자 워킹 트리를 가지고, 수정은 로컬에서 마친 뒤 커밋하고 나서야 Central에 푸시합니다.

Git 공식 [`git init` 문서](https://git-scm.com/docs/git-init)에 따르면, 베어 저장소를 새로 만드는 기본 형태는 이렇습니다:

```bash
# 示例路径，不是我原机器的目录
git init --bare "D:/git-central/project.git"

# 已有 Windows 工作仓库中配置 Central
git remote add central "D:/git-central/project.git"
git push -u central master
```

Mac 쪽은 먼저 SMB 공유를 로컬에 마운트한 뒤, 마운트 경로를 `central` 리모트로 설정합니다. 아래의 `master`는 그 시절 저장소에서 쓰던 브랜치 이름입니다. 기본 브랜치가 `main`이라면 `master`를 그대로 따라 치지 마세요.

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


예전 운영 매뉴얼에는 Mac 쪽 문제 해결 명령도 몇 줄 적혀 있었습니다. "Git이 업데이트를 안 한 건지, SMB가 아예 마운트가 안 된 건지" 추측하는 수고를 덜어주죠:

```bash
# 在 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
```

원래 메모에는 공유 디렉터리에서 베어 저장소가 실제로 보이는지 먼저 확인한 뒤 `central` 리모트를 조작하라고 적혀 있었습니다. `git pull` 오류를 보자마자 급하게 `reset --hard`를 치라는 게 아니었죠. Windows 쪽도 `git status`를 보고 커밋한 뒤 `git push central master` 순서로 했습니다. 이 명령들은 그 시절 운영 메모에서 온 것으로, 이 글에서는 내부 호스트명과 공유 경로를 예시 플레이스홀더로 바꿨습니다.

당시의 일상 습관은 이랬습니다. 개발하기 전에 `git pull central master`, 개발이 끝나면 `git status`를 보고 `git add .`, `git commit`, `git push central master`. 다른 컴퓨터도 같은 입구로 pull합니다. 예시의 `--ff-only`는 제가 지금 더 선호하는 보호 장치입니다. 분기가 생기면 멈춰서 처리하고, 일반 `pull`이 조용히 머지 커밋을 만들어 버리는 걸 막는 거죠.

이 구조의 실용적인 장점 하나: Central이 망가져도 두 대 개발 머신의 Git 히스토리는 살아 있고, 한 대의 개발 머신이 망가져도 히스토리가 함께 사라지는 건 아닙니다. 저는 "온전한 저장소는 재건의 씨앗이 될 수 있다"를 설계 원칙으로 삼아 왔습니다. 다만 이 말에는 조건이 있습니다. 그 저장소가 내가 필요로 하는 브랜치, 태그, 오브젝트를 정말로 가지고 있어야 합니다. 싱글 브랜치 clone만 했거나, shallow clone을 했거나, 미커밋 파일을 워킹 트리에 둔 경우라면 자동으로 온전한 백업이라고 볼 수 없습니다.

또 하나 놓치기 쉬운 경계: SMB로 베어 저장소에 직접 접근하는 건 실제로 공유 파일시스템의 권한, 락, 네트워크 안정성에 의존합니다. 예전 방식은 제 두 대 머신 습관에는 맞았지만, 지역이 다른 환경, 다중 사용자, 불안정한 네트워크에 맞는다는 뜻은 아닙니다. 장기 Git 서비스를 새로 만들 때는 공유 디스크를 범용 Git 서버처럼 쓰는 것보다 Git over SSH 같은 원격 전송을 쓰겠습니다.

## Git mirror 콜드 백업: 동기화 삭제와 히스토리 스냅샷은 다르다

실제로 안전 거리를 벌리는 건 Central 뒤의 몇 계층입니다. 당시 E 드라이브에는 `--mirror` 방식으로 clone한 베어 저장소를 두었습니다. 개발에는 참여하지 않고 업데이트만 하는 거죠. 외장 하드에는 암호화 아카이브를, 클라우드에는 암호화 처리한 오프사이트 사본을 두었습니다. 이건 같은 `git clone` 네 개가 아니라, 서로 다른 장애 범위에 대응하는 겁니다.

Git 공식 문서의 [`git clone --mirror`](https://git-scm.com/docs/git-clone) 정의는 꼼꼼히 볼 가치가 있습니다. mirror는 브랜치뿐 아니라 미러 refspec에 따라 다른 ref도 동기화합니다. 일반 개발용 clone과는 다르고, 이후의 `git remote update`가 대응하는 refs를 덮어쓸 수 있습니다. 처음 만들 때와 일상 업데이트는 이렇게 쓸 수 있습니다:

```bash
# 只在初始化冷备仓库时执行一次
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
```


당시의 E 드라이브 콜드 백업 운영 매뉴얼에는 `git remote update` 외에도 현재 커밋을 확인하는 방법이 명시돼 있었습니다:

```bash
# 在 mirror 裸仓库内执行；本例沿用历史 master 分支
git remote update
git log origin/master -1 --oneline

# 不只比最近一条 log；正式恢复还需要核对 refs 和对象
git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags
```

여기서 `origin/master`는 과거 메모의 명령 표기법입니다. 베어 미러에서 그 이름으로 ref에 접근할 수 있는지는 실제 설정과 `git show-ref` 결과에 따라 다릅니다. 더 믿을 만한 범용 방법은 `refs/heads/master`와 보관해야 할 다른 refs를 직접 보는 겁니다.

저는 Git 2.47.3으로 임시 저장소를 만들어 `--prune`의 삭제 사슬을 한 번 돌려 봤습니다. `test-prune` 브랜치를 푸시하고 mirror를 동기화한 뒤, Central에서 그 브랜치를 삭제하고 다시 미러를 업데이트했습니다. 가장 핵심적인 출력은 몇 줄뿐입니다:

```text
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
```

잘 보세요: `git fsck`는 0을 반환하고 dangling commit도 찾았지만, 미러에는 이미 `test-prune` 브랜치 ref가 없습니다. 남은 오브젝트는 이후 가비지 컬렉션 때 사라질 수도 있습니다. 미러는 "타임머신"이 아닙니다 — 최신 상태를 동기화하고, 삭제도 동기화합니다. 이 실험이 검증하는 건 Git의 ref 동작입니다. 7z와 클라우드의 신뢰성은 실제 아카이브로 복구 연습을 해야 합니다.

[`git remote` 문서](https://git-scm.com/docs/git-remote)에 따르면, `remote update`는 fetch를 수행하고, `--prune`은 설정에 따라 이미 무효화된 원격 ref를 정리합니다. 매번 업데이트는 보통 필요한 새 오브젝트만 전송하므로, 저장소 전체를 다시 복사할 필요는 없습니다. 하지만 **mirror는 동기화 사본이지 불변 스냅샷이 아닙니다**. 업스트림에서 브랜치를 실수로 지우면, 다음 prune 포함 동기화가 그 삭제를 미러에도 전파할 수 있습니다. 현재 상태를 빠르게 복원하는 데는 적합하지만, "실수 방지, 랜섬웨어 방지"를 혼자 떠맡을 수는 없습니다.

그래서 당시에는 두 갈래의 백업 라인을 따로 유지했습니다. 하나는 콜드 백업 저장소를 날짜가 붙은 7z 암호화 아카이브로 묶어, 평소 연결이 끊긴 외장 하드에 두는 것. 다른 하나는 Git 미러를 Cryptomator 암호화 vault에 넣고, 그 vault의 **암호문 디렉터리**를 OneDrive / Google Drive 동기화 디렉터리로 복사하는 것입니다. 같은 Git 히스토리를 보호할 수는 있지만, "모든 클라우드 사본은 먼저 7z를 거쳐 다시 Cryptomator를 거친다"고 단순하게 그릴 수는 없습니다. 진짜 가치 있는 건, Central의 다음 업데이트 때 덮어쓰이지 않는 어느 한 부의 히스토리 아카이브입니다. 암호화 패스워드는 아카이브와 함께 두면 안 되고, 복사 성공도 복구 성공과 같지 않습니다.

같은 Windows 머신의 D 드라이브 Central과 E 드라이브 미러는 저장소 실수나 단일 볼륨 손상에는 도움이 되지만, 전체 머신 장애, 도난, 같은 머신의 랜섬웨어는 막지 못합니다. 외장 하드는 백업이 끝난 뒤 실제로 연결을 끊어야 오프라인 격리가 의미를 갖습니다. 클라우드 스토리지가 삭제나 손상을 동기화했다면, 사용 가능한 예전 버전을 유지하지 못할 수도 있습니다. 이런 차이를 알게 된 뒤로는 "이미 백업이 몇 부나 있는데"라는 말에 만족하지 않게 됐습니다.

한 가지 범위를 더 보충하죠. Git은 커밋된 Git 오브젝트와 ref를 보관하지, `.gitignore`로 제외된 대용량 데이터, 미커밋 워킹 디렉터리 변경, 외부 데이터베이스까지 대신 백업해 주지는 않습니다. Git LFS나 서브모듈을 쓰는 프로젝트라면 해당 오브젝트와 의존성이 백업됐는지 따로 확인해야 합니다. 저장소 미러가 온전하다고 해서 전체 개발 환경이 온전한 건 아닙니다. Docker volume 같은 런타임 데이터는 다른 백업 체계를 써야 합니다. 저는 Docker 데이터 볼륨을 GitHub에 백업하는 재해 복구 실전을 [별도 글](/posts/docker-volume-backup-github-sync/)로 쓴 적도 있습니다.

## Cryptomator + 클라우드: SHA-256으로 암호화 백업 검증하기

클라우드 계층에서 가장 까다로운 건 업로드가 아니라, 업로드 이후에 파일이 빠지지 않았는지 확인하는 일입니다. 당시 Cryptomator + OneDrive, Cryptomator + Google Drive용으로 운영 메모를 두 부 남겼습니다. 여기에는 암호문 베이스라인 목록, `VaultIntegrityCheck.ps1`의 호출 순서, 복사 완료 후 `MISSING`, `CHANGED`, `EXTRA`를 어떻게 찾는지가 들어 있습니다.

Cryptomator는 파일 내용과 이름을 클라이언트 측에서 암호화하고, 클라우드 클라이언트는 전송을 담당합니다. 둘은 같은 소프트웨어의 두 기능이 아닙니다. [Cryptomator 공식 보안 설명](https://docs.cryptomator.org/security/security-target/)도 암호화가 백업을 대신할 수 없다고 강조합니다. 실제 예전 흐름은 이거에 가깝습니다:

```text
E: 콜드 백업 미러 / Cryptomator 암호화 vault(베이스라인)
     │
     ├─ 안정적인 암호문 디렉터리에서 핑거프린트 목록 생성(Update)
     │
     ├─ robocopy로 OneDrive 또는 Google Drive 로컬 동기화 디렉터리로
     │
     ├─ 클라우드 클라이언트 동기화 완료 표시 대기
     │
     └─ 대상 암호문 디렉터리 읽기 전용 비교(Verify)
          ├─ MISSING: 소스에는 있고, 대상에는 없음
          ├─ CHANGED: 경로는 같지만, 파일 핑거프린트 변경
          ├─ EXTRA: 대상에 파일이 더 많음, 충돌/임시 파일 여부 판단 필요
          └─ 일치: 로컬 동기화 디렉터리가 목록과 대응
```

당시 자체 제작한 `VaultIntegrityCheck.ps1`에는 `Update`와 `Verify` 두 모드가 있었습니다. **올바른 E 드라이브 암호문 원본을 확인했을 때만 베이스라인을 갱신**하고, 대상 검증에서는 목록을 바꾸지 않았습니다. 아래는 과거 호출 예시입니다. 뒤의 기본 PowerShell 예제로 두 로컬 암호문 폴더를 비교할 수 있지만, 영구 베이스라인을 관리하지는 못합니다:

```powershell
# 只在确认 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
```

Google Drive 버전은 두 단계 사이에 `robocopy` 복사 명령을 남겼습니다. 파라미터는 `/E /R:2 /W:2 /MT:8 /FFT /Z /NP`. 중립 디렉터리로 바꾼 복사 가능한 골격은 이렇습니다:

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

Microsoft의 [Robocopy 종료 코드 설명](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy/)에 따르면, 반환 코드가 **8 이상**일 때만 적어도 한 번의 복사가 실패했다는 뜻입니다. 하지만 `0–7`은 복사 단계에서 그런 실패를 보고하지 않았다는 의미일 뿐, "모든 파일의 SHA-256이 통과했다"고 쓰면 안 됩니다. `/E`는 하위 디렉터리를 포함하지만, 대상에 이미 있는 여분 파일을 지우지는 않습니다. 유일한 클라우드 사본에서는 목표 파일을 지우는 `/MIR`로 무심코 바꾸지 않겠습니다.

과거 메모에 남은 검증 피드백에는 `[OK] Vault matches the manifest exactly.`, `MISSING`, `CHANGED`, `EXTRA` 같은 유형이 있습니다. `MISSING`이나 `CHANGED`을 보면 먼저 업로드가 끝났는지, 파일이 아직 쓰이는 중인지 확인하세요. 클라우드 디렉터리에 Update를 실행해 이상을 새 베이스라인으로 덮어씌우지 마세요. Google Drive 메모에는 `.tmp.driveupload` 임시 파일이 기록돼 있었습니다. 동기화 중간 상태일 수 있으니, 무조건 무시하지 말고 동기화 완료 후 다시 검증하는 게 안전합니다.

그 개인 스크립트가 없는 독자라면, 최소한 Windows 내장 `Get-FileHash`로 단일 파일의 SHA-256을 확인할 수 있습니다. PowerShell 표준 cmdlet이고, 알고리즘과 `-LiteralPath` 파라미터는 [Microsoft 공식 문서](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash)에 나와 있습니다:

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

하지만 설정 파일 하나만 확인하는 건 턱없이 부족합니다. vault 전체의 목록은 최소한 **상대 경로, 파일 크기, 각 암호문 파일의 SHA-256**을 커버해야 하고, 대상의 파일 추가/누락/동일 이름 변경을 검출해야 합니다. 또 소스 베이스라인 날짜가 명확해야 하고, 양쪽에서 쓰기가 진행되는 중에 비교하면 안 됩니다. 검증 도구가 따로 없다면 범용 읽기 전용 PowerShell 스크립트로도 처리할 수 있습니다. 소스와 대상의 파일들을 재귀적으로 나열해 상대 경로, 바이트 수, SHA-256을 구한 뒤 `Compare-Object`로 차이를 찾는 거죠:

```powershell
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"
```

이 새 예시는 파일만 커버하고, 빈 디렉터리나 클라우드 서버 쪽 상태는 다루지 않으며, 과거 `Update/Verify`의 영속 베이스라인 기능도 없습니다. 진짜 클라우드 검증에는 아직 한 단계가 남았습니다. **클라우드의 어느 한 버전 암호화 아카이브를 동기화 디렉터리와 무관한 위치에 다시 다운로드한 뒤**, 다운로드한 파일의 해시를 업로드 전에 남겨 둔 신뢰할 수 있는 기록과 비교하는 겁니다. "클라우드 동기화 아이콘이 완료됐다 + 로컬 디렉터리가 일치한다"만으로는 원격 파일을 되찾을 수 있다는 증명이 안 됩니다.

오프라인 외장 하드에는 당시 7z 전체 암호화 패키지를 썼고, 핫 미러에만 의존하지 않았습니다. 7z의 `-mhe=on`은 7z 포맷 안의 파일명을 암호화하고, `-p`는 패스워드를 사용한다는 뜻입니다. 패스워드를 공유 스크립트, 명령 히스토리, 로그에 평문으로 박아 넣지 마세요. [7-Zip 공식 7z 포맷 설명](https://www.7-zip.org/7z.html)도 AES-256을 지원한다고 확인해 줍니다. 아카이브를 만들 때는 GUI에서 패스워드와 "파일명 암호화"를 확인하는 걸 권합니다. 아래는 CLI 파라미터 예시를 남기고, 실행 전에는 해당 머신의 `7z -h`로 버전과 패스워드 입력 방식을 확인하세요. 패스워드를 스크립트나 명령 히스토리에 직접 쓰지 마세요:

```powershell
# 只在冷备仓库没有写入时，对其目录创建带口令的 7z 归档
7z a -t7z -p -mhe=on "git-snapshot-YYYYMMDD.7z" "E:\GitCold\project.git"

# 测试归档是否能读完：输入口令后检查退出码
7z t "git-snapshot-YYYYMMDD.7z"
```

예를 들어 이 아카이브를 클라우드에서 `D:\Downloads`로 다시 내려받았다면, 업로드 전후의 바이트 일치를 이렇게 검증할 수 있습니다:

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

해시가 같다는 건 **이 두 아카이브의 바이트가 일치한다**는 뜻일 뿐, 베이스라인 자체가 변조되지 않았다는 증명도, `7z t`·압축 해제·`git fsck`·ref 대조·실제 clone을 대신할 수도 없습니다. 베이스라인 목록은 독립적으로 보관하고 같은 장애에 함께 변조되지 않게 해야 합니다. 외장 하드 백업이 끝나면 실제로 연결을 끊으세요. 이렇게 "암호문 전송이 정확하다"와 "코드 히스토리를 복원할 수 있다"를 서로 다른 검사로 나누어 두면, 사고가 났을 때 하나의 `[OK]`를 만능 보험으로 착각하지 않게 됩니다.


## 아직 없는 기능: 암호문을 보내는 Git push

지금까지의 `git push`와 `git pull`은 Windows/Mac이 **로컬 평문 bare Central**과 주고받은 작업입니다. 별도로 mirror를 Cryptomator vault에 담은 다음 암호화된 파일을 클라우드 드라이브에 동기화했습니다. 파일 동기화가 빠르다고 해서 GitHub의 브랜치와 diff를 사용할 수 있는 암호화 Git remote가 생기는 건 아닙니다.

원하는 것은 커밋이 생긴 뒤 가능한 한 빨리 Git 히스토리 전체를 클라이언트에서 암호화해 원격에 보내는 것입니다. 출발하기 전에 암호화하고, 키를 독립적으로 백업하고, 충돌·중단·강제 덮어쓰기·실제 복원을 확인해야 합니다. 이는 커밋된 이력의 보호이며 아직 커밋하지 않은 작업 디렉터리까지 자동 저장하지는 않습니다.

[git-remote-gcrypt](https://github.com/spwhitton/git-remote-gcrypt)가 연구 후보입니다. GnuPG를 이용해 암호화된 Git remote 표현을 만들지만, [공식 README](https://github.com/spwhitton/git-remote-gcrypt/blob/master/README.rst)에 따르면 일반 Git URL 백엔드에서는 push마다 히스토리 전체를 다시 올려야 하며, 사실상 강제 push가 되는 알려진 문제도 있습니다. rsync 백엔드는 증분 전송이 가능하지만 일반적인 GitHub Private의 사용 방식은 아닙니다. 저는 아직 이 경로를 상용 프로젝트에서 검증하지 않았습니다.

선택한 파일만 암호화하는 [git-crypt](https://github.com/AGWA/git-crypt/blob/master/README.md)는 파일명과 커밋 메시지 같은 메타데이터를 감추지 못하므로 같은 대안이 아닙니다. 로컬 Git을 유지한 채 암호화 백업 빈도를 높이는 것은 현실적인 방법이지만, 클라우드의 PR이나 AI Agent가 암호문을 이해하게 만들지는 못합니다.

## Git 재해 복구: 어떤 사본에서 코드와 히스토리를 복원할 것인가

원래는 저를 위해 두 가지 복원 루트를 남겼습니다. 사고가 났을 때는 빈 프로젝트를 급히 새로 만드는 대신, 손에 있는 **어느 저장소가 목표 refs를 아직 가지고 있는지** 먼저 판단합니다:

```text
Git 프로젝트 복원 필요
  ├─ 개발 머신에 커밋 / 브랜치 / 태그가 온전함
  │    └─ 빈 bare Central 새로 생성 → refs 확인 → 로컬 브랜치와 tags 푸시
  ├─ 개발 머신은 불완전하지만 E 드라이브 mirror 읽기 가능
  │    └─ fsck → refs 확인 → 워킹 디렉터리 clone 후 히스토리 비교
  └─ 온라인 사본 전부 사용 불가
       └─ 오프라인/오프사이트 아카이브 찾기 → SHA-256 검증 → 7z 테스트와 압축 해제
                                     → git fsck → refs 대조 → clone
```

이 판단은 또 하나를 상기시킵니다. `git push --all`은 **로컬** 브랜치만 푸시한다는 겁니다. 현재 워킹 디렉터리가 실행된다고 해서, 과거 모든 개발 머신의 모든 브랜치가 이 한 부에 다 있다고 가정하면 안 됩니다.


하나는 Central을 잃었지만 온전한 개발 저장소가 남아 있는 경우입니다. 이때는 베어 저장소를 새로 만든 뒤 개발 머신에서 필요한 브랜치와 태그를 다시 푸시하면 됩니다:

```bash
# 在新 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
```

여기서 꼭 밝혀야 합니다. `--all`이 푸시하는 건 이 개발 머신의 **로컬 브랜치**이지 모든 `refs/remotes/*`가 아닙니다. 다른 머신에 독립 브랜치가 있었는데 이 개발 머신에 로컬 브랜치로 보관돼 있지 않다면, `git push --all`이 허공에서 찾아와 준다고 주장할 수 없습니다. Central을 정식으로 전환하기 전에는 남아 있는 저장소의 refs 목록과 대조하고, 필요하면 다른 살아남은 사본에서 보충해야 합니다.

더 극단적인 경우는 이렇습니다. 로컬 개발 머신과 Central이 모두 못 쓰게 된 경우. 복원의 입구는 외장 하드나 클라우드에 있는 아카이브가 됩니다. 순서는 이렇습니다. 날짜가 명확한 백업 하나를 확보하고, 복호화와 압축 해제를 마치고, 베어 저장소 오브젝트를 검사하고, 예상 refs를 대조한 뒤, 마지막에 워킹 디렉터리를 clone합니다. 갓 풀어낸 백업에 덮어쓰지 마시고, `--force`나 `--prune`이 붙은 미러 푸시를 첫 단계로 실행하지 마세요.

```bash
# 在解压得到的裸仓库上执行
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`](https://git-scm.com/docs/git-fsck)는 Git 오브젝트의 연결성과 유효성을 검사하고, [`git for-each-ref`](https://git-scm.com/docs/git-for-each-ref)는 브랜치와 태그에 대응하는 오브젝트 ID를 나열할 수 있습니다. 둘을 함께 쓰면 최소한 "패키지가 손상됐는지", "예상한 브랜치와 태그가 있는지"에는 답할 수 있습니다. 하지만 진짜 복원 가능하다는 걸 증명하려면 파일 내용, 의존성, 외부 데이터까지 확인하고, 새 머신에서 계속 빌드하고 작업할 수 있는지도 봐야 합니다.

나중에 `verify-coldbackup.ps1`을 정리하면서 검사 순서를 하나의 사슬로 고정했습니다. **7z 무결성 테스트 → 복호화·압축 해제 → 워킹 디렉터리 clone → `git fsck` 오브젝트 검사 → 전체 브랜치와 태그 SHA 대조**. "압축 파일이 열린다"만 보는 건 턱없이 부족합니다. 코드가 정말 백업됐는지 확인하려면, 독립 디렉터리에서 복원을 처음부터 끝까지 돌려 봐야 합니다.

덧붙이자면, "업스트림 공개 프로젝트를 자기 GitHub 프라이빗 저장소에 미러링하는" 경로는 이 로컬 네트워크 Central과는 다른 이야기입니다. 그쪽은 GitHub의 제한된 refs, 브랜치 덮어쓰기, `--force --prune`의 대상 삭제 위험을 다뤄야 하고, 개인 개발 브랜치를 미러 타깃에 섞어서는 안 됩니다. 구체적인 미러 문제는 [공개 GitHub 저장소를 Private 미러로 동기화하는 백업 방법](/posts/github-public-repo-private-mirror-backup/)에 따로 정리했으니 여기서 반복하지 않겠습니다. 공식 [`git push` 문서](https://git-scm.com/docs/git-push)도 `--mirror`, `--prune`, refspec의 차이를 아주 분명히 설명합니다.

## 휴대전화에서 개발하려 할 때 드러나는 대가

Windows와 Mac 앞에서 개발할 때는 둘 다 코드를 보유하고 로컬 Central에 연결하면 됐습니다. 지금은 휴대전화만 들고 있는 상황에서도 ChatGPT/Codex에 작은 수정이나 테스트를 맡기고 싶습니다. 그런데 Windows가 꺼져 있거나 SMB를 쓸 수 없으면 협업 경로도 멈춥니다.

Mac 저장 공간은 partial clone이나 sparse checkout으로 줄일 수 있지만 누가 복호화할지까지 해결되지는 않습니다. 클라우드 코딩 Agent는 대개 소스를 읽고 실행해야 합니다. 클라우드에는 암호문만 있으면 GitHub의 PR·diff·코드 검색과 Agent가 내용을 이해하지 못하고, 반대로 클라우드에서 복호화하면 처음 정한 금지선을 넘습니다.

더 일관된 방향은 **휴대전화를 제어 화면으로 쓰고 평문 소스와 실행을 신뢰하는 상시 가동 Mac/Linux에 두는 것**입니다. 개인 네트워크와 SSH로 연결해 해당 머신에서 Git과 개발 도구를 실행하는 식입니다. 아직 구축과 운영 검증이 끝난 것이 아닙니다. 호스트 가용성, 인증, 권한을 검토해야 하며, 원격 AI 모델로 복호화된 코드가 전달된다면 그 역시 별도의 평문 전송이라는 점을 숨기면 안 됩니다.

암호화 Git remote가 구현되어도 원격 AI가 소스를 이해하게 되는 것은 아닙니다. **누가 복호화하고, 어디서 코드를 실행하며, 휴대전화가 끊기면 어떻게 되는가**를 따로 검증해야 합니다. 이 과정이 끝나기 전에는 GitHub Private를 단일 Central로 바꾸는 것을 해결책으로 부를 수 없습니다.

## Git bare repo, SMB, 백업 복원 관련 자주 묻는 질문

### SMB로 일반 Git 워킹 디렉터리를 직접 공유하면 안 되나요?

일반 작업 저장소에서는 Git이 현재 체크아웃된 브랜치에 대한 push를 기본적으로 막습니다([`receive.denyCurrentBranch`](https://git-scm.com/docs/git-config)). 워킹 트리가 없는 bare repo를 Central로 두고 Windows/Mac에서 따로 개발하는 편이 맞습니다.

### Mac에서 SMB 마운트 후 `git pull`이 실패하면 무엇을 먼저 확인해야 하나요?

`ls /Volumes`, `git remote -v`, SMB 권한부터 확인하세요. 잠금 파일을 조사할 때는 **Git 프로세스가 끝났는지 확인하기 전 `.lock`을 지우지 마세요**. 네트워크를 자주 넘나들면 Git over SSH가 더 적합할 수 있습니다.

### `git clone --mirror`이 일반 `git clone`보다 안전한가요?

`--mirror`는 refs를 동기화하므로 원본에서 지운 브랜치가 다음 갱신 때 사라질 수 있습니다. 실수에 대비하려면 **원본 삭제를 따라가지 않는 날짜별 아카이브**가 필요합니다.

### OneDrive / Google Drive에 "동기화됨"으로 표시되면 해시 검증 통과인가요?

아닙니다. 동기화 표시, 로컬 암호문 목록 검증, 클라우드에서 새로 다운로드한 SHA-256 비교, 복호화·`git fsck`·refs 확인·clone은 각각 다릅니다. 예전 스크립트의 `[OK]`도 검사한 로컬 대상과 기준에 한정됩니다.

### GitHub Private를 쓰지 않는 이유와 복원 테스트 주기는?

GitHub Private는 접근 권한을 통제하지만 제 핵심 상용 코드의 평문 클라우드 금지 조건은 충족하지 못합니다. 암호화 백업을 설정하거나 변경하면 키·Git 객체·refs를 이용해 독립 복원과 clone을 수행해야 합니다. **키를 잃으면 암호문만으로는 복구할 수 없습니다**.

참고 자료：[Git init](https://git-scm.com/docs/git-init) · [Git clone](https://git-scm.com/docs/git-clone) · [Git remote](https://git-scm.com/docs/git-remote) · [Git push](https://git-scm.com/docs/git-push) · [Git fsck](https://git-scm.com/docs/git-fsck) · [Robocopy](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy/) · [Get-FileHash](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash) · [Cryptomator 보안 경계](https://docs.cryptomator.org/security/security-target/) · [7-Zip 공식 7z 포맷](https://www.7-zip.org/7z.html)
