From 204341c77adae71661772fbac0c7e87aca96fcec Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 28 Mar 2026 21:20:12 -0700 Subject: [PATCH] Restore .git pointer file backup for agents that delete it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The worktree lock (#144) protects .git/worktrees/NAME/ from gc pruning. But agents with Bash/Write access can still delete the .git POINTER FILE from the worktree directory itself. This backup/restore catches that case. Both protections together: - Lock → prevents gc from pruning metadata directory - Backup → restores pointer file if agent deletes it Co-Authored-By: Claude Opus 4.6 (1M context) --- src/runners/claude-code.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/runners/claude-code.ts b/src/runners/claude-code.ts index 0ebff72..91280b2 100644 --- a/src/runners/claude-code.ts +++ b/src/runners/claude-code.ts @@ -1,4 +1,6 @@ import { spawn } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; import type { AgentResult } from "../types.js"; import { getDiff, getDiffStats } from "../utils/git.js"; import type { Runner, RunnerOptions } from "./base.js"; @@ -20,6 +22,17 @@ export const claudeCodeRunner: Runner = { async run(id: number, opts: RunnerOptions): Promise { const start = Date.now(); + // Backup the .git pointer file. Agents can delete it via Bash/Write tools. + // The lock (in createWorktree) protects the metadata directory in .git/worktrees/, + // but we also need to restore the pointer file if the agent removed it. + const gitFilePath = join(opts.worktreePath, ".git"); + let gitFileBackup: string | null = null; + try { + gitFileBackup = await readFile(gitFilePath, "utf-8"); + } catch { + // Not a worktree or .git is a directory + } + return new Promise((resolve) => { let output = ""; let error = ""; @@ -94,6 +107,17 @@ export const claudeCodeRunner: Runner = { if (settled) return; settled = true; + // Restore .git pointer file if the agent deleted it during execution. + // The worktree lock protects .git/worktrees/NAME/ from gc pruning, + // but the agent can still delete the .git file in its own directory. + if (gitFileBackup) { + try { + await readFile(gitFilePath, "utf-8"); + } catch { + await writeFile(gitFilePath, gitFileBackup).catch(() => {}); + } + } + const duration = Date.now() - start; const diff = await getDiff(opts.worktreePath); const stats = await getDiffStats(opts.worktreePath);