- CLAUDE.md: add "Things That Will Bite You" entry about bare Bash requiring interactive approval and needing sub-patterns in CI - docs/forgejo.md: update allowed_tools section with sub-pattern examples, agent mode fix note, and common patterns reference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
27 KiB
Forgejo Platform Support
This action supports Forgejo (and Forgejo Actions) as an alternative to GitHub. The same action code runs on both platforms — platform detection is automatic or explicitly configured.
Prerequisites
- Forgejo instance with Forgejo Actions enabled
- Forgejo runner registered and operational (e.g., a
gruntorubuntu-latestrunner) - Anthropic API key or Claude Code OAuth token for Claude API access
- Forgejo token with repo access (automatically available as
github.tokenin workflows, or use a dedicated bot user)
Quick Start
1. Add Secrets
Add secrets at the organization level (Settings > Actions > Secrets) so all repos can share them:
ANTHROPIC_API_KEY— Your Anthropic API key (starting withsk-ant-)CLAUDE_BOT_TOKEN— API token for the dedicated bot user (see Dedicated Bot User below)
2. Create Workflow
Create .forgejo/workflows/claude.yml:
name: Claude Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned, labeled]
pull_request_review:
types: [submitted]
jobs:
claude-response:
runs-on: grunt # Your Forgejo runner label
steps:
- uses: https://git.wxs.ro/actions/claude-code-action@v1 # Or your mirror URL
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
platform: "forgejo"
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}
bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID (see step 5 below)
bot_name: "claude"
bot_email: "claude@yourdomain.com" # Must match the bot user's registered email
3. Agent Mode (Automation)
For automation workflows triggered by workflow_dispatch, push events, or schedules:
name: Claude Automation
on:
workflow_dispatch:
inputs:
task:
description: "Task for Claude"
required: true
jobs:
run-claude:
runs-on: grunt
steps:
- uses: https://git.wxs.ro/actions/claude-code-action@v1
with:
prompt: ${{ github.event.inputs.task }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
platform: "forgejo"
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}
bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID
bot_name: "claude"
bot_email: "claude@yourdomain.com"
Platform Detection
The action detects the platform using a three-tier strategy:
| Priority | Method | Details |
|---|---|---|
| 1 | Explicit input | platform: "forgejo" in workflow — most reliable |
| 2 | FORGEJO env var |
Set to "true" by Forgejo runner v7.0+ |
| 3 | GITHUB_SERVER_URL heuristic |
If the URL doesn't contain github.com, assumes Forgejo |
Recommendation: Always set platform: "forgejo" explicitly. Auto-detection works but the explicit input eliminates ambiguity.
Security note: Setting
platform: "forgejo"on agithub.comrunner is rejected — the action cross-validates theGITHUB_SERVER_URLand falls through to standard detection. This prevents bypassing GitHub-specific security checks (e.g., actor validation).
Note for GitHub Enterprise Server (GHES) users: The tier-3 heuristic classifies any non-
github.comURL as Forgejo. If you run on GHES (e.g.,https://github.mycompany.com), you must setplatform: "github"explicitly to avoid misdetection.
Differences from GitHub
Authentication
| Aspect | GitHub | Forgejo |
|---|---|---|
| Default auth | GitHub App OIDC token exchange | Direct GITHUB_TOKEN / FORGEJO_TOKEN |
| Token setup | Automatic via Claude GitHub App | Manual — provide github_token input |
| App token revocation | Automatic cleanup step | Skipped (no app token) |
| Bot identity | claude[bot] (ID: 41898282) |
Dedicated bot user via bot_id/bot_name/bot_email inputs |
On Forgejo, there is no OIDC token exchange with Anthropic. The action uses the workflow token (github.token) or a user-provided github_token directly.
API Differences
| Feature | GitHub | Forgejo |
|---|---|---|
| GraphQL API | Used for PR/issue data fetching | Not available — REST API only |
| Data fetching | Single GraphQL query per entity | 5-8 parallel REST API calls |
| Actor validation | Checks for bot/app accounts | Bot-type check skipped; self-trigger guard active |
| GitHub MCP server | ghcr.io/github/github-mcp-server included |
Skipped — GitHub-specific |
| CI/Actions MCP | Full GitHub Actions API | Works via Octokit REST + GITHUB_API_URL |
| Permission check | getCollaboratorPermissionLevel() |
getCollaboratorPermissionLevel() with repos.get() fallback on 403 |
| Git ref lookup | git.getRef() (/git/ref/heads/...) |
repos.getBranch() (/git/refs is plural on Forgejo) |
Webhook & Event Differences
Forgejo's webhook payloads and event naming diverge from GitHub in several ways. These are handled automatically by the action but important to understand when debugging:
| Behavior | GitHub | Forgejo |
|---|---|---|
| PR review comment event name | pull_request_review_comment |
pull_request_comment (same event for both review comments and reviews) |
| PR review event name | pull_request_review |
pull_request_comment (merged with review comments) |
| Issue label event action | labeled |
label_updated (normalized by FORGEJO_EVENT_ACTION_MAP in context.ts) |
| Issue assigned event action | assigned |
assigned (matches GitHub — no normalization needed) |
| Review comment payload | Top-level comment object present |
No top-level comment — tracking comment falls back to issue comment |
| Assigned payload | Top-level assignee object present |
No top-level assignee — must read from issue.assignee |
| PR compare URL parameter | quick_pull=1 |
expand=1 |
Workflow if: body fields |
github.event.comment.body populated |
Not populated for pull_request_comment events |
Workflow if: action field |
github.event.action populated for issues events |
Not populated — if: cannot gate on action |
Workflow if: expression parser |
Allows # comments |
Rejects # as invalid token — comments must go outside if: block |
| Creating issue with label | Fires opened only |
Fires both opened and label_updated as separate events |
| Assignees API | PATCH /issues/{id} with {"assignees": [...]} (array) |
Array form returns 404 — must use singular {"assignee": "user"} |
Practical impact: The @claude mention check and event action validation happen in application code (trigger.ts, context.ts) rather than the workflow if: condition. The workflow lets all events through and the action decides what to act on.
Bun Installation
The upstream action uses oven-sh/setup-bun. On Forgejo, this is replaced with a universal curl install because Forgejo runners pre-fetch all uses: action references before evaluating if: conditions, causing failures for GitHub-hosted actions that aren't mirrored.
Comment URLs
Comment tracking URLs (job links, branch links, PR creation links) use Forgejo URL patterns instead of GitHub patterns. This is handled automatically.
Action Inputs
The platform input is the only Forgejo-specific addition:
platform:
description: "Platform to run on: 'auto' (default auto-detects), 'github', or 'forgejo'"
required: false
default: "auto"
All other inputs work identically on both platforms. See the Usage Guide 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_toolswas not defined inaction.yml, so the env var was never set- Tag mode only merged
mcp__github_*tools fromclaude_args, filtering out everything else run.tsnever passedallowedTools/disallowedToolstorunClaude()createPrompt()hardcodedWebSearchandWebFetchas disallowed with no way to override
What this fork fixes:
- Added
allowed_toolsanddisallowed_toolsas proper action inputs - User tools are merged into the tag-mode tool list
disallowedToolsis passed through to the SDKWebSearch/WebFetchare removed from the disallowed list when included inallowed_toolsrun.tsnow passesALLOWED_TOOLS_INPUTasoptions.allowedToolstorunClaude(), fixing agent mode where tools were silently ignored
Important: Bash sub-patterns are required for CI auto-approval. Bare Bash in allowed_tools makes the tool available but individual commands still require interactive approval (which can't happen in CI — there's nobody to click "approve"). Use sub-patterns to pre-approve command families:
- uses: actions/claude-code-action@main
with:
# ❌ WRONG — bare Bash requires interactive approval for every command
allowed_tools: "Bash,Read,Write,Edit"
# ✅ CORRECT — sub-patterns auto-approve specific command families
allowed_tools: "Bash(git:*),Bash(bun:*),Bash(curl:*),Read,Write,Edit"
Common sub-patterns:
Bash(git:*)— all git commands (push, commit, add, etc.)Bash(bun:*)— all bun commands (test, run, install, etc.)Bash(curl:*)— curl for API callsBash(npm test:*)— specific command with any args
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.
Creating a dedicated bot user solves this:
- Clear attribution: Commits and comments show as
claude, not your personal account - Least privilege: The bot token has only the permissions Claude needs
- Audit trail: Easy to distinguish human actions from bot actions in repo history
- Security: If the action is compromised, only the bot's scoped token is exposed
Setup Steps
1. Create the bot user
Go to Site Administration > User Accounts > Create User Account or use the admin API:
- Username:
claude - Email: e.g.,
claude@yourdomain.com - Password: any strong password (you'll use API tokens, not password login)
- Uncheck "Require user to change password"
- Leave "Admin" unchecked
- Optionally set a display name and avatar to distinguish from human users
2. Add to organizations via teams
For each organization where Claude should operate:
- Go to Org Settings > Teams > New Team
- Create a team named
Claudewith:- Permission:
Write - Access: All Repositories (so new repos are automatically covered)
- Permission:
- Add
claudeto the team's Members
3. Generate a scoped API token
Log in as claude, go to Settings > Applications, and create a token:
- Token Name:
claude-code-action - Scopes:
repo— read/write (issues, PRs, comments, branches, commits)user:read— needed for API identity- Leave
admin,organization,packageunchecked
4. Store the token as an org-level secret
Add the token as CLAUDE_BOT_TOKEN at the organization level (Settings > Actions > Secrets) with visibility set to "All repositories". This way all repos in the org share the same secret.
5. Get the bot user's ID
You'll need the Forgejo user ID for the bot_id input:
curl -s "https://your-forgejo-instance/api/v1/users/claude" | jq '.id'
6. Configure the workflow
- uses: https://git.wxs.ro/actions/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
platform: "forgejo"
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}
bot_id: "YOUR_BOT_ID" # Replace with the ID from step 5
bot_name: "claude"
bot_email: "claude@yourdomain.com" # Must match the bot user's registered email
The bot_email input overrides the default noreply email format. When it matches the bot user's registered email in Forgejo, commits will be linked to the bot's profile in the UI.
Token Rotation
Forgejo API tokens can expire. Plan for rotation:
- Set a calendar reminder for before the token expires
- Generate a new token by logging in as
claude(same scopes as above) - Update the org secret
CLAUDE_BOT_TOKENwith the new token value - No code changes needed — the workflow reads the secret at runtime
Alternatively, generate a token without an expiration date (if your Forgejo instance allows it).
Included CI Workflows
This fork includes four Forgejo-native workflows in .forgejo/workflows/:
claude.yml — Tag Mode (@claude Mentions)
Responds to @claude mentions on issues, PR review comments, and PR reviews. Also triggers on issue assignment and label events. This is the main interactive workflow for using Claude on Forgejo.
ci.yml — Tests, Formatting, Type Checking
Runs on push to main and pull requests. Three parallel jobs:
test—bun test(withGITHUB_SERVER_URLoverride so GitHub-context unit tests pass)prettier—bun run format:checktypecheck—bun run typecheck
test-action.yml — Action Self-Test
Tests the action itself on Forgejo in agent mode. Runs weekly and on PRs that modify src/, action.yml, .forgejo/, or base-action/. Validates:
- Claude Code executes successfully with
platform: "forgejo" - Output file is produced with expected content
upstream-sync.yml — Upstream Sync Check
Runs weekly. Compares main with upstream/main (anthropics/claude-code-action). If upstream has new commits:
- Creates or updates an issue with the
upstream-synclabel - Includes commit log, diff stats, and merge instructions
- Lists conflict-prone files
Merging Upstream Changes
This fork is designed for easy upstream merges. Only 4-5 files have Forgejo-specific modifications, and changes are placed at function entry points using an early-return pattern.
Merge Process
# Fetch latest upstream
git fetch upstream main
# Merge into your main branch
git merge upstream/main
# If conflicts occur, they'll be in these files:
# - src/entrypoints/run.ts (platform branch at top)
# - src/modes/tag/index.ts (data fetcher swap)
# - src/mcp/install-mcp-server.ts (platform routing)
# - action.yml (platform input, bun install)
# Resolve conflicts, keeping both the upstream changes and
# the Forgejo platform branches at function entry points
# Run tests to verify
bun test
# Push
git push origin main
Conflict Mitigation
All Forgejo changes follow the early-return pattern — platform-specific logic is placed at the top of functions, before the existing GitHub code. This means:
- Upstream changes to the GitHub code path rarely conflict with Forgejo additions
- When conflicts do occur, they're localized to the few lines where the platform branch starts
- Resolution is straightforward: keep both the platform check and the upstream changes
What Happens When Upstream Adds New Features
New upstream features that use GraphQL or GitHub-specific APIs won't automatically work on Forgejo. The Forgejo data fetcher (src/forgejo/data/fetcher.ts) would need REST equivalents added. The action will still run — features that depend on missing data will simply lack that context.
E2E Test Results
All execution modes and trigger types have been validated end-to-end on a Forgejo instance (git.wxs.ro):
| Mode | Trigger | Status | Details |
|---|---|---|---|
| Agent mode | workflow_dispatch |
Validated | Platform detection, token auth, Claude execution, output file — all green |
| Tag mode | issue_comment (@claude) |
Validated | Issue context (title + body) read correctly, platform detected as forgejo, tracking comment created with spinner then updated with response (31s), job link URL correct |
| Tag mode | pull_request_review_comment |
Validated | PR diff and review comment body visible to Claude. Tracking comment created as issue comment (not review reply — see webhook quirks below). 3 bugs fixed: missing top-level comment in payload, event name mismatch, MCP comment routing |
| Tag mode | pull_request_review |
Validated | Claude responds to @claude in PR review body. Forgejo sends pull_request_comment instead of pull_request_review — the @claude check runs in trigger.ts, not the workflow if: condition |
| Tag mode | issues with labeled action |
Validated | Claude responds when the configured label is applied. 3 bugs fixed: workflow if: expression parser rejects # comments, github.event.action not populated in if: context, event action is label_updated not labeled |
| Tag mode | issues with assigned action |
Validated | Claude responds when assigned to an issue. 1 bug fixed: assignee field missing from top-level payload (nested under issue.assignee instead) |
| Tag mode | Code changes (branch + commits) | Validated | Claude creates branch, pushes commits, links branch in tracking comment. Bot identity (claude <claude@wxs.ro>) correct. PR compare URL uses expand=1 (Forgejo) instead of quick_pull=1 (GitHub) |
Tag Mode Verified Capabilities
@claudetrigger phrase detected in issue comments, PR review comments, and PR reviews- Issue body, title, PR diff, and review comment body fully visible to Claude
- Tracking comment created with progress spinner, then updated with final response
- Forgejo job run URL correctly formatted in tracking comment
- Branch creation works via
repos.getBranch()API (Forgejo uses/git/refsnot/git/ref) - Permission check passes via
repos.get()fallback - Label trigger fires when configured label is applied to an issue
- Assignee trigger fires when the bot user is assigned to an issue
- Code changes: branch creation, file commits, and PR compare URL all work correctly
- Bot identity (commits attributed to
claudeuser) verified via git log and API
Known Limitations
- GitHub Enterprise Server (GHES) misdetection: The tier-3 platform detection heuristic treats any
GITHUB_SERVER_URLthat isn'tgithub.comas Forgejo. GHES instances (e.g.,https://github.mycompany.com) will be misidentified. Setplatform: "github"explicitly when running on GHES. - Git ref API: Forgejo uses
/git/refs(plural) instead of GitHub's/git/ref(singular). The action handles this by usingrepos.getBranch()on Forgejo for branch SHA lookups. - User display name fetch:
GET /users/{username}returns 403 withoutread:userscope. Non-fatal — falls back to login name. pull_request_targetevent: May not exist in Forgejo. The action degrades gracefully (maps topull_request).- GitHub MCP server: The
ghcr.io/github/github-mcp-serverDocker image is GitHub-specific and is skipped on Forgejo. - Commit signing: SSH signing has not been tested on Forgejo. The
use_commit_signingoption (GitHub API signing) won't work since it uses GitHub's commit API. downloadJobLogForWorkflowRun: Forgejo's Actions API may lack this endpoint. The CI MCP server attempts it via Octokit REST but may return limited data.- Bot filtering:
allowed_botschecks won't matchclaude[bot]since Forgejo has no bot accounts. Bot-type actor validation is skipped on Forgejo, but self-trigger prevention is active — the action blocks events where the actor matches the configuredbot_nameto prevent infinite loops. pull_request_reviewtrigger is dead code: Forgejo sendspull_request_commentfor both review comments and review submissions. Thepull_request_reviewtrigger in the workflow never fires — all review-related events are handled through thepull_request_commentcodepath.- Review comment tracking comments are always issue comments: On Forgejo, review comment events lack the top-level
commentobject needed to create PR review comment replies. The tracking comment is always created as a regular issue comment instead. - Workflow
if:conditions are unreliable for Forgejo events: Forgejo does not populategithub.event.comment.body,github.event.review.body, orgithub.event.actionin the workflow expression context for several event types. All trigger validation happens in application code (trigger.ts), not in workflow conditions. - Assignees API (plural array form):
PATCH /repos/{owner}/{repo}/issues/{index}with{"assignees": ["user"]}returns 404 on Forgejo. Use the deprecated singular{"assignee": "user"}field instead.
Troubleshooting
"Resource not accessible by integration" errors
This usually means the token lacks permissions. On Forgejo:
- Use a personal access token with
reposcope via thegithub_tokeninput - The default
github.tokenin Forgejo workflows may have limited permissions depending on your runner configuration
Tests fail with platform detection errors
Unit tests assume GitHub context by default. When running on a Forgejo runner, set GITHUB_SERVER_URL=https://github.com before running tests (the CI workflow handles this automatically).
Bun installation fails
If curl -fsSL https://bun.sh/install | bash fails on your runner, pre-install Bun on the runner image or use the path_to_bun_executable input to point to a custom Bun binary.
Claude's tracking comment doesn't appear as a review reply
On Forgejo, review comment triggers (pull_request_review_comment) always create a regular issue comment — not a reply in the review thread. This is because Forgejo's webhook payload lacks the top-level comment object needed to create a review reply. The response still appears on the PR, just as a standalone comment.
Workflow if: condition skips events it shouldn't
Forgejo doesn't populate github.event.comment.body, github.event.review.body, or github.event.action in the workflow expression context for pull_request_comment and issues events. If your workflow if: condition checks these fields, events will be silently skipped. The fix is to let all events through and handle validation in trigger.ts instead.
Workflow if: expression fails with "invalid token" error
Forgejo's expression parser may reject # characters as invalid tokens when using | (literal block) scalars, since each line is preserved and # starts a comment that extends to end-of-line. With >- (folded scalar), newlines become spaces, and # comments may be tolerated by some Forgejo versions — but for maximum compatibility, move comments outside the if: block entirely.
Label trigger fires twice when creating issue with label
If you create an issue with a label in one operation, Forgejo fires both opened and label_updated as separate events. The action handles this correctly — it only checks labels on label_updated events, not on opened.
mock.module() breaks tests on Forgejo CI (Linux Bun)
Bun's mock.module() replaces the module globally in the process module registry. With Bun's non-deterministic test file ordering on Linux, this causes cross-file pollution: one test file's mock leaks into another. Use dependency injection or spyOn on individual functions instead. If the real function is safe to call (e.g., returns empty results for test data), avoid mocking entirely.
Upstream workflows run and fail
If .github/workflows/ from upstream are executing on your Forgejo runner, configure the runner to only read from .forgejo/workflows/. This is a runner-level setting — see your Forgejo instance's runner configuration.