cli.sgit.ai / Research / Partial clones — a folder scope and a history depth (6 Oct 2026)

Partial clones — a folder scope and a history depth (6 Oct 2026)

Date2026-10-06 · 23:01 UTCKinddesignTopicsclone partial-clones agentsWritten bythe sgit-ai CLI team (Claude Code sessions), for Dinis CruzSourceteam/humans/dinis_cruz/claude-code-web/10/06/partial-clones__scope-and-depth.md in SGit-AI__CLI at 5597526. Published here as written; the CLI repository is where the work happened.

Branch: claude/charming-babbage-5hzcxq · Vault measured: y8pwtjlw (626 commits, 8,013+ trees, 9,404 blobs, 18,720 objects at HEAD time) Ask: agents that know which folders they read and write should be able to clone just those, read and write; plus a shallow (no-history) mode; and none of it may change the full clone.

1. The idea, in one paragraph

The vault is a Merkle tree: a commit names a root tree, a tree names its children by id. To hold mail/crm.riskmandate/ a clone needs the root tree, the mail/ tree (the spine) and the folder's own subtree; everything else is an entry it keeps by id ("opaque"). To commit a change inside the folder, rebuild the folder's subtree from disk, then each tree on the spine with one entry replaced, and keep the siblings' ids — exactly what the whole-vault builder would produce, byte for byte, because tree encryption is deterministic (a test proves the rebuilt root id equals the full builder's). Push uploads the new commit, the spine trees, the changed subtree and the new blobs; the server needs nothing new. Two scoped clones in different folders cannot conflict: out-of-scope entries are always the remote's, by id.

2. What was built

piecewhere
scope model (paths + boundaries, pure path logic)storage/Vault__Scope.py (re-exported from core/scope/)
opaque entries in the tree builder (build, build_from_flat, opaque=), canonical entry orderstorage/Vault__Sub_Tree.py
scoped flatten → (flat, opaque); level-by-level fetch walk that only opens held + spine foldersstorage/Vault__Scoped_Tree.py
local config: scope_paths, shallow_boundaries (empty on full clones)schemas/Schema__Local_Config.py
clone: --depth N, --path F (repeatable); depth-limited commit walk records boundaries; scoped tree walk; scoped checkout; bulk sweep skipped for partial clonesclone steps, Vault__Sync__Clone, CLI__Main/CLI__Vault
commit / write_file: scoped flatten + opaque rebuild; writes outside scope refused with the pathVault__Sync__Commit
status: scoped diff; chain fetch stops at boundariesVault__Sync__Status
pull: scoped fetch (held folders only), merge within scope with theirs' opaque entries, commit walk stops at boundaries (push's implicit pull used to deepen a shallow clone to the root)Step__Pull__Fetch_Missing, Step__Pull__Merge, Vault__Sync__Pull
push: scoped flatten, trees/blobs the clone never fetched are skipped (unchanged, on the server), cache reconcile skippedVault__Sync__Push, Vault__Batch
widen (sgit fetch <folder>), unshallow (sgit fetch --unshallow), require_wholecore/actions/scope/Vault__Sync__Scope.py
guard on check fsck, dump, publish, vault movecli/CLI__Scope_Guard.py
branch switch keeps scope fieldsVault__Branch_Switch

3. Numbers on the real vault (this session, through its egress gateway)

cloneobjectsstorefileswall
full (bulk sweep, unchanged)18,720173 MB3,44281 s
--path mail/crm.riskmandate --depth 14293.2 MB36514 s
--path docs --path conductor --depth 120—155 s
widen the CRM clone with docs+10—+82 s

On the scoped clone: status in sync; history log shows HEAD and stops; check fsck refuses naming what the clone holds and how to widen.

4. "No side effects on the full clone" — how that was held

5. Caveats and follow-ups

6. Deep review before the PR to main (same day, evening)

Two passes over origin/main...origin/dev after the merge: a code review (bugs, side effects) and a security review (new exploitable issues only, >80 % confidence). CI on dev was green first (run 37544894784 on ddee8a4).

Security review — no new vulnerability; three hardening gaps closed

No high-confidence exploitable issue was introduced. Three pre-existing "trust the host / trust the committer" gaps that the new fetch paths exercise more were closed anyway:

GapFixTest
A batch read wrote whatever file_id the host answered with, and the id names the on-disk path_batch_read_chunk keeps only requested idsTest_API__Ignores_Unrequested_File_Ids (real local HTTP server answering with local/config.json and an extra object)
Vault__Verified_Write.save accepted any contained path (local/config.json, a working-copy file)store-leaf allow-list STORE_FILE_ID + protected-dir check, before the content-address checkTest_Verified_Write__Store_Paths_Only
Pull let an untracked file be overwritten when the tree entry's content_hash CLAIMED the same contentproven from the decrypted blob (blob_hash_fn), the claim is ignoredtest_commit_whose_entry_hash_lies_cannot_overwrite_an_untracked_file (a commit built with a lying hash)

Code review — 15 findings, what changed

#FindingResolution
1Vault__Head_Paths flattened the whole tree on a scoped clone → tracked-wins rule broke, a tracked file under an ignored dir was dropped by the next commitscoped flatten; test
2read-only scoped clones: status/checkout used the whole treescoped in _status_read_only and the RO checkout step; test
3sparse push could try to load a blob it never fetchedcollector skips any blob not local
4sgit ls/fetch/cat on a scoped clone walked a sibling's tree and failed_get_head_flat_map scoped; test
5other commands crash with "vault may be corrupted — run fsck" on a partial clonefriendly error naming the scope + how to widen (generic, for every command); test
6branch-only push re-uploaded the whole clone-branch history each timestops at the server's clone head; blobs the server has are not re-sent — and the ref CAS was matching the LOCAL ref bytes, so the ref write conflicted on every push after a commit (silent on the in-memory API, a 409 on the real one): now matches the server's bytes; test checks the server ref and that nothing re-sent is a blob
7status walked the whole local history on every callwalk stops at known-complete heads; test (0 object reads when up to date; N when N behind); the truncated-fetch case is covered by the existing lower-bound test
8widen overwrote files already on disk under the new folderrefuses naming the clash; held folders untouched; test
9one failing bulk-sweep chunk aborted the clonefail-soft per chunk; test
10sweep counters unsynchronised across threadslock + summed futures
11duplicate parents counted against the fetch limitdeduped
12CLI__Scope_Guard duplicated require_wholedelegates
13pull-step call shapekept (the fake-client tests prove the full path is unchanged)
14guard re-read the configconfig read once in the merge step, passed through
15Vault__Scope held raw list[str]Safe_Str__File_Path / Safe_Str__Commit_Id with validation (.., absolute, non-object ids refused; nested folders collapse)

What the new tests then found (would have shipped otherwise)

pytest tests/unit/ -n auto: 3,953 passed (21 new review tests under tests/unit/review/).

Follow-up (7 Oct): the merge of main into dev and a CI flake that was a real leak

Merging main into dev for PR #7 conflicted only on the release bot's version bumps (dev v0.17.3, main v0.17.0; dev kept). CI on that merge then failed on test_Dev__Tree__Graph__Happy::test_analyse_temp_dir_cleaned_up, which diffed /tmp before and after analyse() and so could see any vault another xdist worker was creating at that moment. Rewriting it to watch the mkdtemp calls made inside the call exposed something real: every sgit clone (all five entry points) creates /tmp/sgit-clone-* as the workflow workspace's root, the runner removes the workspace inside it on success, and the root stays — one empty dir per clone, 3,881 of them in this sandbox. _run_clone_workflow now removes the root in its finally, success or failure; a review test clones three ways and checks none of the roots survive.

This page as markdown · its source, admin/content/articles/partial-clones-scope-and-depth.md