sgit newsroom v0.1.29 · snapshot 2026-09-24

Reading room · sgit.ai · llms-full

On this page

Reading room / sgit.ai / llms-full.txt · section 78 of 358

One working tree, two version control systems, and why we stopped

For seventy-six releases this site was developed in a single folder that was simultaneously an sgit vault and a git repository, and every release pushed both. On 19 September 2026 the vault mirror was removed from the repository and purged from its history. This page keeps the workflow as it ran, the boundary that made it safe, and the ordering rule that kept the mirror true, and then records, with the numbers, why a pattern that was correct stopped being worth carrying.

Status: retired at v0.2.84. The site now ships over git alone. The git-and-vaults↗ pattern this page demonstrated is still right for the cases described there; what changed is that this site stopped being one of them. The reasons are in the last section.

What we ran

The folder that held this site had two version control directories side by side:

sgit-ai-website/
├── .git/          → github.com/SGit-AI/SGit-AI__Website  (public mirror + Pages deploy)
├── .sg_vault/     → an SG/Send vault                     (encrypted history + live vault)
└── index.html, why/, case-studies/, admin/ …         ← the same files, once

They were not two copies kept in agreement. They were two version control systems pointed at one directory: git status and sgit status described the same bytes. There was no synchronisation step because there was nothing to synchronise; a release was simply two pushes of the same tree:

$ sgit commit -m "site v0.2.1: …" && sgit push   # encrypted history → SG/Send
$ git add -A && git commit && git push            # public mirror → GitHub → Pages

Each remote did a different job. The git push was what deployed, a GitHub Action publishes the tree to GitHub Pages. The sgit push was meant to be what other agents read: the plan was that Claude Code sessions building the site would clone and pull the vault, and that the encrypted store committed to git would double as a full off-site mirror of it.

The boundary that made it safe

The entire pattern rested on one file: .gitignore. Everything encrypted was committed to git, ciphertext objects under .sg_vault/bare/, all under opaque ids. What was excluded was exactly the plaintext tier:

.sg_vault/local/     # vault key, access token, private PEM — plaintext, never committed
.sg_vault/work/      # scratch state
*.pem

That was the whole rule, and it is worth stating as a condition rather than a pattern: committing ciphertext alongside plaintext is right for a public site, where the working tree is meant to be published anyway. For a confidential vault the same layout is a leak-audit boundary, one bad .gitignore edit away from publishing the plaintext tree. The git repos inside vaults↗ page covers which side of that line a given project is on.

Because the boundary was one file, it got a machine check, not a convention. The build's validator read the vault passphrase from the gitignored local/ tier at runtime and scanned every tracked file for it, so a key that reached the tree failed the build by construction. That check exists because of the day it actually happened ↗: an agent hardcoded this site's passphrase into a tracked file, it survived three public commits, and the vault had to be rekeyed. The tripwire outlived the mirror. It now reads the write keys of published demo vaults from a gitignored admin/local/demo-keys/ folder and scans for those instead; the mechanism is unchanged, only the folder moved.

The one ordering rule: sgit first, then git

Almost everything in the shared store is content-addressed, an object's name is the hash of its ciphertext, so it never changes under its id and the two systems can never disagree about it. Exactly one file is mutable: the vault's HEAD pointer, the ref. Every real sgit push rewrites it, and that made the order of the two pushes matter:

✓ sgit commit && sgit push     # 1st — writes the new ref
✓ git add -A && git commit && git push   # 2nd — captures that exact ref

Done in this order, the git mirror always carried the ref the push had just written, and a clean git status was the normal end state of every release. Reverse the order and the failure was quiet: the mirror carried the previous ref, in sync by bytes, one commit behind in meaning.

One property worth keeping from this: the ref's bytes carry no information about staleness. AES-GCM encrypts with a fresh random IV on every write, so a rewritten ref never byte-matches its previous encryption even when it decrypts to the same commit id. "Dirty" did not mean stale, and byte-equality would not have meant current, the only real answers were decrypting the ref or asking sgit status. This is also why .gitattributes marked the store binary -diff -merge: a textual diff of ciphertext is noise, and a git merge of two encrypted refs would produce garbage that decrypts to nothing.

The release script, then and now

Two pushes with no enforcement is an invitation to forget one, so the release was one script, admin/build/release.sh↗, and it was strict: build, validate, sgit push and verify in sync, git push and verify HEAD equals origin, then poll the live site until it served the new version. The same script ships the site today with the sgit step removed. It still refuses to push until the validator, including the key-leak tripwire, has passed, and it still refuses to call a release done until https://sgit.ai/ is serving the version it just pushed.

Why we stopped

Nothing about the pattern broke. What happened is that its costs stayed and its benefits never arrived, and a container recycle made that impossible to ignore.

So on 19 September 2026 the store was removed from the tree, the history was rewritten with git filter-repo to drop it from every commit, and the branch was force-pushed. The mechanics, and why the purged files were still downloadable from GitHub afterwards, are drawn out in a page of their own ↗. The purge is safe for the same reason the mirror was safe: everything removed was ciphertext under a key that was never in the repository. What is gone from GitHub is a copy of the vault's encrypted history; the vault itself, on the SG/Send server, is untouched and still holds it.

The numbers

WhatCount
Releases shipped with both pushes76 (through v0.2.76)
Releases shipped over git alone while the mirror was unreachable7 (v0.2.77 – v0.2.83)
Encrypted objects removed from git15,933 files, 258 MB
Site content that remained~750 files, 24 MB
Times the mirror was cloned to produce a release0
Plaintext ever in the purged historynone, the local/ tier was gitignored from the first commit

What we would do differently

← Case studies↗Git repos inside vaults →↗