Compare commits

...

6 Commits

Author SHA1 Message Date
Octavian Guzu
b6dac6f121
Only pass wrapper defaultSettingSources when project config was restored from base
🏠 Remote-Dev: homespace
2026-04-23 17:40:41 +00:00
Octavian Guzu
044a1036f6
Include issue_comment in event-gated settingSources default
🏠 Remote-Dev: homespace
2026-04-23 17:24:56 +00:00
Octavian Guzu
a551ae4682
Gate base-action settingSources default on event type
Defaults to user-only under pull_request_target/workflow_run; otherwise
keeps user,project,local. Wrapper's runtime default unchanged.

🏠 Remote-Dev: homespace
2026-04-23 17:21:02 +00:00
Octavian Guzu
12f457aad8
Apply setting_sources default at runtime instead of via YAML default
Keeps the input default empty so claude_args --setting-sources is not
shadowed; the wrapper applies user,project,local as the runtime fallback,
base-action applies user.

🏠 Remote-Dev: homespace
2026-04-23 17:19:03 +00:00
Octavian Guzu
625ab08afd
Update MCP server tests for new setting_sources default
- test-mcp-integration: opt in to project settings so .mcp.json is discovered
- test-mcp-config-flag: replace removed mcp_config input with claude_args --mcp-config

🏠 Remote-Dev: homespace
2026-04-23 17:07:32 +00:00
Kashyap Murali
8dfb31d8a5
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
2026-04-23 17:05:38 +00:00
9 changed files with 175 additions and 9 deletions

View File

@ -27,6 +27,8 @@ jobs:
with: with:
prompt: "List all available tools" prompt: "List all available tools"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Explicitly include project so .mcp.json is discovered regardless of the event-gated default
setting_sources: "user,project"
env: env:
# Change to test directory so it finds .mcp.json # Change to test directory so it finds .mcp.json
CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test
@ -108,7 +110,11 @@ jobs:
with: with:
prompt: "List all available tools" prompt: "List all available tools"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
mcp_config: '{"mcpServers":{"test-server":{"type":"stdio","command":"bun","args":["simple-mcp-server.ts"],"env":{}}}}' # mcp_config input was removed; pass via claude_args. Pin setting_sources to "user"
# so .mcp.json is NOT auto-discovered — this proves the flag itself works.
setting_sources: "user"
claude_args: >-
--mcp-config '{"mcpServers":{"test-server":{"type":"stdio","command":"bun","args":["simple-mcp-server.ts"],"env":{}}}}'
env: env:
# Change to test directory so bun can find the MCP server script # Change to test directory so bun can find the MCP server script
CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test

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). When unset, the action applies 'user,project,local' at runtime for PR contexts where .claude/ is restored from the base branch; for other contexts it applies the same event-gated default as base-action. Set to 'user' to ignore in-repo settings entirely."
required: false
default: ""
# 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 | event-dependent (see below) |
| `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 | '' |
@ -111,6 +112,8 @@ Add the following to your workflow file:
\*\*`show_full_output` is automatically enabled when GitHub Actions debug mode is active. See [security documentation](../docs/security.md#-full-output-security-warning) for important security considerations. \*\*`show_full_output` is automatically enabled when GitHub Actions debug mode is active. See [security documentation](../docs/security.md#-full-output-security-warning) for important security considerations.
`setting_sources` defaults to `user,project,local` for most events. Under `pull_request_target`, `workflow_run`, and `issue_comment` it defaults to `user` only; set it explicitly if you want project/local settings to load for those events.
## Outputs ## Outputs
| Output | Description | | Output | Description |

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,project,local'; under pull_request_target/workflow_run/issue_comment, defaults to 'user' only. Project/local settings additively merge permissions with allowed_tools — set explicitly to control which sources load."
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,22 @@ 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) // The default is supplied by the caller (the wrapper action passes
settingSources: extraArgs["setting-sources"] // ["user","project","local"]); base-action applies an event-gated default of ["user"]
? (extraArgs["setting-sources"].split( // under pull_request_target/workflow_run/issue_comment and ["user","project","local"]
",", // otherwise. Both action.yml files leave the YAML default empty so that
) as SdkOptions["settingSources"]) // --setting-sources in claude_args is reachable when the input is not set.
: ["user", "project", "local"], settingSources: (options.settingSources
? options.settingSources.split(",").map((s) => s.trim())
: extraArgs["setting-sources"]
? extraArgs["setting-sources"].split(",").map((s) => s.trim())
: (options.defaultSettingSources ??
(process.env.GITHUB_EVENT_NAME === "pull_request_target" ||
process.env.GITHUB_EVENT_NAME === "workflow_run" ||
process.env.GITHUB_EVENT_NAME === "issue_comment"
? ["user"]
: ["user", "project", "local"]))) 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

@ -1,6 +1,7 @@
import { runClaudeWithSdk } from "./run-claude-sdk"; import { runClaudeWithSdk } from "./run-claude-sdk";
import type { ClaudeRunResult } from "./run-claude-sdk"; import type { ClaudeRunResult } from "./run-claude-sdk";
import { parseSdkOptions } from "./parse-sdk-options"; import { parseSdkOptions } from "./parse-sdk-options";
import type { Options as SdkOptions } from "@anthropic-ai/claude-agent-sdk";
export type ClaudeOptions = { export type ClaudeOptions = {
claudeArgs?: string; claudeArgs?: string;
@ -14,6 +15,8 @@ export type ClaudeOptions = {
appendSystemPrompt?: string; appendSystemPrompt?: string;
fallbackModel?: string; fallbackModel?: string;
showFullOutput?: string; showFullOutput?: string;
settingSources?: string;
defaultSettingSources?: SdkOptions["settingSources"];
}; };
export async function runClaude( export async function runClaude(

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { describe, test, expect } from "bun:test"; import { describe, test, expect, afterEach } from "bun:test";
import { parseSdkOptions } from "../src/parse-sdk-options"; import { parseSdkOptions } from "../src/parse-sdk-options";
import type { ClaudeOptions } from "../src/run-claude"; import type { ClaudeOptions } from "../src/run-claude";
@ -422,4 +422,129 @@ describe("parseSdkOptions", () => {
} }
}); });
}); });
describe("settingSources", () => {
const originalEventName = process.env.GITHUB_EVENT_NAME;
afterEach(() => {
if (originalEventName === undefined) {
delete process.env.GITHUB_EVENT_NAME;
} else {
process.env.GITHUB_EVENT_NAME = originalEventName;
}
});
test("should default to ['user','project','local'] for non-gated events", () => {
process.env.GITHUB_EVENT_NAME = "push";
const result = parseSdkOptions({});
expect(result.sdkOptions.settingSources).toEqual([
"user",
"project",
"local",
]);
});
test("should default to ['user'] under pull_request_target", () => {
process.env.GITHUB_EVENT_NAME = "pull_request_target";
const result = parseSdkOptions({});
expect(result.sdkOptions.settingSources).toEqual(["user"]);
});
test("should default to ['user'] under workflow_run", () => {
process.env.GITHUB_EVENT_NAME = "workflow_run";
const result = parseSdkOptions({});
expect(result.sdkOptions.settingSources).toEqual(["user"]);
});
test("should default to ['user'] under issue_comment", () => {
process.env.GITHUB_EVENT_NAME = "issue_comment";
const result = parseSdkOptions({});
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",
]);
});
test("explicit defaultSettingSources overrides the event-gated default", () => {
process.env.GITHUB_EVENT_NAME = "pull_request_target";
const options: ClaudeOptions = {
defaultSettingSources: ["user", "project", "local"],
};
const result = parseSdkOptions(options);
expect(result.sdkOptions.settingSources).toEqual([
"user",
"project",
"local",
]);
});
test("--setting-sources in claudeArgs should win over defaultSettingSources", () => {
const options: ClaudeOptions = {
claudeArgs: "--setting-sources user",
defaultSettingSources: ["user", "project", "local"],
};
const result = parseSdkOptions(options);
expect(result.sdkOptions.settingSources).toEqual(["user"]);
});
test("empty-string settingSources falls through to claudeArgs then default", () => {
// YAML default: "" — INPUT_SETTING_SOURCES is "" when the user doesn't set the input
const options: ClaudeOptions = {
settingSources: "",
claudeArgs: "--setting-sources user,project",
defaultSettingSources: ["user", "project", "local"],
};
const result = parseSdkOptions(options);
expect(result.sdkOptions.settingSources).toEqual(["user", "project"]);
});
});
}); });

View File

@ -241,6 +241,7 @@ async function run() {
// lacks base.ref, so we fall back to the mode-provided value — tag mode // lacks base.ref, so we fall back to the mode-provided value — tag mode
// fetches it from GraphQL; agent mode on issue_comment is an edge case // fetches it from GraphQL; agent mode on issue_comment is an edge case
// that at worst restores from the wrong trusted branch (still secure). // that at worst restores from the wrong trusted branch (still secure).
let configRestoredFromBase = false;
if (isEntityContext(context) && context.isPR) { if (isEntityContext(context) && context.isPR) {
let restoreBase = baseBranch; let restoreBase = baseBranch;
if ( if (
@ -253,6 +254,7 @@ async function run() {
} }
if (restoreBase) { if (restoreBase) {
restoreConfigFromBase(restoreBase); restoreConfigFromBase(restoreBase);
configRestoredFromBase = true;
} }
} }
@ -278,6 +280,13 @@ 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,
// Only assert that project/local config is safe to load when it was actually
// restored from the base branch above. Otherwise leave undefined so
// parseSdkOptions applies its event-gated default.
defaultSettingSources: configRestoredFromBase
? ["user", "project", "local"]
: undefined,
}); });
claudeSuccess = claudeResult.conclusion === "success"; claudeSuccess = claudeResult.conclusion === "success";