diff --git a/base-action/src/parse-sdk-options.ts b/base-action/src/parse-sdk-options.ts index 1dc5224c5..bb66499c1 100644 --- a/base-action/src/parse-sdk-options.ts +++ b/base-action/src/parse-sdk-options.ts @@ -79,6 +79,24 @@ function mergeMcpConfigs(configValues: string[]): string { return JSON.stringify(merged); } +/** + * Strip shell-style comments from a string. + * Comments start with # at the beginning of a line (after optional whitespace) + * and continue to the end of the line. + * + * This is necessary because shell-quote treats # as a comment character + * and swallows ALL content after it (including newlines), which would + * cause subsequent flags to be lost. + */ +function stripShellComments(input: string): string { + // Match lines that start with optional whitespace followed by # + // and remove them entirely + return input + .split("\n") + .filter((line) => !line.trim().startsWith("#")) + .join("\n"); +} + /** * Parse claudeArgs string into extraArgs record for SDK pass-through * The SDK/CLI will handle --mcp-config, --json-schema, etc. @@ -91,8 +109,12 @@ function parseClaudeArgsToExtraArgs( ): Record { if (!claudeArgs?.trim()) return {}; + // Strip shell-style comments before parsing to prevent shell-quote + // from treating everything after # as a comment + const cleanedArgs = stripShellComments(claudeArgs); + const result: Record = {}; - const args = parseShellArgs(claudeArgs).filter( + const args = parseShellArgs(cleanedArgs).filter( (arg): arg is string => typeof arg === "string", ); diff --git a/base-action/test/parse-sdk-options.test.ts b/base-action/test/parse-sdk-options.test.ts index 175508af3..132b68bcc 100644 --- a/base-action/test/parse-sdk-options.test.ts +++ b/base-action/test/parse-sdk-options.test.ts @@ -312,4 +312,101 @@ describe("parseSdkOptions", () => { expect(result.hasJsonSchema).toBe(true); }); }); + + describe("shell comment handling", () => { + test("should strip shell-style comments from claudeArgs", () => { + const options: ClaudeOptions = { + claudeArgs: `--model 'claude-haiku' +# This is a comment +--allowed-tools 'Edit,Read'`, + }; + + const result = parseSdkOptions(options); + + expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read"]); + expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku"); + }); + + test("should handle multiline comments before flags", () => { + // This is the exact scenario from issue #800 + const options: ClaudeOptions = { + claudeArgs: `--model 'claude-haiku-4-5' +--fallback-model 'claude-sonnet-4-5' + +# Bug workaround: 'mcp__github_*' tools MUST be in the first --allowed-tools flag. +# parseAllowedTools() only reads the first flag. +# https://github.com/anthropics/claude-code-action/issues/800 +--allowed-tools 'mcp__github_inline_comment__create_inline_comment' + +--mcp-config '{"mcpServers": {"context7": {"type": "http"}}}' +--allowed-tools 'mcp__context7__*'`, + }; + + const result = parseSdkOptions(options); + + // All flags should be parsed correctly, comments stripped + expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku-4-5"); + expect(result.sdkOptions.extraArgs?.["fallback-model"]).toBe( + "claude-sonnet-4-5", + ); + expect(result.sdkOptions.allowedTools).toContain( + "mcp__github_inline_comment__create_inline_comment", + ); + expect(result.sdkOptions.allowedTools).toContain("mcp__context7__*"); + expect(result.sdkOptions.extraArgs?.["mcp-config"]).toBeDefined(); + }); + + test("should handle comments containing quoted strings", () => { + const options: ClaudeOptions = { + claudeArgs: `--model 'claude-haiku' +# Note: 'mcp__github_*' must be first +--allowed-tools 'Edit'`, + }; + + const result = parseSdkOptions(options); + + expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku"); + expect(result.sdkOptions.allowedTools).toEqual(["Edit"]); + }); + + test("should handle comments containing flag-like text", () => { + const options: ClaudeOptions = { + claudeArgs: `--model 'claude-haiku' +# Use --allowed-tools to specify tools +--allowed-tools 'Edit'`, + }; + + const result = parseSdkOptions(options); + + // The --allowed-tools in the comment should not be parsed + // Only the actual flag should be parsed + expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku"); + expect(result.sdkOptions.allowedTools).toEqual(["Edit"]); + }); + + test("should handle indented comments", () => { + const options: ClaudeOptions = { + claudeArgs: `--model 'claude-haiku' + # This is an indented comment +--allowed-tools 'Edit'`, + }; + + const result = parseSdkOptions(options); + + expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku"); + expect(result.sdkOptions.allowedTools).toEqual(["Edit"]); + }); + + test("should preserve hash characters inside quoted strings", () => { + // Hash characters inside quotes are NOT comments + const options: ClaudeOptions = { + claudeArgs: `--model 'claude#haiku'`, + }; + + const result = parseSdkOptions(options); + + // The hash inside quotes should be preserved + expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude#haiku"); + }); + }); });