feat: add claudeArgs input for direct CLI argument passing

- Add claude_args input to both action.yml files
- Implement shell-style argument parsing with quote handling
- Pass arguments directly to Claude CLI for maximum flexibility
- Add comprehensive tests for argument parsing
- Log custom arguments for debugging

Users can now pass any Claude CLI arguments directly:
  claude_args: '--max-turns 3 --mcp-config /path/to/config.json'

This provides power users full control over Claude's behavior without
waiting for specific inputs to be added to the action.
This commit is contained in:
km-anthropic
2025-08-07 14:01:27 -07:00
parent 36c720c2db
commit dfcaac854e
5 changed files with 212 additions and 0 deletions

View File

@@ -61,6 +61,10 @@ inputs:
description: "Timeout in minutes for Claude Code execution"
required: false
default: "10"
claude_args:
description: "Additional arguments to pass directly to Claude CLI (e.g., '--max-turns 3 --mcp-config /path/to/config.json')"
required: false
default: ""
experimental_slash_commands_dir:
description: "Experimental: Directory containing slash command files to install"
required: false

View File

@@ -31,6 +31,7 @@ async function run() {
claudeEnv: process.env.INPUT_CLAUDE_ENV,
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
model: process.env.ANTHROPIC_MODEL,
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
});
} catch (error) {
core.setFailed(`Action failed with error: ${error}`);

View File

@@ -22,6 +22,7 @@ export type ClaudeOptions = {
fallbackModel?: string;
timeoutMinutes?: string;
model?: string;
claudeArgs?: string;
};
type PreparedConfig = {
@@ -30,6 +31,67 @@ type PreparedConfig = {
env: Record<string, string>;
};
/**
* Parse shell-style arguments string into array.
* Handles quoted strings and escaping.
*/
function parseShellArgs(argsString?: string): string[] {
if (!argsString || argsString.trim() === "") {
return [];
}
const args: string[] = [];
let current = "";
let inSingleQuote = false;
let inDoubleQuote = false;
let escapeNext = false;
for (let i = 0; i < argsString.length; i++) {
const char = argsString[i];
if (escapeNext) {
current += char;
escapeNext = false;
continue;
}
if (char === "\\") {
if (inSingleQuote) {
current += char;
} else {
escapeNext = true;
}
continue;
}
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
continue;
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
continue;
}
if (char === " " && !inSingleQuote && !inDoubleQuote) {
if (current) {
args.push(current);
current = "";
}
continue;
}
current += char;
}
if (current) {
args.push(current);
}
return args;
}
function parseCustomEnvVars(claudeEnv?: string): Record<string, string> {
if (!claudeEnv || claudeEnv.trim() === "") {
return {};
@@ -66,8 +128,10 @@ export function prepareRunConfig(
promptPath: string,
options: ClaudeOptions,
): PreparedConfig {
// Start with base args
const claudeArgs = [...BASE_ARGS];
// Add specific options first (these can be overridden by claudeArgs)
if (options.allowedTools) {
claudeArgs.push("--allowedTools", options.allowedTools);
}
@@ -107,6 +171,19 @@ export function prepareRunConfig(
}
}
// Parse and append custom arguments (these can override the above)
if (options.claudeArgs) {
const customArgs = parseShellArgs(options.claudeArgs);
// Filter out -p flag since we handle prompt via pipe
const filteredArgs = customArgs.filter(
(arg, index) =>
arg !== "-p" &&
arg !== "--prompt" &&
(index === 0 || (customArgs[index - 1] !== "-p" && customArgs[index - 1] !== "--prompt")),
);
claudeArgs.push(...filteredArgs);
}
// Parse custom environment variables
const customEnv = parseCustomEnvVars(options.claudeEnv);
@@ -147,8 +224,14 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
console.log(`Custom environment variables: ${envKeys}`);
}
// Log custom arguments if any
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
console.log(`Custom Claude arguments: ${options.claudeArgs}`);
}
// Output to console
console.log(`Running Claude with prompt from file: ${config.promptPath}`);
console.log(`Full command: claude ${config.claudeArgs.join(" ")}`);
// Start sending prompt to pipe in background
const catProcess = spawn("cat", [config.promptPath], {

View File

@@ -0,0 +1,119 @@
import { describe, expect, test } from "bun:test";
// Import the function directly from run-claude.ts for testing
// We'll need to export it first
function parseShellArgs(argsString?: string): string[] {
if (!argsString || argsString.trim() === "") {
return [];
}
const args: string[] = [];
let current = "";
let inSingleQuote = false;
let inDoubleQuote = false;
let escapeNext = false;
for (let i = 0; i < argsString.length; i++) {
const char = argsString[i];
if (escapeNext) {
current += char;
escapeNext = false;
continue;
}
if (char === "\\") {
if (inSingleQuote) {
current += char;
} else {
escapeNext = true;
}
continue;
}
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
continue;
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
continue;
}
if (char === " " && !inSingleQuote && !inDoubleQuote) {
if (current) {
args.push(current);
current = "";
}
continue;
}
current += char;
}
if (current) {
args.push(current);
}
return args;
}
describe("parseShellArgs", () => {
test("should handle empty input", () => {
expect(parseShellArgs("")).toEqual([]);
expect(parseShellArgs(undefined)).toEqual([]);
expect(parseShellArgs(" ")).toEqual([]);
});
test("should parse simple arguments", () => {
expect(parseShellArgs("--max-turns 3")).toEqual(["--max-turns", "3"]);
expect(parseShellArgs("-a -b -c")).toEqual(["-a", "-b", "-c"]);
});
test("should handle double quotes", () => {
expect(parseShellArgs('--config "/path/to/config.json"')).toEqual([
"--config",
"/path/to/config.json",
]);
expect(parseShellArgs('"arg with spaces"')).toEqual(["arg with spaces"]);
});
test("should handle single quotes", () => {
expect(parseShellArgs("--config '/path/to/config.json'")).toEqual([
"--config",
"/path/to/config.json",
]);
expect(parseShellArgs("'arg with spaces'")).toEqual(["arg with spaces"]);
});
test("should handle escaped characters", () => {
expect(parseShellArgs("arg\\ with\\ spaces")).toEqual(["arg with spaces"]);
expect(parseShellArgs('arg\\"with\\"quotes')).toEqual(['arg"with"quotes']);
});
test("should handle mixed quotes", () => {
expect(parseShellArgs(`--msg "It's a test"`)).toEqual([
"--msg",
"It's a test",
]);
expect(parseShellArgs(`--msg 'He said "hello"'`)).toEqual([
"--msg",
'He said "hello"',
]);
});
test("should handle complex real-world example", () => {
const input = `--max-turns 3 --mcp-config "/Users/john/config.json" --model claude-3-5-sonnet-latest --system-prompt 'You are helpful'`;
expect(parseShellArgs(input)).toEqual([
"--max-turns",
"3",
"--mcp-config",
"/Users/john/config.json",
"--model",
"claude-3-5-sonnet-latest",
"--system-prompt",
"You are helpful",
]);
});
});