# SOP: Keep a git repo alive inside cloud storage

Drop this file into your own Company Brain. It is the complete standard operating procedure
for any repository that lives inside a cloud-synced folder (Google Drive, OneDrive, SharePoint,
Dropbox, Box, iCloud Drive) and is worked on from more than one machine, or by AI agents.

Full guide with copy-paste scripts: https://os.agenticsociety.com/tutorials/google-drive-drift

Last verified 2026-08-14.

---

## 1. The problem

`.git` is a database. Cloud storage is a file syncer that resolves conflicts by **renaming**.
Point several machines, harnesses and agents at one synced repo and that database will be
corrupted eventually. This is not a bug in your sync tool and no script prevents it.

Two mechanics do the damage:

1. **The sync tool sometimes renames the original.** It does not only add `object 2` beside
   `object`. It sometimes renames the original to `object 2` and leaves nothing at the real
   name. Git objects are content-addressed and immutable, so a renamed object is a missing
   object.
2. **Naive cleanup finishes the job.** A guardrail that deletes every spaced filename inside
   `.git` deletes those renamed originals too. That is how a scare becomes a lost repository.

So the goal is not prevention. **The goal is that corruption costs thirty seconds and is
impossible to miss.**

## 2. The working rule

> **The remote is the source of truth. The synced folder is a replaceable view of it.
> Nothing irreplaceable ever lives only in that folder.**

Three habits carry it:

- **Starting work on any machine:** run the parity check. You are then provably on the remote.
- **Before walking away:** push. Unpushed work is the only thing that can actually be lost.
- **Never** run `git gc`, `git prune` or `git repack` on a repo you suspect is damaged. Those
  turn a recoverable repository into a lost one.

## 3. The one rule that makes automatic cleanup safe

Git never puts a space in the filenames it creates inside `.git`, so a spaced name there is
always sync junk. But "junk" does not mean "delete":

| What you find inside `.git`            | What happened                          | Correct action                     |
| -------------------------------------- | -------------------------------------- | ---------------------------------- |
| `x 2` exists, `x` is **missing**       | Sync renamed the original              | **Restore.** Rename it back.       |
| `x 2` exists, `x` is present           | Sync added a duplicate, original intact | Delete the duplicate               |

Nothing inside `.git` is ever deleted unless a correctly named file survives beside it.

In the working tree, treat a spaced name as junk **only** when its de-suffixed sibling also
exists, so real filenames like `chapter 2.md` are left alone.

## 4. The four guardrails

1. **`clean-drive-drift.sh`** - restores files the sync tool renamed inside `.git`, removes
   genuine duplicates, then verifies with `git fsck`. `--check` reports only; `--fix` acts.
2. **`repo-parity-check.sh`** - the session trust check. Answers four questions and exits
   non-zero if any fails: is the object store intact, does HEAD match the remote, is the
   working tree clean, are there conflict copies.
3. **`sync-mount.sh`** - forces a stale or diverged folder back to the remote in seconds
   instead of a re-clone. Refuses when unpushed commits or local edits would be destroyed.
4. **Git hooks** - `pre-commit`, `post-merge` and `post-checkout` run the cleaner
   automatically. `pre-push` also runs `git fsck` and refuses to push from a damaged repo.
   Activate once per clone: `git config core.hooksPath .githooks`.

Everything is plain bash, coreutils and git, with no dependencies to install, so it behaves
identically for any agent or a human at a terminal.

## 5. Install checklist

Do these in order. Step 1 is the one people skip and the one that prevents data loss.

1. **Audit for non-git material FIRST.** Anything that exists only in the repo folder dies in
   a re-clone.

   ```bash
   git status --porcelain --ignored | grep '^!!' | sed 's/^!! //' \
     | grep -vE '^(dist/|node_modules/|\.git/|build/|target/)' | grep -v '__pycache__'
   ```

   Move working documents and client material to a sibling folder outside the repo. Commit
   real source. Leave machine-local secrets in place, and write down which ones a new machine
   needs by hand.

2. **Prove the remote exists and is current.**

   ```bash
   git remote -v && git status --short && git log --oneline origin/main..HEAD
   ```

3. **Install the scripts into `scripts/guardrails/` and the hooks into `.githooks/`**, then:

   ```bash
   chmod +x scripts/guardrails/*.sh .githooks/*
   git config core.hooksPath .githooks
   ```

4. **Wire the start-of-session check for every tool that touches the repo.** The git hooks
   already fire for all of them. This is the extra layer: a `SessionStart` hook for Claude
   Code, an instruction block in `AGENTS.md` for Codex, Cursor and similar, a `make check`
   target for a human. Do not wire only the tool you happen to be using.

5. **Verify by simulation. Do not assume it works.**

   ```bash
   OBJ="$(find .git/objects -type f ! -name '* *' ! -path '*pack*' | head -1)"
   mv "$OBJ" "$OBJ 2"                                 # simulate the rename
   scripts/guardrails/clean-drive-drift.sh --fix      # must say RESTORED, not removed
   [ -e "$OBJ" ] && echo "PASS: object restored" || echo "FAIL: object lost"
   git archive HEAD >/dev/null && echo "PASS: repo intact"
   ```

   If the guardrail *removes* that object, you have the naive version. Stop and replace it.

6. **Write it down in both `AGENTS.md` and `CLAUDE.md`.** A repo documented in only one is
   protected for one tool and silently unprotected for every other one.

**You are done when:** the audit returns only machine-local secrets, the simulation restores
the object rather than deleting it, and both files carry the rule.

## 6. Rules for every agent and human

1. Run the parity check before starting work. Do not build on a folder you have not verified.
2. Never commit a filename ending in `" 2"` / `" 3"` / `" (1)"`. It is sync junk, not a file.
3. If a git command fails with object or ref corruption, or `badRefName`: run the cleaner with
   `--fix` first. If `git fsck` still fails, STOP. No `gc`, `prune` or `repack`, no commit, no
   push. Recover with a fresh clone.
4. Push before you walk away.
5. If the hooks never seem to fire, run `git config core.hooksPath .githooks`. Cloud sync also
   periodically strips the executable bit, so re-run `chmod +x` on the scripts and hooks.

## 7. Recovery, when it happens anyway

```bash
# 1. Do NOT gc/prune/repack. Confirm the damage.
git fsck --connectivity-only && git archive HEAD >/dev/null   # both must succeed

# 2. Everything pushed is safe. Clone fresh alongside.
cd "$(dirname "$PWD")" && git clone <remote-url> <name>-fresh

# 3. Carry across only the machine-local files, then compare before swapping.
diff -rq "<old>" "<name>-fresh" | grep -v '\.git/'

# 4. Rename rather than delete the old folder. Bin it a week later.
```

Verify the fresh clone before trusting it: `git fsck` clean, `git archive HEAD` succeeds, the
build runs, and the machine-local files are present.

## 8. What this does not solve

**Two machines editing the same file at the same moment.** No hook catches that. The parity
check will tell each machine it has diverged, but the conflict already exists by then. Pushing
before you walk away is what actually prevents it.

**The root cause.** The only arrangement that removes this entirely is one clone per machine on
local disk, with cloud storage holding your brain and documents but not the git repository.
That is *more* multi-machine, not less, because git becomes the sync layer instead of your
storage provider. If the parity check starts firing often, treat it as the signal that the
convenience now costs more than it saves.
