Browse Source

fix: prevent git templates from leaking into shadow checkpoint repos (#8629)

Pass --template="" to git init and strip GIT_TEMPLATE_DIR from the
environment so system/user git hooks and other template files never
get copied into the shadow repository used for checkpoints.

Co-authored-by: Roo Code <[email protected]>
roomote[bot] 1 month ago
parent
commit
b34678488e

+ 3 - 2
src/services/checkpoints/ShadowCheckpointService.ts

@@ -39,7 +39,8 @@ function createSanitizedGit(baseDir: string): SimpleGit {
 			key === "GIT_INDEX_FILE" ||
 			key === "GIT_OBJECT_DIRECTORY" ||
 			key === "GIT_ALTERNATE_OBJECT_DIRECTORIES" ||
-			key === "GIT_CEILING_DIRECTORIES"
+			key === "GIT_CEILING_DIRECTORIES" ||
+			key === "GIT_TEMPLATE_DIR"
 		) {
 			removedVars.push(`${key}=${value}`)
 			continue
@@ -172,7 +173,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {
 			this.baseHash = await git.revparse(["HEAD"])
 		} else {
 			this.log(`[${this.constructor.name}#initShadowGit] creating shadow git repo at ${this.checkpointsDir}`)
-			await git.init()
+			await git.init({ "--template": "" })
 			await git.addConfig("core.worktree", this.workspaceDir) // Sets the working tree to the current workspace.
 			await git.addConfig("commit.gpgSign", "false") // Disable commit signing for shadow repo.
 			await git.addConfig("user.name", "Roo Code")

+ 49 - 0
src/services/checkpoints/__tests__/ShadowCheckpointService.spec.ts

@@ -824,6 +824,55 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
 				expect(await fs.readFile(testFile, "utf-8")).toBe("Hello, world!")
 			})
 
+			it("does not apply git templates when initializing shadow repo", async () => {
+				// This test verifies that git init uses --template="" and GIT_TEMPLATE_DIR
+				// is stripped, preventing system/user git hooks from leaking into the shadow repo.
+				const templateDir = path.join(tmpDir, `git-template-${Date.now()}`)
+				const hooksDir = path.join(templateDir, "hooks")
+				await fs.mkdir(hooksDir, { recursive: true })
+				await fs.writeFile(path.join(hooksDir, "pre-commit"), "#!/bin/sh\nexit 1", { mode: 0o755 })
+
+				const testShadowDir = path.join(tmpDir, `shadow-template-test-${Date.now()}`)
+				const testWorkspaceDir = path.join(tmpDir, `workspace-template-test-${Date.now()}`)
+				await initWorkspaceRepo({ workspaceDir: testWorkspaceDir })
+
+				const originalTemplateDir = process.env.GIT_TEMPLATE_DIR
+				process.env.GIT_TEMPLATE_DIR = templateDir
+
+				try {
+					const testService = await klass.create({
+						taskId: `test-template-${Date.now()}`,
+						shadowDir: testShadowDir,
+						workspaceDir: testWorkspaceDir,
+						log: () => {},
+					})
+					await testService.initShadowGit()
+
+					// Verify no hooks were copied from the template
+					const shadowHooksDir = path.join(testShadowDir, ".git", "hooks")
+					let hookFiles: string[] = []
+
+					try {
+						hookFiles = await fs.readdir(shadowHooksDir)
+					} catch {
+						// hooks dir may not exist at all, which is fine
+					}
+
+					// The pre-commit hook from the template should NOT be present
+					expect(hookFiles).not.toContain("pre-commit")
+				} finally {
+					if (originalTemplateDir !== undefined) {
+						process.env.GIT_TEMPLATE_DIR = originalTemplateDir
+					} else {
+						delete process.env.GIT_TEMPLATE_DIR
+					}
+
+					await fs.rm(testShadowDir, { recursive: true, force: true })
+					await fs.rm(testWorkspaceDir, { recursive: true, force: true })
+					await fs.rm(templateDir, { recursive: true, force: true })
+				}
+			})
+
 			it("isolates checkpoint operations from GIT_DIR environment variable", async () => {
 				// This test verifies the fix for the issue where GIT_DIR environment variable
 				// causes checkpoint commits to go to the wrong repository.