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
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines +65 to +68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The main action sets default: "user,project,local" for setting_sources, which GitHub Actions always resolves even when the user omits the input. This makes options.settingSources always truthy in parseSdkOptions, so --setting-sources passed via claude_args is silently ignored for main action users. Any workflow previously using claude_args: "--setting-sources user" to restrict sources will now silently get user,project,local instead.

Extended reasoning...

What the bug is

The new setting_sources input in action.yml (line 65-68) has default: "user,project,local". In GitHub Actions, when an input has a non-empty default and the user does not explicitly provide that input, GitHub resolves it to the default value. This means INPUT_SETTING_SOURCES is always set to "user,project,local" for every main action user who does not explicitly set setting_sources.

The code path

In parseSdkOptions (line 257-262), the precedence chain is:

  1. options.settingSources — from INPUT_SETTING_SOURCES (always "user,project,local" for main action)
  2. extraArgs["setting-sources"] — parsed from claude_args
  3. Default ["user"]

Since options.settingSources is always truthy for the main action, the extraArgs["setting-sources"] branch is dead code. The fallback from claude_args is unreachable.

Step-by-step proof

Consider a workflow that previously worked:

uses: anthropics/claude-code-action@v1
with:
  claude_args: "--setting-sources user"
  1. User does not set setting_sources input explicitly.
  2. GitHub Actions resolves inputs.setting_sources to "user,project,local" (the default).
  3. action.yml passes INPUT_SETTING_SOURCES: ${{ inputs.setting_sources }}"user,project,local".
  4. run.ts passes settingSources: process.env.INPUT_SETTING_SOURCES"user,project,local".
  5. In parseSdkOptions, options.settingSources is "user,project,local" (truthy), so the first branch is taken.
  6. The --setting-sources user from claude_args is parsed into extraArgs["setting-sources"] but never checked.
  7. Result: settingSources = ["user", "project", "local"] instead of the intended ["user"].

Why existing code doesn't prevent it

The precedence logic in parseSdkOptions is correct in isolation — it properly prioritizes direct input over claude_args. The problem is that GitHub Actions conflates "user provided a value" with "default was applied". The base-action avoids this by using default: "", which is falsy. The main action uses a non-empty default, making it indistinguishable from an explicit user input.

Impact

Any main action user who previously used claude_args: "--setting-sources user" to restrict setting sources (e.g., to avoid in-repo .claude/settings.json expanding their allowlist) will have that restriction silently removed. The PR description explicitly states "Verified existing --setting-sources users via claude_args are unaffected," which is only true for the base-action.

How to fix

Either make the main action's default empty (like the base-action) and apply the permissive default ["user", "project", "local"] in run.ts before calling runClaude (only when INPUT_SETTING_SOURCES is empty/unset), or document this as a breaking change for users who relied on claude_args to override setting sources.


# Auth configuration
anthropic_api_key:
Expand Down Expand Up @@ -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 }}
Expand Down
1 change: 1 addition & 0 deletions base-action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | '' |
Expand Down
5 changes: 5 additions & 0 deletions base-action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand Down
1 change: 1 addition & 0 deletions base-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 9 additions & 7 deletions base-action/src/parse-sdk-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions base-action/src/run-claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type ClaudeOptions = {
appendSystemPrompt?: string;
fallbackModel?: string;
showFullOutput?: string;
settingSources?: string;
};

export async function runClaude(
Expand Down
55 changes: 55 additions & 0 deletions base-action/test/parse-sdk-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]);
});
});
});
1 change: 1 addition & 0 deletions src/entrypoints/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading