mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-22 03:18:54 +08:00
fix(parse-sdk-options): prevent shell-quote from collapsing unquoted Bash(X:*) rules to bare Bash (#1350)
* fix(parse-sdk-options): prevent shell-quote from collapsing unquoted Bash(X:*) rules to bare Bash shell-quote's parse() tokenizes unquoted `(`, `)` as control operators and barewords containing `*` as glob ops, all returned as non-string objects. parseClaudeArgsToExtraArgs filtered those out, so an unquoted `--allowedTools View,Bash(gh:*),Bash(cat:*)` collapsed to bare `Bash` — silently widening scoped permission rules to unrestricted Bash(*). Escape shell control metachars to Unicode private-use placeholders before parse() and restore after; extract .pattern from glob ops. Preserves existing quote/whitespace handling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: retrigger Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d5726de019
commit
a5e5d3b82e
@@ -137,6 +137,110 @@ describe("parseSdkOptions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("should preserve unquoted Bash(cmd:*) rules instead of collapsing to bare Bash", () => {
|
||||
// Regression: shell-quote tokenizes unquoted `(`/`)` as control ops and
|
||||
// `*` as a glob, which were filtered out — collapsing scoped rules like
|
||||
// `Bash(gh:*)` into bare `Bash` (= Bash(*), unrestricted shell).
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--allowedTools View,Bash(gh:*),Bash(cat:*)",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"View",
|
||||
"Bash(gh:*)",
|
||||
"Bash(cat:*)",
|
||||
]);
|
||||
expect(result.sdkOptions.allowedTools).not.toContain("Bash");
|
||||
});
|
||||
|
||||
test("should preserve unquoted space-separated Bash(cmd:*) rules", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--allowed-tools Bash(gh:*) Bash(cat:*) Read(//tmp/**)",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Bash(gh:*)",
|
||||
"Bash(cat:*)",
|
||||
"Read(//tmp/**)",
|
||||
]);
|
||||
expect(result.sdkOptions.allowedTools).not.toContain("Bash");
|
||||
});
|
||||
|
||||
test("should preserve unquoted Tool(content) rules without glob chars", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs:
|
||||
"--allowedTools Read(~/file),WebFetch(domain:example.com),Edit",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Read(~/file)",
|
||||
"WebFetch(domain:example.com)",
|
||||
"Edit",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should still preserve quoted Bash(cmd:*) rules (no regression)", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Bash(gh:*),Bash(cat:*)"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Bash(gh:*)",
|
||||
"Bash(cat:*)",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should merge quoted tag-mode tools with unquoted user tools without widening", () => {
|
||||
// Real-world shape: the action's tag mode wraps its own --allowedTools in
|
||||
// double quotes, then appends the user's claude_args (typically unquoted
|
||||
// in workflow YAML). Both halves must round-trip.
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs:
|
||||
'--permission-mode acceptEdits --allowedTools "Glob,Grep,Read,Bash(git add:*),Bash(git commit:*)" ' +
|
||||
"--model claude-opus-4-7\n" +
|
||||
"--allowedTools View,Bash(gh:*),Bash(printf:*),Bash(cat:*)",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Read",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"View",
|
||||
"Bash(gh:*)",
|
||||
"Bash(printf:*)",
|
||||
"Bash(cat:*)",
|
||||
]);
|
||||
expect(result.sdkOptions.allowedTools).not.toContain("Bash");
|
||||
});
|
||||
|
||||
test("should preserve unquoted disallowedTools rules without widening", () => {
|
||||
// Same bug class on the deny side: a scoped deny collapsing to bare
|
||||
// `Bash` would block all shell instead of the intended prefix.
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--disallowedTools Bash(rm:*),Bash(sudo:*)",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.disallowedTools).toEqual([
|
||||
"Bash(rm:*)",
|
||||
"Bash(sudo:*)",
|
||||
]);
|
||||
expect(result.sdkOptions.disallowedTools).not.toContain("Bash");
|
||||
});
|
||||
|
||||
test("should handle mixed camelCase and hyphenated allowedTools flags", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read" --allowed-tools "Write,Glob"',
|
||||
|
||||
Reference in New Issue
Block a user