Forgejo compatibility GitHub-shaped CLI output Rust runtime Codex.app friendly

A beginner's map of the Forgejo shim

gh-forgejo-shim is a small compatibility layer for tools that know how to ask GitHub-style questions but are running inside Forgejo repositories. It routes the commands it understands to Forgejo, translates the answers into the shapes those tools expect, and delegates everything else to the real GitHub CLI.

Honest scope: this is not a complete GitHub CLI clone. It supports the gh calls and repository probes that developer tools commonly use for repository, pull request, issue, check, branch, and auth workflows.

What a shim is

A shim is an adapter that sits between a caller and the tool or service the caller thinks it is using. The caller keeps speaking the same language. The shim decides whether it can translate that request, and either handles it or passes it through.

The caller
A human, Codex.app, T3 Code, another editor, or a script runs a command like gh pr status --json number,title,url.
The expected language
Those tools expect GitHub CLI command names, flags, exit codes, text output, and JSON fields.
The actual service
The repository is hosted on Forgejo, whose API and web URLs are similar but not identical to GitHub's.
The shim
This project receives selected gh calls, talks to Forgejo when appropriate, and returns enough GitHub-shaped output for the caller to keep working.

What this project does

The product has two command names and one optional wrapper. The long command, gh-forgejo-shim, is the management command. The shorter gfj alias is for daily use. The optional managed gh wrapper is what lets GitHub-oriented tools find the shim automatically.

Management and setup

Use gfj bootstrap, gfj doctor, gfj auth login, gfj config add-host, and rollback commands to install, verify, and remove shim-owned state.

GitHub-style compatibility

When the wrapper is first in PATH, a supported gh command in an allowlisted Forgejo repo can be handled by the shim instead of the real GitHub CLI.

Important guardrail: known GitHub hosts such as github.com still delegate to the real gh. A Forgejo host must be explicitly allowlisted before routed commands use Forgejo behavior.

Call flow

The easiest way to understand the project is to follow a single command from the app to the final output.

A typical routed gh call

The app does not need to know about Forgejo. It receives the same kind of answer it expected from gh, or it receives a clear error when the shim cannot honestly support the request.

How Codex and other apps make calls

GUI coding tools usually do not link to this Rust crate. They launch external commands in a working directory. That means setup, PATH, auth, and local Git shape matter as much as the Forgejo API code.

1

The app starts in a repository

Codex or another tool opens a checkout and inspects it with commands like git status --short --branch, git remote get-url origin, and git rev-parse --abbrev-ref --symbolic-full-name @{u}.

2

The app runs GitHub CLI probes

It may call gh --version, gh auth status, gh api user, gh repo view, gh pr status, gh pr list, or gh issue list to decide which UI and workflow features are available.

3

PATH chooses the wrapper

If the managed gh wrapper appears before the real gh, the call enters this shim. On macOS, GUI apps launched from Finder, Dock, Spotlight, Raycast, or Alfred may need gfj install-gui-path so they inherit the same tool paths your shell sees.

4

The shim routes or delegates

A Forgejo checkout on an allowlisted host routes selected commands through the Forgejo API. A GitHub checkout, unknown command, or non-allowlisted host delegates to the real GitHub CLI.

5

The app consumes normal-looking output

For JSON calls, the shim filters and names fields the way GitHub CLI callers expect: nameWithOwner, defaultBranchRef, currentBranch, statusCheckRollup, and related fields.

Raw Git is separate: the shim normally intercepts gh, not git. If an app's branch UI depends on raw Git calls, fix the checkout's origin, origin/HEAD, and branch upstream shape too.

Routing rules

The dispatcher is conservative. It only routes a call when the command is in the supported surface and the repository host is an allowlisted Forgejo host.

Condition Result Why
gh pr view in an allowlisted Forgejo repo Forgejo route The command is supported and the repository host is configured as Forgejo.
gh repo view -R git.example.com/owner/repo Forgejo route -R gives the shim an explicit host, owner, and repository.
gh auth status --hostname git.example.com Forgejo route Auth probes can route by explicit hostname or by the current repo.
gh api user with GH_HOST=git.example.com Forgejo route The shim supports the user probe because apps use it to verify auth.
gh release list Delegate Release commands are not part of the supported Forgejo compatibility surface.
gh pr view in a GitHub repo Delegate Known GitHub hosts are protected so normal GitHub work stays native.
Supported command with unsupported Forgejo-only flag handling Explicit error Once routed, unsupported flags fail clearly instead of pretending to work.

Repository detection order

The shim looks for repository identity in this order:

  1. -R or --repo on the command line.
  2. A repository URL passed as a command argument.
  3. GH_REPO, optionally combined with GH_HOST.
  4. GH_HOST plus local Git remote owner and repository data.
  5. Local Git remotes, starting with origin.

Supported command families

gh repo view gh pr view gh pr status gh pr list gh pr checks gh pr create gh pr comment gh pr checkout gh pr diff gh issue list gh issue view gh issue create gh auth status gh auth token gh api user

How normalization works

Normalization is the translation step. Forgejo returns Forgejo-shaped API objects. GitHub-oriented callers ask for GitHub CLI-shaped fields. The shim maps the useful parts, fills safe defaults where GitHub has concepts Forgejo does not expose the same way, and filters the response to the requested --json fields.

Forgejo source GitHub-shaped output What the caller gets
html_url or url url A web URL field where apps expect to link a PR, issue, or repository.
created_at, updated_at createdAt, updatedAt GitHub-style camelCase timestamps.
user.login, user.username, or user.full_name author.login, author.name A stable author object for list and view displays.
Forgejo commit statuses for a PR head SHA statusCheckRollup and gh pr checks Check names, states, conclusions, descriptions, and links in the buckets apps recognize.
Repository owner, name, default branch, topics, features, permissions nameWithOwner, defaultBranchRef, viewerPermission, and related repo fields Enough repository metadata for GitHub-style project detection.
No open PR for the current branch {} for pr view --json or "currentBranch": null for pr status --json A successful empty result instead of a broken app state.

Text output

Human-readable commands print compact lines or URLs that resemble GitHub CLI output closely enough for everyday terminal use.

JSON output

When --json is present, the shim returns selected fields, supports --jq and templates where implemented, and avoids dumping raw Forgejo payloads.

Compatibility detail: created Forgejo PR URLs include a harmless fragment like #codex-pr=/pull/7 so apps that look for GitHub-style /pull/7 markers can still surface the new PR link.

Implementation map

These are the main source files behind the behavior. Start with the dispatcher, then read the handlers and normalization layer.

File Responsibility
shim.rs Builds and removes the managed gh wrapper. Refuses to overwrite unrelated files unless --force is used.
routing.rs Loads config, checks allowlisted hosts, detects supported commands, chooses Forgejo routing or real-gh delegation, and records route traces.
repo.rs Parses repository specs from flags, URLs, environment variables, and Git remotes.
auth.rs Finds Forgejo tokens from environment variables, shim storage, macOS Keychain, and common fj, tea, or gitea config files.
read_only.rs Implements the routed gh repo, gh pr, gh issue, gh auth, and gh api user command behavior.
forgejo.rs Talks to Forgejo's /api/v1 endpoints for repositories, PRs, issues, labels, comments, users, diffs, files, and statuses.
normalize.rs Transforms Forgejo objects into GitHub-shaped repository, PR, issue, and check objects.
create.rs Parses PR creation flags, derives default base/head branches, reads body files, and formats created PR URLs for app compatibility.
bootstrap.rs Runs first-time setup: detects the repo, allowlists the host, installs the wrapper, checks PATH, checks auth, and prints repair commands.
doctor.rs Reports setup health for real gh, fj, allowlisted hosts, auth, wrapper order, current repo, and macOS GUI PATH.
trace.rs Writes redacted JSONL trace records with argv, cwd, route decision, host, repo, duration, and exit code when tracing is enabled.

Setup and safety model

The project tries to be recoverable. It owns specific files, marks the wrapper it creates, and keeps normal GitHub CLI behavior available.

First setup

gfj bootstrap
gfj auth login git.example.com
gfj doctor

bootstrap detects the current repo, allowlists the host when it is not GitHub, installs the wrapper, and prints repair commands for anything it cannot fix.

Manual setup

gfj config add-host git.example.com
gfj install-shim
gfj doctor

Manual setup is useful when you want to control exactly when the host allowlist or wrapper path changes.

Auth lookup order

  1. Current-process environment variables: FJ_SHIM_TOKEN, FORGEJO_TOKEN, GITEA_TOKEN, then FJ_TOKEN.
  2. Shim-owned token storage: macOS Keychain when available, otherwise ~/.config/gh-forgejo-shim/auth.json.
  3. Best-effort imports from common fj, tea, and gitea config files.

Rollback

gfj uninstall-shim
gfj uninstall-gui-path
gfj auth logout git.example.com

These commands remove shim-owned integration points. They do not uninstall the real GitHub CLI, delete your repository, or remove unrelated files.

Trace and debug

When an app says GitHub CLI is unavailable, PR status is missing, or branch state is stale, capture the actual commands before changing compatibility code.

export FJ_SHIM_TRACE="$PWD/codex-probe-trace.jsonl"
export FJ_SHIM_TRACE_BODY=1
gfj trace smoke
gfj trace summarize codex-probe-trace.jsonl

Trace records include the command, working directory, route decision, host, repository, duration, exit code, output sizes, and redacted excerpts when body tracing is enabled. Sensitive token-like values are redacted, and gh auth token output is not exposed in trace excerpts.

Debugging rule: use traces to prove the failing command shape. Do not add broad compatibility just because a tool says "GitHub CLI unavailable." The fix should match the observed argv, cwd, route decision, and output expectation.

When raw Git calls matter

Some app checks never call gh. For one launch session, the project can create a temporary git recorder wrapper that forwards to real Git and appends records to the same trace file.

gfj trace git-recorder create "$PWD/codex-probe-trace.jsonl"
gfj trace git-recorder remove /path/to/printed/wrapper-dir