Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion base-action/src/parse-sdk-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -91,8 +109,12 @@ function parseClaudeArgsToExtraArgs(
): Record<string, string | null> {
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<string, string | null> = {};
const args = parseShellArgs(claudeArgs).filter(
const args = parseShellArgs(cleanedArgs).filter(
(arg): arg is string => typeof arg === "string",
);

Expand Down
97 changes: 97 additions & 0 deletions base-action/test/parse-sdk-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});