diff --git a/src/github/operations/branch.ts b/src/github/operations/branch.ts index 870cd649c..ab5872286 100644 --- a/src/github/operations/branch.ts +++ b/src/github/operations/branch.ts @@ -28,7 +28,7 @@ function extractFirstLabel(githubData: FetchDataResult): string | undefined { * * Valid branch names: * - Start with alphanumeric character (not dash, to prevent option injection) - * - Contain only alphanumeric, forward slash, hyphen, underscore, period, or hash (#) + * - Contain only alphanumeric, forward slash, hyphen, underscore, period, hash (#), plus (+), or at-sign (@) * - Do not start or end with a period * - Do not end with a slash * - Do not contain '..' (path traversal) @@ -58,16 +58,19 @@ export function validateBranchName(branchName: string): void { ); } - // Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period/hash/plus. + // Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period/hash/plus/at-sign. // # 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 generated by Claude Code's EnterWorktree tool when // 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/_.#+-]*$/; + // @ is valid per git-check-ref-format and generated by multi-agent orchestration tools like + // Gastown (https://github.com/gastownhall/gastown) which produce names like "polecat/name@sessionid". + // Note: "@{" is still rejected below (git reflog syntax); bare "@" carries no injection risk since + // all git calls use execFileSync (not shell interpolation). + const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.#+@-]*$/; if (!validPattern.test(branchName)) { 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, hashes (#), or plus signs (+).`, + `Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), plus signs (+), or at-signs (@).`, ); } diff --git a/test/validate-branch-name.test.ts b/test/validate-branch-name.test.ts index 9594b48d9..894d33021 100644 --- a/test/validate-branch-name.test.ts +++ b/test/validate-branch-name.test.ts @@ -55,6 +55,16 @@ describe("validateBranchName", () => { expect(() => validateBranchName("fix+issue-123")).not.toThrow(); expect(() => validateBranchName("feature+new-thing")).not.toThrow(); }); + + it("should accept branch names containing @ (generated by multi-agent tools like Gastown)", () => { + // Gastown (https://github.com/gastownhall/gastown) generates branch names like + // "polecat/name/project@sessionid" where "@" separates the task name from a session ID. + expect(() => + validateBranchName("polecat/nux/bo-gpl@moxcd5jj"), + ).not.toThrow(); + expect(() => validateBranchName("agent/task@abc123")).not.toThrow(); + expect(() => validateBranchName("feature@session-id")).not.toThrow(); + }); }); describe("command injection attempts", () => {