Full-opacity redesign: no gitlinks, encrypted manifest holds all submodule state
Motivation
The current design hashes directory names and encrypts the manifest, but still leaves a gitlink (mode 160000 + SHA) visible in the git tree. A public observer of a repo using modules sees:
submodules/ directory (reveals the feature is in use)
- One hashed dir per module (reveals count of modules)
- A gitlink SHA per module (reveals the pinned commit of each module)
The SHA is the leaky bit. If the upstream repo is public, anyone can search GitHub for that commit hash and resolve it back to the upstream repo — the hashed dir name buys nothing.
Target use case: public home repo with private dependency graph. An observer should learn nothing about dependencies beyond "this repo uses modules."
Proposal
Git tracks nothing under submodules/. No gitlinks, no hashed dirs. All submodule state lives in the encrypted manifest. submodules/ becomes pure local state.
What's in git
.modules/manifest — encrypted JSON (git-crypt). The only file that reveals modules are in use.
.gitignore entry for submodules/.
What's in the manifest
{
"fold": {
"url": "https://github.com/ricon-family/fold.git",
"pin": "4feed004a2ae5ba710b7ae643847ab92d5669086"
},
"den": {
"url": "https://github.com/ricon-family/den.git",
"pin": "321bf53..."
}
}
The path field is gone — path is always submodules/<name>/ locally.
What's on disk (after modules unlock && modules init)
submodules/
├── fold/ ← real clone, readable, gitignored
└── den/ ← real clone, readable, gitignored
No hashing. No symlinks. cd submodules/fold just works.
Command semantics
| Command |
Behavior |
modules setup |
creates .modules/manifest, gitignores submodules/, installs hooks, assigns manifest to git-crypt via rudi |
modules add <url> [--name n] [--ref r] |
clones to submodules/<name>/, records url+pin in manifest |
modules init |
reads manifest, clones each entry + checks out pin |
modules update [name] |
fetches + bumps manifest pin to local HEAD |
modules status |
compares local HEADs to manifest pins (drift detection, replaces what gitlinks gave us for free) |
modules remove <name> |
rm clone dir, drop from manifest |
modules lock / unlock |
thin wrappers around rudi lock / unlock |
What GitHub observers see (locked)
.modules/manifest ← encrypted, unreadable
.gitignore ← contains "submodules/"
Presence of the feature leaks. Nothing else — no names, no SHAs, no count.
What we lose + how we replace it
Git's built-in "submodule pin drift" signal in git status. Replacement: pre-commit hook runs modules status and warns if any submodules/<name>/ HEAD ≠ manifest pin. Same safety net, implemented by the tool.
Migration
Existing repos (den, fold clones in the wild) need a one-time modules migrate task:
- Read old manifest (with
path + pin).
- For each entry: move
submodules/<hash>/ → submodules/<name>/, remove gitlink from index.
- Rewrite manifest to drop
path field.
- Move manifest:
submodules/.manifest → .modules/manifest.
- Update
.gitignore: add submodules/.
- Update
.gitattributes (rudi assign): repoint git-crypt from submodules/.manifest to .modules/manifest.
One commit. Reversible if we keep the old manifest format as a backup.
Open questions
- Manifest location —
.modules/manifest (dedicated namespace, mirrors .git/) vs modules.lock at root vs .modules.encrypted. Leaning .modules/manifest.
- Pin drift enforcement — pre-commit hook should warn by default, with a strict mode available as an opt-in. Alternative: always block, with an explicit override flag.
- Name conflicts — today hashing saves you from
modules add fold twice. With readable names, we error on duplicate. Calling it out; fine as-is.
- Breaking change — only den and fold use this in production. Migration task makes it a single commit. Propose landing v1.0.0 with the new design + migration in the same release.
Non-goals
- Hiding the presence of modules (the encrypted manifest itself is a signal). Accepted — can't hide that a feature is in use, only its contents.
- Supporting both old (hashed + gitlink) and new (manifest-only) modes. Two modes is long-term complexity. Clean break + migration.
Related
Full-opacity redesign: no gitlinks, encrypted manifest holds all submodule state
Motivation
The current design hashes directory names and encrypts the manifest, but still leaves a gitlink (mode 160000 + SHA) visible in the git tree. A public observer of a repo using
modulessees:submodules/directory (reveals the feature is in use)The SHA is the leaky bit. If the upstream repo is public, anyone can search GitHub for that commit hash and resolve it back to the upstream repo — the hashed dir name buys nothing.
Target use case: public home repo with private dependency graph. An observer should learn nothing about dependencies beyond "this repo uses modules."
Proposal
Git tracks nothing under
submodules/. No gitlinks, no hashed dirs. All submodule state lives in the encrypted manifest.submodules/becomes pure local state.What's in git
.modules/manifest— encrypted JSON (git-crypt). The only file that reveals modules are in use..gitignoreentry forsubmodules/.What's in the manifest
{ "fold": { "url": "https://github.com/ricon-family/fold.git", "pin": "4feed004a2ae5ba710b7ae643847ab92d5669086" }, "den": { "url": "https://github.com/ricon-family/den.git", "pin": "321bf53..." } }The
pathfield is gone — path is alwayssubmodules/<name>/locally.What's on disk (after
modules unlock && modules init)No hashing. No symlinks.
cd submodules/foldjust works.Command semantics
modules setup.modules/manifest, gitignoressubmodules/, installs hooks, assigns manifest to git-crypt via rudimodules add <url> [--name n] [--ref r]submodules/<name>/, records url+pin in manifestmodules initmodules update [name]modules statusmodules remove <name>modules lock / unlockrudi lock / unlockWhat GitHub observers see (locked)
Presence of the feature leaks. Nothing else — no names, no SHAs, no count.
What we lose + how we replace it
Git's built-in "submodule pin drift" signal in
git status. Replacement: pre-commit hook runsmodules statusand warns if anysubmodules/<name>/HEAD ≠ manifest pin. Same safety net, implemented by the tool.Migration
Existing repos (den, fold clones in the wild) need a one-time
modules migratetask:path+pin).submodules/<hash>/→submodules/<name>/, remove gitlink from index.pathfield.submodules/.manifest→.modules/manifest..gitignore: addsubmodules/..gitattributes(rudi assign): repoint git-crypt fromsubmodules/.manifestto.modules/manifest.One commit. Reversible if we keep the old manifest format as a backup.
Open questions
.modules/manifest(dedicated namespace, mirrors.git/) vsmodules.lockat root vs.modules.encrypted. Leaning.modules/manifest.modules add foldtwice. With readable names, we error on duplicate. Calling it out; fine as-is.Non-goals
Related