Add setting_sources input and default base-action to user-only

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'.


🏠 Remote-Dev: homespace
This commit is contained in:
Kashyap Murali 2026-03-23 20:04:15 -07:00 committed by Octavian Guzu
parent 6ee201f023
commit 8dfb31d8a5
No known key found for this signature in database
8 changed files with 78 additions and 7 deletions

View File

@ -62,6 +62,10 @@ inputs:
description: "Claude Code settings as JSON string or path to settings JSON file" description: "Claude Code settings as JSON string or path to settings JSON file"
required: false required: false
default: "" 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 # Auth configuration
anthropic_api_key: anthropic_api_key:
@ -279,6 +283,7 @@ runs:
# Base-action inputs # Base-action inputs
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
INPUT_SETTINGS: ${{ inputs.settings }} INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_SETTING_SOURCES: ${{ inputs.setting_sources }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands 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_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}

View File

@ -94,6 +94,7 @@ Add the following to your workflow file:
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' | | `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 | '' | | `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 | '' | | `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 | '' | | `system_prompt` | Override system prompt | No | '' |
| `append_system_prompt` | Append to 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 | '' | | `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |

View File

@ -18,6 +18,10 @@ inputs:
description: "Claude Code settings as JSON string or path to settings JSON file" description: "Claude Code settings as JSON string or path to settings JSON file"
required: false required: false
default: "" 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 # Action settings
claude_args: claude_args:
@ -165,6 +169,7 @@ runs:
INPUT_PROMPT: ${{ inputs.prompt }} INPUT_PROMPT: ${{ inputs.prompt }}
INPUT_PROMPT_FILE: ${{ inputs.prompt_file }} INPUT_PROMPT_FILE: ${{ inputs.prompt_file }}
INPUT_SETTINGS: ${{ inputs.settings }} INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_SETTING_SOURCES: ${{ inputs.setting_sources }}
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }} INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}

View File

@ -48,6 +48,7 @@ async function run() {
model: process.env.ANTHROPIC_MODEL, model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable: claudeExecutable, pathToClaudeCodeExecutable: claudeExecutable,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
settingSources: process.env.INPUT_SETTING_SOURCES,
}); });
// Set outputs for the standalone base-action // Set outputs for the standalone base-action

View File

@ -271,13 +271,15 @@ export function parseSdkOptions(options: ClaudeOptions): ParsedSdkOptions {
extraArgs, extraArgs,
env, env,
// Load settings from sources - prefer user's --setting-sources if provided, otherwise use all sources // Setting sources precedence: direct input > --setting-sources in claude_args > default.
// This ensures users can override the default behavior (e.g., --setting-sources user to avoid in-repo configs) // Default is ["user"] only: project/local settings additively merge permissions with
settingSources: extraArgs["setting-sources"] // allowedTools, which silently expands a workflow's intended allow-set. Workflows that
? (extraArgs["setting-sources"].split( // want project settings must opt in explicitly.
",", settingSources: (options.settingSources
) as SdkOptions["settingSources"]) ? options.settingSources.split(",").map((s) => s.trim())
: ["user", "project", "local"], : 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 // Remove setting-sources from extraArgs to avoid passing it twice

View File

@ -14,6 +14,7 @@ export type ClaudeOptions = {
appendSystemPrompt?: string; appendSystemPrompt?: string;
fallbackModel?: string; fallbackModel?: string;
showFullOutput?: string; showFullOutput?: string;
settingSources?: string;
}; };
export async function runClaude( export async function runClaude(

View File

@ -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",
]);
});
});
}); });

View File

@ -278,6 +278,7 @@ async function run() {
model: process.env.ANTHROPIC_MODEL, model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable: claudeExecutable, pathToClaudeCodeExecutable: claudeExecutable,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
settingSources: process.env.INPUT_SETTING_SOURCES,
}); });
claudeSuccess = claudeResult.conclusion === "success"; claudeSuccess = claudeResult.conclusion === "success";