mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
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:
@@ -89,6 +89,10 @@ inputs:
|
|||||||
description: "Timeout in minutes for execution"
|
description: "Timeout in minutes for execution"
|
||||||
required: false
|
required: false
|
||||||
default: "30"
|
default: "30"
|
||||||
|
claude_args:
|
||||||
|
description: "Additional arguments to pass directly to Claude CLI"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
use_sticky_comment:
|
use_sticky_comment:
|
||||||
description: "Use just one comment to deliver issue/PR comments"
|
description: "Use just one comment to deliver issue/PR comments"
|
||||||
required: false
|
required: false
|
||||||
@@ -193,6 +197,7 @@ runs:
|
|||||||
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
|
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
|
||||||
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
|
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
|
||||||
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
|
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
|
||||||
|
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||||
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ inputs:
|
|||||||
description: "Timeout in minutes for Claude Code execution"
|
description: "Timeout in minutes for Claude Code execution"
|
||||||
required: false
|
required: false
|
||||||
default: "10"
|
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:
|
experimental_slash_commands_dir:
|
||||||
description: "Experimental: Directory containing slash command files to install"
|
description: "Experimental: Directory containing slash command files to install"
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ async function run() {
|
|||||||
claudeEnv: process.env.INPUT_CLAUDE_ENV,
|
claudeEnv: process.env.INPUT_CLAUDE_ENV,
|
||||||
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
|
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
|
||||||
model: process.env.ANTHROPIC_MODEL,
|
model: process.env.ANTHROPIC_MODEL,
|
||||||
|
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(`Action failed with error: ${error}`);
|
core.setFailed(`Action failed with error: ${error}`);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type ClaudeOptions = {
|
|||||||
fallbackModel?: string;
|
fallbackModel?: string;
|
||||||
timeoutMinutes?: string;
|
timeoutMinutes?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
claudeArgs?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PreparedConfig = {
|
type PreparedConfig = {
|
||||||
@@ -30,6 +31,67 @@ type PreparedConfig = {
|
|||||||
env: Record<string, string>;
|
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> {
|
function parseCustomEnvVars(claudeEnv?: string): Record<string, string> {
|
||||||
if (!claudeEnv || claudeEnv.trim() === "") {
|
if (!claudeEnv || claudeEnv.trim() === "") {
|
||||||
return {};
|
return {};
|
||||||
@@ -66,8 +128,10 @@ export function prepareRunConfig(
|
|||||||
promptPath: string,
|
promptPath: string,
|
||||||
options: ClaudeOptions,
|
options: ClaudeOptions,
|
||||||
): PreparedConfig {
|
): PreparedConfig {
|
||||||
|
// Start with base args
|
||||||
const claudeArgs = [...BASE_ARGS];
|
const claudeArgs = [...BASE_ARGS];
|
||||||
|
|
||||||
|
// Add specific options first (these can be overridden by claudeArgs)
|
||||||
if (options.allowedTools) {
|
if (options.allowedTools) {
|
||||||
claudeArgs.push("--allowedTools", 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
|
// Parse custom environment variables
|
||||||
const customEnv = parseCustomEnvVars(options.claudeEnv);
|
const customEnv = parseCustomEnvVars(options.claudeEnv);
|
||||||
|
|
||||||
@@ -147,8 +224,14 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
console.log(`Custom environment variables: ${envKeys}`);
|
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
|
// Output to console
|
||||||
console.log(`Running Claude with prompt from file: ${config.promptPath}`);
|
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
|
// Start sending prompt to pipe in background
|
||||||
const catProcess = spawn("cat", [config.promptPath], {
|
const catProcess = spawn("cat", [config.promptPath], {
|
||||||
|
|||||||
119
base-action/test/parse-shell-args.test.ts
Normal file
119
base-action/test/parse-shell-args.test.ts
Normal 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",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user