From df7afc08d480de633e9813aff9580d1132b309b8 Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Fri, 27 Feb 2026 11:16:38 +0200 Subject: [PATCH] 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 --- action.yml | 11 +++++++ docs/forgejo.md | 31 ++++++++++++++++++++ src/create-prompt/index.ts | 19 ++++++------ src/entrypoints/run.ts | 1 + src/modes/tag/index.ts | 5 ++++ src/utils/parse-tools-input.ts | 10 +++++++ test/utils/parse-tools-input.test.ts | 43 ++++++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/utils/parse-tools-input.ts create mode 100644 test/utils/parse-tools-input.test.ts diff --git a/action.yml b/action.yml index 83a99fb..77f7894 100644 --- a/action.yml +++ b/action.yml @@ -77,6 +77,14 @@ inputs: required: 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: description: "Additional arguments to pass directly to Claude CLI" required: false @@ -254,6 +262,9 @@ runs: INCLUDE_FIX_LINKS: ${{ inputs.include_fix_links }} ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }} 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) }} # Base-action inputs diff --git a/docs/forgejo.md b/docs/forgejo.md index 0df768a..6f9ff2d 100644 --- a/docs/forgejo.md +++ b/docs/forgejo.md @@ -159,6 +159,37 @@ platform: 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) 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. diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 8b79963..7c8f41d 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -23,6 +23,7 @@ import { GITHUB_SERVER_URL } from "../github/api/config"; import { detectPlatform } from "../platform/detector"; import { getForgejoRunIndex } from "../forgejo/comment-logic"; import { extractUserRequest } from "../utils/extract-user-request"; +import { parseToolsInput } from "../utils/parse-tools-input"; export type { CommonFields, PreparedContext } from "./types"; /** Filename for the user request file, read by the SDK runner */ @@ -993,17 +994,19 @@ export async function createPrompt( console.log("========================"); } - // Set allowed tools - const hasActionsReadPermission = false; + // Compute disallowed tools — merge user inputs with defaults (fixes upstream #181, #287, #724) + // 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( - [], - hasActionsReadPermission, - context.inputs.useCommitSigning, + const allDisallowedTools = buildDisallowedToolsString( + userDisallowedTools, + userAllowedTools, ); - const allDisallowedTools = buildDisallowedToolsString([], []); - core.exportVariable("ALLOWED_TOOLS", allAllowedTools); core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools); } catch (error) { throw new Error(`Create prompt failed with error: ${error}`); diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index 25de9d5..cfc95d2 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -245,6 +245,7 @@ async function run() { const claudeResult: ClaudeRunResult = await runClaude(promptConfig.path, { claudeArgs: prepareResult.claudeArgs, + disallowedTools: process.env.DISALLOWED_TOOLS || undefined, appendSystemPrompt: process.env.APPEND_SYSTEM_PROMPT, model: process.env.ANTHROPIC_MODEL, pathToClaudeCodeExecutable: diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts index f648b9a..a534655 100644 --- a/src/modes/tag/index.ts +++ b/src/modes/tag/index.ts @@ -19,6 +19,7 @@ import { isEntityContext } from "../../github/context"; import type { GitHubContext } from "../../github/context"; import type { Octokits } from "../../github/api/client"; import { parseAllowedTools } from "../agent/parse-tools"; +import { parseToolsInput } from "../../utils/parse-tools-input"; /** * Prepares the tag mode execution context. @@ -124,6 +125,9 @@ export async function prepareTagMode({ 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 // Tag mode REQUIRES these tools to function properly const tagModeTools = [ @@ -139,6 +143,7 @@ export async function prepareTagMode({ "mcp__github_ci__get_workflow_run_details", "mcp__github_ci__download_job_log", ...userAllowedMCPTools, + ...userAllowedTools, ]; // Add git commands when using git CLI (no API commit signing, or SSH signing) diff --git a/src/utils/parse-tools-input.ts b/src/utils/parse-tools-input.ts new file mode 100644 index 0000000..acd2a30 --- /dev/null +++ b/src/utils/parse-tools-input.ts @@ -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); +} diff --git a/test/utils/parse-tools-input.test.ts b/test/utils/parse-tools-input.test.ts new file mode 100644 index 0000000..9c4dc72 --- /dev/null +++ b/test/utils/parse-tools-input.test.ts @@ -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"]); + }); +});