From 8dfb31d8a5fe278a18c62f71175c78bfb5a9e46d Mon Sep 17 00:00:00 2001 From: Kashyap Murali Date: Mon, 23 Mar 2026 20:04:15 -0700 Subject: [PATCH] Add setting_sources input and default base-action to user-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project and local settings additively merge their permissions with whatever allowed_tools a workflow specifies. A workflow author writing a restrictive allowlist reasonably expects it to be the complete allow-set, but .claude/settings.json can silently expand it. Changes: - Add setting_sources as a first-class input to both actions (previously only reachable via --setting-sources in claude_args) - base-action now defaults to settingSources: ['user'] — workflows that want project/local settings must opt in explicitly - Main action defaults to 'user,project,local' since .claude/ is restored from the PR base branch before execution, so project settings are maintainer-trusted in that context - Precedence: setting_sources input > --setting-sources in claude_args > default Breaking change for base-action: workflows relying on .claude/settings.json being loaded automatically need to add setting_sources: 'user,project,local'. :house: Remote-Dev: homespace --- action.yml | 5 ++ base-action/README.md | 1 + base-action/action.yml | 5 ++ base-action/src/index.ts | 1 + base-action/src/parse-sdk-options.ts | 16 ++++--- base-action/src/run-claude.ts | 1 + base-action/test/parse-sdk-options.test.ts | 55 ++++++++++++++++++++++ src/entrypoints/run.ts | 1 + 8 files changed, 78 insertions(+), 7 deletions(-) diff --git a/action.yml b/action.yml index b6d0f05..51ea521 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: @@ -279,6 +283,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 495ebf6..f1f89d2 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 ecbb8d7..4d78acf 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 160e641..f50dd8d 100644 --- a/base-action/src/index.ts +++ b/base-action/src/index.ts @@ -48,6 +48,7 @@ async function run() { model: process.env.ANTHROPIC_MODEL, pathToClaudeCodeExecutable: claudeExecutable, 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 ec65b8f..0fd33c4 100644 --- a/base-action/src/parse-sdk-options.ts +++ b/base-action/src/parse-sdk-options.ts @@ -271,13 +271,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 b18b3f9..4b4e777 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 c74d98e..0d279e2 100644 --- a/base-action/test/parse-sdk-options.test.ts +++ b/base-action/test/parse-sdk-options.test.ts @@ -422,4 +422,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 59723a9..11200ae 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -278,6 +278,7 @@ async function run() { model: process.env.ANTHROPIC_MODEL, pathToClaudeCodeExecutable: claudeExecutable, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, + settingSources: process.env.INPUT_SETTING_SOURCES, }); claudeSuccess = claudeResult.conclusion === "success";