fix: wire allowed_tools/disallowed_tools inputs through to Claude SDK
The disallowed tools env var read in run.ts was DISALLOWED_TOOLS_INPUT (raw user input) instead of DISALLOWED_TOOLS (computed value including WebSearch/WebFetch defaults). This meant default tool disallowance was never enforced. Also removes orphaned ALLOWED_TOOLS env var export from createPrompt() (tag mode passes allowed tools via claudeArgs instead), extracts shared parseToolsInput utility to eliminate duplication, and adds tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
29a1032aa6
commit
df7afc08d4
7 files changed
+112
-8
No files matched your search
+11
@@ -77,6 +77,14 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
|
allowed_tools:
|
||||||
|
description: "Comma-separated list of additional tools to allow (e.g., 'WebSearch,WebFetch,Bash(npm test:*)'). These are merged with the default tag-mode tools."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
disallowed_tools:
|
||||||
|
description: "Comma-separated list of tools to disallow (e.g., 'WebSearch'). Overrides defaults."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
claude_args:
|
claude_args:
|
||||||
description: "Additional arguments to pass directly to Claude CLI"
|
description: "Additional arguments to pass directly to Claude CLI"
|
||||||
required: false
|
required: false
|
||||||
@@ -254,6 +262,9 @@ runs:
|
|||||||
INCLUDE_FIX_LINKS: ${{ inputs.include_fix_links }}
|
INCLUDE_FIX_LINKS: ${{ inputs.include_fix_links }}
|
||||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||||
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||||
|
# _INPUT suffix = raw user input; createPrompt() computes DISALLOWED_TOOLS from these
|
||||||
|
ALLOWED_TOOLS_INPUT: ${{ inputs.allowed_tools }}
|
||||||
|
DISALLOWED_TOOLS_INPUT: ${{ inputs.disallowed_tools }}
|
||||||
ALL_INPUTS: ${{ toJson(inputs) }}
|
ALL_INPUTS: ${{ toJson(inputs) }}
|
||||||
|
|
||||||
# Base-action inputs
|
# Base-action inputs
|
||||||
|
|||||||
@@ -159,6 +159,37 @@ platform:
|
|||||||
|
|
||||||
All other inputs work identically on both platforms. See the [Usage Guide](./usage.md) for the full list.
|
All other inputs work identically on both platforms. See the [Usage Guide](./usage.md) for the full list.
|
||||||
|
|
||||||
|
## Fork-Specific Fixes (All Platforms)
|
||||||
|
|
||||||
|
This fork fixes upstream bugs that affect **both GitHub and Forgejo** usage:
|
||||||
|
|
||||||
|
### `allowed_tools` / `disallowed_tools` inputs now work (upstream #181, #287, #724)
|
||||||
|
|
||||||
|
Upstream's `allowed_tools` and `disallowed_tools` inputs are completely broken — they are never wired through to Claude execution. This affects tag mode (the `@claude` mention flow) where the tool list is hardcoded and user inputs are silently ignored.
|
||||||
|
|
||||||
|
**What was broken:**
|
||||||
|
|
||||||
|
- `allowed_tools` was not defined in `action.yml`, so the env var was never set
|
||||||
|
- Tag mode only merged `mcp__github_*` tools from `claude_args`, filtering out everything else
|
||||||
|
- `run.ts` never passed `allowedTools`/`disallowedTools` to `runClaude()`
|
||||||
|
- `createPrompt()` hardcoded `WebSearch` and `WebFetch` as disallowed with no way to override
|
||||||
|
|
||||||
|
**What this fork fixes:**
|
||||||
|
|
||||||
|
- Added `allowed_tools` and `disallowed_tools` as proper action inputs
|
||||||
|
- User tools are merged into the tag-mode tool list
|
||||||
|
- `disallowedTools` is passed through to the SDK
|
||||||
|
- `WebSearch`/`WebFetch` are removed from the disallowed list when included in `allowed_tools`
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: actions/claude-code-action@main
|
||||||
|
with:
|
||||||
|
allowed_tools: "WebSearch,WebFetch"
|
||||||
|
# All other inputs as usual
|
||||||
|
```
|
||||||
|
|
||||||
## Dedicated Bot User (Recommended)
|
## Dedicated Bot User (Recommended)
|
||||||
|
|
||||||
On GitHub, Claude gets its own identity via the GitHub App (`claude[bot]`). On Forgejo, there's no such mechanism — by default the action runs under whatever token you provide, meaning commits and comments appear as the human user.
|
On GitHub, Claude gets its own identity via the GitHub App (`claude[bot]`). On Forgejo, there's no such mechanism — by default the action runs under whatever token you provide, meaning commits and comments appear as the human user.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { GITHUB_SERVER_URL } from "../github/api/config";
|
|||||||
import { detectPlatform } from "../platform/detector";
|
import { detectPlatform } from "../platform/detector";
|
||||||
import { getForgejoRunIndex } from "../forgejo/comment-logic";
|
import { getForgejoRunIndex } from "../forgejo/comment-logic";
|
||||||
import { extractUserRequest } from "../utils/extract-user-request";
|
import { extractUserRequest } from "../utils/extract-user-request";
|
||||||
|
import { parseToolsInput } from "../utils/parse-tools-input";
|
||||||
export type { CommonFields, PreparedContext } from "./types";
|
export type { CommonFields, PreparedContext } from "./types";
|
||||||
|
|
||||||
/** Filename for the user request file, read by the SDK runner */
|
/** Filename for the user request file, read by the SDK runner */
|
||||||
@@ -993,17 +994,19 @@ export async function createPrompt(
|
|||||||
console.log("========================");
|
console.log("========================");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set allowed tools
|
// Compute disallowed tools — merge user inputs with defaults (fixes upstream #181, #287, #724)
|
||||||
const hasActionsReadPermission = false;
|
// Note: Allowed tools are passed via claudeArgs in prepareTagMode(), not through env vars.
|
||||||
|
// DISALLOWED_TOOLS is consumed by run.ts and passed to runClaude().
|
||||||
|
const userAllowedTools = parseToolsInput(process.env.ALLOWED_TOOLS_INPUT);
|
||||||
|
const userDisallowedTools = parseToolsInput(
|
||||||
|
process.env.DISALLOWED_TOOLS_INPUT,
|
||||||
|
);
|
||||||
|
|
||||||
const allAllowedTools = buildAllowedToolsString(
|
const allDisallowedTools = buildDisallowedToolsString(
|
||||||
[],
|
userDisallowedTools,
|
||||||
hasActionsReadPermission,
|
userAllowedTools,
|
||||||
context.inputs.useCommitSigning,
|
|
||||||
);
|
);
|
||||||
const allDisallowedTools = buildDisallowedToolsString([], []);
|
|
||||||
|
|
||||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
|
||||||
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
|
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Create prompt failed with error: ${error}`);
|
throw new Error(`Create prompt failed with error: ${error}`);
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ async function run() {
|
|||||||
|
|
||||||
const claudeResult: ClaudeRunResult = await runClaude(promptConfig.path, {
|
const claudeResult: ClaudeRunResult = await runClaude(promptConfig.path, {
|
||||||
claudeArgs: prepareResult.claudeArgs,
|
claudeArgs: prepareResult.claudeArgs,
|
||||||
|
disallowedTools: process.env.DISALLOWED_TOOLS || undefined,
|
||||||
appendSystemPrompt: process.env.APPEND_SYSTEM_PROMPT,
|
appendSystemPrompt: process.env.APPEND_SYSTEM_PROMPT,
|
||||||
model: process.env.ANTHROPIC_MODEL,
|
model: process.env.ANTHROPIC_MODEL,
|
||||||
pathToClaudeCodeExecutable:
|
pathToClaudeCodeExecutable:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { isEntityContext } from "../../github/context";
|
|||||||
import type { GitHubContext } from "../../github/context";
|
import type { GitHubContext } from "../../github/context";
|
||||||
import type { Octokits } from "../../github/api/client";
|
import type { Octokits } from "../../github/api/client";
|
||||||
import { parseAllowedTools } from "../agent/parse-tools";
|
import { parseAllowedTools } from "../agent/parse-tools";
|
||||||
|
import { parseToolsInput } from "../../utils/parse-tools-input";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares the tag mode execution context.
|
* Prepares the tag mode execution context.
|
||||||
@@ -124,6 +125,9 @@ export async function prepareTagMode({
|
|||||||
tool.startsWith("mcp__github_"),
|
tool.startsWith("mcp__github_"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Merge user-provided allowed_tools input (fixes upstream #181, #287, #724)
|
||||||
|
const userAllowedTools = parseToolsInput(process.env.ALLOWED_TOOLS_INPUT);
|
||||||
|
|
||||||
// Build claude_args for tag mode with required tools
|
// Build claude_args for tag mode with required tools
|
||||||
// Tag mode REQUIRES these tools to function properly
|
// Tag mode REQUIRES these tools to function properly
|
||||||
const tagModeTools = [
|
const tagModeTools = [
|
||||||
@@ -139,6 +143,7 @@ export async function prepareTagMode({
|
|||||||
"mcp__github_ci__get_workflow_run_details",
|
"mcp__github_ci__get_workflow_run_details",
|
||||||
"mcp__github_ci__download_job_log",
|
"mcp__github_ci__download_job_log",
|
||||||
...userAllowedMCPTools,
|
...userAllowedMCPTools,
|
||||||
|
...userAllowedTools,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add git commands when using git CLI (no API commit signing, or SSH signing)
|
// Add git commands when using git CLI (no API commit signing, or SSH signing)
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Parse a comma-separated tools string into a trimmed, non-empty array.
|
||||||
|
* Used for ALLOWED_TOOLS_INPUT and DISALLOWED_TOOLS_INPUT env vars.
|
||||||
|
*/
|
||||||
|
export function parseToolsInput(input: string | undefined): string[] {
|
||||||
|
return (input || "")
|
||||||
|
.split(",")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, test, expect } from "bun:test";
|
||||||
|
import { parseToolsInput } from "../../src/utils/parse-tools-input";
|
||||||
|
|
||||||
|
describe("parseToolsInput", () => {
|
||||||
|
test("returns empty array for undefined", () => {
|
||||||
|
expect(parseToolsInput(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns empty array for empty string", () => {
|
||||||
|
expect(parseToolsInput("")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses single tool", () => {
|
||||||
|
expect(parseToolsInput("WebSearch")).toEqual(["WebSearch"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses comma-separated tools", () => {
|
||||||
|
expect(parseToolsInput("WebSearch,WebFetch")).toEqual([
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trims whitespace around tools", () => {
|
||||||
|
expect(parseToolsInput(" WebSearch , WebFetch ")).toEqual([
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters empty entries from trailing commas", () => {
|
||||||
|
expect(parseToolsInput("WebSearch,,WebFetch,")).toEqual([
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles tool patterns with special characters", () => {
|
||||||
|
expect(
|
||||||
|
parseToolsInput("Bash(npm test:*),mcp__github_ci__get_ci_status"),
|
||||||
|
).toEqual(["Bash(npm test:*)", "mcp__github_ci__get_ci_status"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user