fix(cleanup): keep the base-branch config revert out of the auto-commit (#1677)

restoreConfigFromBase replaces .claude/, CLAUDE.md and the other sensitive
paths with the PR base branch's versions, then deliberately unstages them so
the revert does not reach a commit. checkAndCommitOrDeleteBranch then ran a
bare `git add -A`, which staged them again and pushed a silent revert of the
PR author's own config onto their branch, under a commit message that says
only "Auto-commit: Save uncommitted changes from Claude".

restoreConfigFromBase now returns the paths it restored. run.ts threads them
through updateCommentLink into checkAndCommitOrDeleteBranch, which excludes
them via pathspec from both the staging and the git status check.

The exclusion is driven by what was actually restored rather than applied
unconditionally. This path also runs for issues, where no restore happens and
Claude may legitimately have been asked to edit CLAUDE.md or
.claude/settings.json; excluding those there would silently drop the work —
trading one silent-data-loss bug for another. Reverting the fix, dropping the
status scoping, and switching to an unconditional exclusion each fail the new
tests.

The status check is scoped the same way as the staging: when the reverted
config is the only dirty entry there is no real work, so the branch is now
correctly treated as empty and deleted instead of receiving a pure revert.

Reachable on a closed or merged PR where Claude left uncommitted changes with
use_commit_signing false — the only combination where a restore has run and
claudeBranch is set.

Fixes #1669
This commit is contained in:
Gautam Sharma
2026-08-20 16:17:03 -07:00
committed by GitHub
parent 39ad3c8977
commit 6a5f1d8e0a
5 changed files with 314 additions and 6 deletions
+29 -3
View File
@@ -9,10 +9,32 @@ export async function checkAndCommitOrDeleteBranch(
claudeBranch: string | undefined,
baseBranch: string,
useCommitSigning: boolean,
restoredConfigPaths: string[] = [],
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
let branchLink = "";
let shouldDeleteBranch = false;
// On pull requests, restoreConfigFromBase replaces .claude/, CLAUDE.md and
// friends with the base branch's versions and leaves them unstaged so the
// revert does not reach a commit. Auto-committing with a bare `git add -A`
// would stage them anyway and push a silent revert of the PR author's own
// config onto their branch.
//
// The exclusion is driven by what was actually restored rather than applied
// unconditionally: this path also runs for issues, where no restore happens
// and Claude may legitimately have been asked to edit CLAUDE.md or
// .claude/settings.json. Excluding those there would silently drop the work.
const pathspecArgs =
restoredConfigPaths.length > 0
? ["--", ".", ...restoredConfigPaths.map((p) => `:(exclude)${p}`)]
: [];
if (pathspecArgs.length > 0) {
console.log(
`Excluding base-restored config from auto-commit: ${restoredConfigPaths.join(", ")}`,
);
}
if (claudeBranch) {
// First check if the branch exists remotely
let branchExistsRemotely = false;
@@ -57,15 +79,19 @@ export async function checkAndCommitOrDeleteBranch(
// Check for uncommitted changes using git status
try {
const gitStatus = await $`git status --porcelain`.quiet();
// Scoped the same way as the staging below: if the restored config
// is the only dirty entry there is no real work, and the branch
// should be treated as empty rather than receiving a pure revert.
const gitStatus =
await $`git status --porcelain ${pathspecArgs}`.quiet();
const hasUncommittedChanges =
gitStatus.stdout.toString().trim().length > 0;
if (hasUncommittedChanges) {
console.log("Found uncommitted changes, committing them...");
// Add all changes
await $`git add -A`;
// Add all changes, minus anything restored from the base branch
await $`git add -A ${pathspecArgs}`;
// Commit with a descriptive message
const runId = process.env.GITHUB_RUN_ID || "unknown";