From b34678488e9c8b0e35866d6a43e3521e632ea920 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:39:10 -0700 Subject: [PATCH] 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 --- .../checkpoints/ShadowCheckpointService.ts | 5 +- .../__tests__/ShadowCheckpointService.spec.ts | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/services/checkpoints/ShadowCheckpointService.ts b/src/services/checkpoints/ShadowCheckpointService.ts index bd44afb358..89ae52c435 100644 --- a/src/services/checkpoints/ShadowCheckpointService.ts +++ b/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") diff --git a/src/services/checkpoints/__tests__/ShadowCheckpointService.spec.ts b/src/services/checkpoints/__tests__/ShadowCheckpointService.spec.ts index 92bf1f8e7d..5bc43d54ce 100644 --- a/src/services/checkpoints/__tests__/ShadowCheckpointService.spec.ts +++ b/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.