diff --git a/action.yml b/action.yml index 9e830cb92..d22ef61a1 100644 --- a/action.yml +++ b/action.yml @@ -62,6 +62,10 @@ inputs: description: "Claude Code settings as JSON string or path to settings JSON file" required: false default: "" + setting_sources: + description: "Comma-separated list of setting sources to load (user, project, local). Defaults to 'user,project,local' — project settings are safe here because .claude/ is restored from the PR base branch before execution. Set to 'user' to ignore in-repo settings entirely." + required: false + default: "user,project,local" # Auth configuration anthropic_api_key: @@ -233,6 +237,7 @@ runs: # Base-action inputs INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt INPUT_SETTINGS: ${{ inputs.settings }} + INPUT_SETTING_SOURCES: ${{ inputs.setting_sources }} INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} diff --git a/base-action/README.md b/base-action/README.md index 495ebf6fb..f1f89d227 100644 --- a/base-action/README.md +++ b/base-action/README.md @@ -94,6 +94,7 @@ Add the following to your workflow file: | `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' | | `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' | | `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' | +| `setting_sources` | Comma-separated setting sources to load (`user`, `project`, `local`). Project/local merge permissions additively. | No | 'user' | | `system_prompt` | Override system prompt | No | '' | | `append_system_prompt` | Append to system prompt | No | '' | | `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' | diff --git a/base-action/action.yml b/base-action/action.yml index 657775caa..9e9f34e5d 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -18,6 +18,10 @@ inputs: description: "Claude Code settings as JSON string or path to settings JSON file" required: false default: "" + setting_sources: + description: "Comma-separated list of setting sources to load (user, project, local). Defaults to 'user' only. Project/local settings additively merge permissions with allowed_tools — set to 'user,project,local' to opt in." + required: false + default: "" # Action settings claude_args: @@ -165,6 +169,7 @@ runs: INPUT_PROMPT: ${{ inputs.prompt }} INPUT_PROMPT_FILE: ${{ inputs.prompt_file }} INPUT_SETTINGS: ${{ inputs.settings }} + INPUT_SETTING_SOURCES: ${{ inputs.setting_sources }} INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }} INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} diff --git a/base-action/src/index.ts b/base-action/src/index.ts index 970e79d1c..cdd8f88e0 100644 --- a/base-action/src/index.ts +++ b/base-action/src/index.ts @@ -41,6 +41,7 @@ async function run() { pathToClaudeCodeExecutable: process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, + settingSources: process.env.INPUT_SETTING_SOURCES, }); // Set outputs for the standalone base-action diff --git a/base-action/src/parse-sdk-options.ts b/base-action/src/parse-sdk-options.ts index 35df281d2..7c5d53e69 100644 --- a/base-action/src/parse-sdk-options.ts +++ b/base-action/src/parse-sdk-options.ts @@ -251,13 +251,15 @@ export function parseSdkOptions(options: ClaudeOptions): ParsedSdkOptions { extraArgs, env, - // Load settings from sources - prefer user's --setting-sources if provided, otherwise use all sources - // This ensures users can override the default behavior (e.g., --setting-sources user to avoid in-repo configs) - settingSources: extraArgs["setting-sources"] - ? (extraArgs["setting-sources"].split( - ",", - ) as SdkOptions["settingSources"]) - : ["user", "project", "local"], + // Setting sources precedence: direct input > --setting-sources in claude_args > default. + // Default is ["user"] only: project/local settings additively merge permissions with + // allowedTools, which silently expands a workflow's intended allow-set. Workflows that + // want project settings must opt in explicitly. + settingSources: (options.settingSources + ? options.settingSources.split(",").map((s) => s.trim()) + : extraArgs["setting-sources"] + ? extraArgs["setting-sources"].split(",").map((s) => s.trim()) + : ["user"]) as SdkOptions["settingSources"], }; // Remove setting-sources from extraArgs to avoid passing it twice diff --git a/base-action/src/run-claude.ts b/base-action/src/run-claude.ts index b18b3f938..4b4e77709 100644 --- a/base-action/src/run-claude.ts +++ b/base-action/src/run-claude.ts @@ -14,6 +14,7 @@ export type ClaudeOptions = { appendSystemPrompt?: string; fallbackModel?: string; showFullOutput?: string; + settingSources?: string; }; export async function runClaude( diff --git a/base-action/test/parse-sdk-options.test.ts b/base-action/test/parse-sdk-options.test.ts index 9c1095cef..95804da21 100644 --- a/base-action/test/parse-sdk-options.test.ts +++ b/base-action/test/parse-sdk-options.test.ts @@ -367,4 +367,59 @@ describe("parseSdkOptions", () => { ); }); }); + + describe("settingSources", () => { + test("should default to ['user'] when not specified", () => { + const options: ClaudeOptions = {}; + const result = parseSdkOptions(options); + + expect(result.sdkOptions.settingSources).toEqual(["user"]); + }); + + test("should use direct settingSources input when provided", () => { + const options: ClaudeOptions = { + settingSources: "user,project,local", + }; + const result = parseSdkOptions(options); + + expect(result.sdkOptions.settingSources).toEqual([ + "user", + "project", + "local", + ]); + }); + + test("should use --setting-sources from claudeArgs when no direct input", () => { + const options: ClaudeOptions = { + claudeArgs: "--setting-sources user,project", + }; + const result = parseSdkOptions(options); + + expect(result.sdkOptions.settingSources).toEqual(["user", "project"]); + expect(result.sdkOptions.extraArgs?.["setting-sources"]).toBeUndefined(); + }); + + test("direct input should take precedence over claudeArgs", () => { + const options: ClaudeOptions = { + settingSources: "user", + claudeArgs: "--setting-sources user,project,local", + }; + const result = parseSdkOptions(options); + + expect(result.sdkOptions.settingSources).toEqual(["user"]); + }); + + test("should trim whitespace in comma-separated values", () => { + const options: ClaudeOptions = { + settingSources: "user, project , local", + }; + const result = parseSdkOptions(options); + + expect(result.sdkOptions.settingSources).toEqual([ + "user", + "project", + "local", + ]); + }); + }); }); diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index cfba1cb93..2200f77a5 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -272,6 +272,7 @@ async function run() { pathToClaudeCodeExecutable: process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, + settingSources: process.env.INPUT_SETTING_SOURCES, }); claudeSuccess = claudeResult.conclusion === "success";