fix: allow + in branch names (generated by Claude Code EnterWorktree) (#1248)

Claude Code's EnterWorktree tool converts "/" to "+" when generating
branch names from worktree names (e.g. EnterWorktree("feat/foo") creates
branch "worktree-feat+foo"). The strict whitelist in validateBranchName
rejected these names, causing claude-code-action to fail on any PR opened
from an EnterWorktree-generated branch.

Since all git calls use execFileSync (not shell interpolation), "+" carries
no command injection risk — the same rationale used for allowing "#".
Git itself permits "+" in branch names per git-check-ref-format.

Fixes: https://github.com/anthropics/claude-code-action/issues/1244

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Naoyoshi Aikawa 2026-04-23 14:17:44 +09:00 committed by GitHub
parent b4d6741327
commit 6ee201f023
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 16 additions and 4 deletions

View File

@ -58,14 +58,16 @@ export function validateBranchName(branchName: string): void {
); );
} }
// Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period/hash. // Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period/hash/plus.
// # is valid per git-check-ref-format and commonly used in branch names like "fix/#123-description". // # is valid per git-check-ref-format and commonly used in branch names like "fix/#123-description".
// All git calls use execFileSync (not shell interpolation), so # carries no injection risk. // + is valid per git-check-ref-format and generated by Claude Code's EnterWorktree tool when
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.#-]*$/; // converting worktree names containing "/" (e.g. "feat/foo" becomes "worktree-feat+foo").
// All git calls use execFileSync (not shell interpolation), so neither # nor + carries injection risk.
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.#+-]*$/;
if (!validPattern.test(branchName)) { if (!validPattern.test(branchName)) {
throw new Error( throw new Error(
`Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, or hashes (#).`, `Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), or plus signs (+).`,
); );
} }

View File

@ -45,6 +45,16 @@ describe("validateBranchName", () => {
).not.toThrow(); ).not.toThrow();
expect(() => validateBranchName("fix/issue-#42")).not.toThrow(); expect(() => validateBranchName("fix/issue-#42")).not.toThrow();
}); });
it("should accept branch names containing + (generated by Claude Code EnterWorktree)", () => {
// EnterWorktree converts "/" in worktree names to "+" when generating branch names.
// e.g. EnterWorktree("feat/skill-consolidation") → branch "worktree-feat+skill-consolidation"
expect(() =>
validateBranchName("worktree-feat+skill-consolidation"),
).not.toThrow();
expect(() => validateBranchName("fix+issue-123")).not.toThrow();
expect(() => validateBranchName("feature+new-thing")).not.toThrow();
});
}); });
describe("command injection attempts", () => { describe("command injection attempts", () => {