--no-recurse-submodules placed before subcommand → fails with exit 129; defense-in-depth gap
Summary
src/core/git-remote.ts declares --no-recurse-submodules as part of GIT_SSRF_FLAGS, which is spread into args before the subcommand (clone or pull). Git rejects --no-recurse-submodules as a top-level flag with exit 129. Both cloneRepo and pullRepo are affected.
Reproduction
$ git --no-recurse-submodules clone --help
unknown option: --no-recurse-submodules
$ git -C some-repo --no-recurse-submodules pull --ff-only
unknown option: --no-recurse-submodules
In a working brain checkout (gbrain 0.30.3):
$ /root/gbrain-env.sh gbrain sync --repo /root/brain --dry-run
Warning: git pull failed: git pull failed in /root/brain: Command failed: git -C /root/brain -c http.followRedirects=false -c
(Truncation in src/commands/sync.ts:428 (msg.slice(0, 100)) hides the real cause; running the same command outside gbrain reveals the unknown option error.)
Impact
- Cron noise. Every gbrain-sync run that does a remote-pull emits a truncated warning, and falls back to "sync from local state".
- Defense-in-depth gap on clone. The comment above
GIT_SSRF_FLAGS says --no-recurse-submodules is meant so that .gitmodules cannot become a second fetch surface. Because the flag is misplaced, cloneRepo actually clones submodules normally if the upstream repo has any. The SSRF posture documented in the comment is not what's running.
Proposed fix
Move --no-recurse-submodules out of GIT_SSRF_FLAGS (which holds top-level -c flags) and into each subcommand's args after the subcommand verb:
/**
- * SSRF-defensive flag set. Used by both cloneRepo and pullRepo.
- * - http.followRedirects=false: closes DNS rebinding via redirect chains
- * - protocol.file.allow=never: no local-file URLs (defense in depth)
- * - protocol.ext.allow=never: no external helpers (`git-remote-foo`)
- * - --no-recurse-submodules: .gitmodules cannot become a second fetch surface
+ * SSRF-defensive flag set. Used by both cloneRepo and pullRepo as top-level
+ * git flags (i.e., before the subcommand). `--no-recurse-submodules` is NOT
+ * a valid top-level git flag — git rejects it with exit 129 — so subcommand
+ * args (clone/pull) append it themselves.
+ * - http.followRedirects=false: closes DNS rebinding via redirect chains
+ * - protocol.file.allow=never: no local-file URLs (defense in depth)
+ * - protocol.ext.allow=never: no external helpers (`git-remote-foo`)
*/
export const GIT_SSRF_FLAGS = [
'-c', 'http.followRedirects=false',
'-c', 'protocol.file.allow=never',
'-c', 'protocol.ext.allow=never',
- '--no-recurse-submodules',
] as const;
- const args: string[] = [...GIT_SSRF_FLAGS, 'clone'];
+ const args: string[] = [...GIT_SSRF_FLAGS, 'clone', '--no-recurse-submodules'];
- const args: string[] = ['-C', repoPath, ...GIT_SSRF_FLAGS, 'pull', '--ff-only'];
+ const args: string[] = ['-C', repoPath, ...GIT_SSRF_FLAGS, 'pull', '--ff-only', '--no-recurse-submodules'];
Test coverage suggestion: test/git-remote.test.ts (or wherever pullRepo / cloneRepo get unit-covered) could spy on execFileSync and assert that --no-recurse-submodules appears AFTER the subcommand name in the args array.
Bonus finding (different bug, related path)
While investigating, I noticed protocol.file.allow=never in GIT_SSRF_FLAGS blocks pullRepo for repos whose origin is a file:// URL (or local bare-repo path). Local-file origins are common for backup mirrors and sneakernet workflows. For cloneRepo the flag is intentional (don't clone untrusted file:// remotes). For pullRepo of an already-trusted, already-cloned repo, the same defense doesn't add much — the origin URL was vetted at clone time.
Possible fix (separate from the main one): split GIT_SSRF_FLAGS_CLONE (full set) and GIT_SSRF_FLAGS_PULL (omit protocol.file.allow=never). Or detect file:///local-path origins in pullRepo and skip the flag for those. Happy to file separately if you want.
Environment
gbrain --version: 0.30.3
- Branch:
garrytan/copenhagen-v3 HEAD c1e2a6d (this branch was used because it has the v0.29.1 effective-date migration fixes that aren't yet on master)
- Bug also present on
origin/master (89ae720, v0.31.0). Same code in git-remote.ts.
- Tested against git
2.x (Ubuntu 24.04 / /usr/bin/git).
--no-recurse-submodulesplaced before subcommand → fails with exit 129; defense-in-depth gapSummary
src/core/git-remote.tsdeclares--no-recurse-submodulesas part ofGIT_SSRF_FLAGS, which is spread into args before the subcommand (cloneorpull). Git rejects--no-recurse-submodulesas a top-level flag with exit 129. BothcloneRepoandpullRepoare affected.Reproduction
In a working brain checkout (
gbrain 0.30.3):$ /root/gbrain-env.sh gbrain sync --repo /root/brain --dry-run Warning: git pull failed: git pull failed in /root/brain: Command failed: git -C /root/brain -c http.followRedirects=false -c(Truncation in
src/commands/sync.ts:428(msg.slice(0, 100)) hides the real cause; running the same command outside gbrain reveals theunknown optionerror.)Impact
GIT_SSRF_FLAGSsays--no-recurse-submodulesis meant so that.gitmodulescannot become a second fetch surface. Because the flag is misplaced,cloneRepoactually clones submodules normally if the upstream repo has any. The SSRF posture documented in the comment is not what's running.Proposed fix
Move
--no-recurse-submodulesout ofGIT_SSRF_FLAGS(which holds top-level-cflags) and into each subcommand's args after the subcommand verb:Test coverage suggestion:
test/git-remote.test.ts(or whereverpullRepo/cloneRepoget unit-covered) could spy onexecFileSyncand assert that--no-recurse-submodulesappears AFTER the subcommand name in the args array.Bonus finding (different bug, related path)
While investigating, I noticed
protocol.file.allow=neverinGIT_SSRF_FLAGSblockspullRepofor repos whoseoriginis afile://URL (or local bare-repo path). Local-file origins are common for backup mirrors and sneakernet workflows. ForcloneRepothe flag is intentional (don't clone untrusted file:// remotes). ForpullRepoof an already-trusted, already-cloned repo, the same defense doesn't add much — the origin URL was vetted at clone time.Possible fix (separate from the main one): split
GIT_SSRF_FLAGS_CLONE(full set) andGIT_SSRF_FLAGS_PULL(omitprotocol.file.allow=never). Or detectfile:///local-path origins inpullRepoand skip the flag for those. Happy to file separately if you want.Environment
gbrain --version:0.30.3garrytan/copenhagen-v3HEADc1e2a6d(this branch was used because it has the v0.29.1 effective-date migration fixes that aren't yet onmaster)origin/master(89ae720, v0.31.0). Same code ingit-remote.ts.2.x(Ubuntu 24.04 //usr/bin/git).