Skip to main content
Fabro supports two GitHub integration strategies:
  • token — the default for local and individual use. Fabro captures gh auth token during fabro install, stores it as GITHUB_TOKEN, and uses that token directly for repo access, pull requests, and sandbox GITHUB_TOKEN injection.
  • app — the team-oriented option. Fabro registers a GitHub App, uses installation tokens for repo access, enables browser OAuth, and supports webhook delivery when you configure a webhook strategy.
token changes GitHub integration auth only. It does not provide browser sign-in, so the embedded web UI is disabled when strategy = "token".

Strategy matrix

GitHub App mode

The rest of this page describes the app strategy, which is required for browser auth and for any webhook delivery strategy.

Setup

Prerequisites

  • A GitHub account (personal or organization)
  • Shell access to the host that runs the Fabro server
  • The Fabro web app running (cd apps/fabro-web && bun run dev) if you want browser sign-in after setup

Register the GitHub App with fabro install

Run the installer on the same machine that will run the Fabro server:
When you choose the GitHub App strategy, the CLI opens GitHub with a pre-filled App Manifest containing: These permissions are included when Fabro registers a new app. For an existing GitHub App, add the missing permissions in the app’s settings, then approve the permission update on each installation before workflows can use them. Fabro’s App manifest does not request the Workflows permission, so Apps registered through Fabro cannot publish changes under .github/workflows/. The installer:
  1. Lets you choose where to register the app. If you have the gh CLI installed, Fabro detects your GitHub username and any organizations you administer and lets you pick. If gh is not available, the installer prompts for a GitHub token and uses it to look up your identity and organizations.
  2. Opens GitHub in your browser so you can review the manifest and click Create GitHub App.
  3. Receives GitHub’s temporary callback on localhost, exchanges it for permanent app credentials, and writes:
    • app_id, client_id, and slug to ~/.fabro/settings.toml
    • GITHUB_APP_CLIENT_SECRET, GITHUB_APP_WEBHOOK_SECRET, and GITHUB_APP_PRIVATE_KEY to the server vault
  4. Prints the resulting app slug so you can install the app on the repositories Fabro should access.
After setup completes, restart the Fabro server before attempting browser login.

Install the app on GitHub

Go to https://github.com/settings/apps/<your-app-slug>/installations and install it on the repositories Fabro should access.

Verify the configuration

Run the doctor command to check that all GitHub App credentials are in place:
The GitHub App check verifies five fields: If all five are set, the check passes. If none are set, it warns (GitHub integration is optional). If some are set but others are missing, it errors with the specific missing fields.

Configuration

The GitHub App configuration lives in two places:

~/.fabro/settings.toml

settings.toml

Server vault

Fabro stores the GitHub App secrets in the server vault under these keys:
  • GITHUB_APP_CLIENT_SECRET
  • GITHUB_APP_WEBHOOK_SECRET
  • GITHUB_APP_PRIVATE_KEY
The private key is stored as base64-encoded PEM. Fabro also accepts raw PEM format (starting with -----BEGIN). Install mode writes these automatically. If you rotate them manually, use fabro secret set on the server:

Webhook delivery strategies

Fabro receives GitHub webhooks on POST /api/v1/webhooks/github whenever GITHUB_APP_WEBHOOK_SECRET is configured. The webhook strategy controls how that route becomes reachable from GitHub:
settings.toml
  • server_url: recommended for production. Fabro assumes server.api.url is already publicly reachable and best-effort updates the GitHub App webhook URL to <server.api.url>/api/v1/webhooks/github each time fabro server start runs.
  • tailscale_funnel: opt-in for machines reachable through Tailscale but not through a stable public URL. Fabro runs tailscale funnel against the main server port and best-effort updates the GitHub App webhook URL to the resulting Funnel origin.
  • Unset strategy: Fabro still serves the webhook route if the secret is present, but it does not expose the route for you and does not mutate the GitHub App webhook URL.
tailscale_funnel has host-wide side effects: it changes Tailscale Funnel state on the server and rewrites the GitHub App webhook URL on startup. Use server_url when you already have HTTPS and a stable hostname.

Reconfigure GitHub after install

To switch GitHub strategies or re-register the app without re-running the full install wizard, use fabro install github:
This walks through just the GitHub setup steps — choosing a strategy, registering an app, or storing a token — and restarts the server when done. Stale settings and secrets from the previous strategy are cleaned up automatically.

token mode

Choose GitHub CLI in fabro install to use the default local-user flow. The installer:
  1. Runs gh auth token
  2. Stores the token as vault secret GITHUB_TOKEN
  3. Writes strategy = "token" under [server.integrations.github]
After install, Fabro reads GITHUB_TOKEN from the server vault only. To update it after install, run:
Short-lived GitHub installation tokens (ghs_*) are rejected as static tokens; use a PAT for token mode, or use GitHub App mode so Fabro can refresh installation tokens for you. In this mode, Fabro disables the embedded web UI and browser auth routes. Machine API routes and /health continue to work.

How it works

OAuth login

The web app uses the GitHub App’s OAuth credentials to authenticate users:
  1. User clicks Sign in with GitHub on the login page
  2. Fabro redirects to GitHub’s OAuth authorization endpoint with scopes read:user and user:email
  3. User authorizes the app on GitHub
  4. GitHub redirects back with an authorization code
  5. Fabro exchanges the code for an access token and fetches the user’s profile and verified email
  6. Fabro checks the username against the allowed_usernames list in settings.toml
Configure allowed users in settings.toml:
settings.toml
An empty allowed_usernames list rejects all users.

Repository cloning in sandboxes

When a run targets a GitHub repository, its workspace is checked out inside the sandbox (Docker or Daytona) before the first stage runs:
  1. When the run’s worker starts, it signs a short-lived JWT using the App ID and private key (RS256, 10-minute validity)
  2. Using the JWT, Fabro looks up the GitHub App installation for the repository (GET /repos/\{owner\}/\{repo\}/installation)
  3. Fabro requests a scoped Installation Access Token with contents: read permission on the specific repository
  4. Inside the sandbox, Fabro fetches the selected revision from https://github.com/<owner>/<repo> at the run’s [run.clone] depth, presenting the token as an HTTP header on that one command, and checks out the working branch
  5. The workspace’s origin is the plain HTTPS URL: the token is never written into the repository, its configuration, or its remote
The files belong to the user the sandbox runs commands as. For public repositories the fetch works without credentials when none are configured.

Git targets for run intents

The RunIntent create body always names a GitHub repository and a working branch. It may also select a bare tag, pin a full 40-character commit SHA, or include both: branch is always the attached branch inside the sandbox. tag is a bare tag name such as v1.2.3; refs/tags/v1.2.3 and tags/v1.2.3 are rejected. An unpinned tag is resolved when the worker starts, so moving a tag before that point changes the selected commit. Creating the run validates the selectors and lowercase-normalizes sha, but does not contact GitHub or prove ancestry. An exact SHA is authoritative: Fabro does not prove it belongs to the branch or matches the accompanying tag. If a requested tag or exact commit is unavailable, sandbox setup fails without falling back to a same-named branch or the branch’s newer HEAD.

GITHUB_TOKEN injection

When any settings layer declares [run.integrations.github.permissions], Fabro prepares a scoped GitHub App token source and exposes it as the GITHUB_TOKEN environment variable in sandbox command and agent execution. Agents running inside the sandbox can use this token for GitHub API calls and pushes within the granted permissions. The GitHub CLI (gh) reads GITHUB_TOKEN automatically, so command stages can run gh pr list, gh issue create, and similar commands without an explicit gh auth login.
workflow.toml
Only the listed permissions are requested. If no GitHub credentials are configured, Fabro does not inject a managed token. If configured credentials cannot resolve a token, the execution scope fails to initialize. GitHub-target runs also receive Git read access to their origin without declaring an integration permission map. In App mode this default token requests only contents = "read", and it is used by Git’s credential helper. Declaring integration permissions additionally supplies GITHUB_TOKEN to command and agent processes. The managed token carries exactly the access the run declares, so it replaces any GITHUB_TOKEN set in the workflow environment, an ACP agent’s environment or the sandbox’s own environment. To give a stage different GitHub access, change the declared permissions or repositories instead. One-shot containers a stage runs (such as docker:// steps) receive the managed GITHUB_TOKEN but not the Git credential helper, which reads a store inside the stage’s sandbox. In App mode, the token covers only the run’s origin repository unless the run declares additional repositories. Injecting GITHUB_TOKEN alone does not make other private repositories reachable.

Additional repositories

A run can declare extra GitHub repositories that its stages may access through the same GITHUB_TOKEN:
workflow.toml
The run origin stays implicit — never list it. Each entry is a full owner/repository slug (no scheme, host, ref, or extra path component). Fabro mints one installation token scoped to the origin plus every declared repository, with the one shared permissions map applying to all of them. What works against every declared repository, within the granted permissions:
  • gh CLI and raw GitHub API calls through GITHUB_TOKEN.
  • Plain Git over HTTPS (git clone https://github.com/owner/repo), through a credential helper that reads a private, renewable store outside the workspace.
  • The common SSH spellings git@github.com:owner/repo[.git] and ssh://git@github.com/owner/repo[.git], through SSH-to-HTTPS rewrites injected into the stage environment.
Fabro does not clone additional repositories for you; a workflow that needs one on disk adds its own clone step (git clone https://github.com/owner/repo or gh repo clone owner/repo). Requirements and validation:
  • Every repository in the effective set must share one owner and be reachable by the origin repository’s GitHub App installation, because one App installation covers one account. Cross-owner declarations fail configuration validation; a same-owner repository outside the installation fails preflight and run initialization with the repository named.
  • A non-empty additional_repositories requires contents = "read" or contents = "write" in the permission map.
  • Malformed slugs, duplicates (repository identity is case-insensitive), and sets larger than 499 entries fail configuration validation with indexed error paths.
  • Unlike permissions-only configuration, declared additional repositories are a hard requirement: missing GitHub credentials, a missing origin, or an inaccessible declared repository fails preflight and run initialization instead of continuing without the token.
  • Layering: the higher-precedence additional_repositories list replaces the lower one wholesale (no union, no ... splice), and additional_repositories = [] explicitly clears an inherited list. permissions keeps its existing whole-map replacement behavior. If layering leaves repositories declared with permissions cleared, configuration resolution reports the invalid combination.
Behavior notes:
  • Token strategy (PAT): the configured PAT is used as-is. The repository list drives validation and preflight probes, but it cannot narrow the PAT’s inherent GitHub scope — App mode remains the least-authority option.
  • GH_TOKEN precedence: gh checks GH_TOKEN before GITHUB_TOKEN. If the resolved run environment defines GH_TOKEN, gh uses it instead of the managed token; Fabro never sets or removes GH_TOKEN, and preflight warns when additional repositories are declared alongside one.
  • GitHub SSH URLs are rewritten to HTTPS. The helper supplies credentials only for the origin and declared repository paths, including their .git spellings. An undeclared private repository does not receive a credential from this helper.

Security boundary

Workflow authors may name any repository reachable by the server’s GitHub App installation; Fabro applies no second server-side repository intersection. The token is scoped server-side to exactly the declared set — a request to an undeclared repository fails at GitHub, and Fabro never mints an unscoped installation-wide token. With contents = "write", any stage can push to any declared repository. Declare the smallest repository set and the weakest permissions that work. Installation Access Tokens are short-lived. Fabro reuses cached tokens until they are within ten minutes of expiry. Each process launch resolves its current credentials, and a background task checks the Git credential store every minute while the execution scope is acquired. Command stages, native agent tools, ACP agents, and resumed scopes use the same mechanism. A long-lived ACP agent’s later Git operations read the renewed store. The token never appears in the repository’s remote URL, Git configuration, or workspace snapshots; the private store is removed when the scope releases. Background refresh failures are logged and retried, and a still-valid cached token remains usable. GITHUB_TOKEN is a process environment variable, so a process that is already running keeps its launch-time value. The renewable Git helper continues to work across token rotation, but a long-running process that calls the GitHub API through GITHUB_TOKEN must restart or obtain a new token separately. Static PATs cannot be renewed by Fabro. Publication uses a separate token source with the permissions needed to push and open pull requests. The permissions table follows the standard layer-merge order (workflow > project > user > defaults). Set defaults at [run.integrations.github.permissions] in ~/.fabro/settings.toml so every run inherits a baseline; tighten or override per-workflow as needed. A higher layer that defines permissions = {} clears the inherited map (no token requested).

Security model

The upper bound on what Fabro will mint is whatever permissions the GitHub App installation has been granted. Fabro does not impose a separate server-side cap on the run-level permissions map: any value the App has been granted can be requested by run config. Operators must not run untrusted workflow, project, or user TOML against a broadly-scoped GitHub App installation. Preflight prints the resolved permission set so reviewers can see what each run will request.

Commit attribution

Every commit a run creates is authored and committed by the run’s GitHub credential identity. In App mode that is the App’s bot account, <slug>[bot] <id+slug[bot]@users.noreply.github.com>, so GitHub attributes checkpoint commits and workflow-created commits to the App. In token mode it is the token’s user with their GitHub noreply address. Fabro resolves the identity once per run and passes it to every command stage, agent tool, and ACP agent as GIT_AUTHOR_* / GIT_COMMITTER_* variables, so a workflow that clones or initializes another repository commits as the same identity without configuring Git itself. [run.git.author] overrides either field; see server configuration.

Checkpoint pushing

After each workflow stage, Fabro checkpoints the workspace on the run branch, fabro/run/<run-id>, and pushes it directly from that workspace. Fabro keeps no second Git repository or checkpoint bundles on the server. In App mode, the worker pushes with an Installation Access Token with contents: write, reusing one token until it nears expiry and then minting the next, and passes it only to the sandbox Git command. The token is not saved in the repository’s remote URL or configuration. [run.run_branch] push = false keeps the branch only in the run workspace. An intermediate push failure logs a warning; later checkpoints and final publication retry the push. After a successful run’s last stage and before the run finishes, Fabro awaits one final push. When pull request creation is enabled and the run changed files, Fabro then checks that GitHub reports the run branch at the exact final commit and opens the pull request. A failed final push, publication preparation, branch check, or PR creation marks the run as failed with publish_failed; the terminal run event is emitted only after this step finishes.

Troubleshooting

”GitHub App is not installed for {owner}”

The GitHub App exists but hasn’t been installed on the organization or user account that owns the repository. Install it at:
Or for personal accounts:

“GitHub App is private but this repo belongs to a different owner”

GitHub Apps are private by default, meaning they can only be installed on repositories owned by the same account or organization that owns the app. If you need to install the app on a repository owned by a different user or organization, you must make the app public:
  1. Go to GitHub → Settings → Developer settings → GitHub Apps → {your app}
  2. Scroll to the bottom under Danger zone
  3. Click Make public
Once the app is public, anyone with the installation link can install it on their repositories. This does not grant the app any additional permissions — repository owners still choose which repositories the app can access during installation.

”GitHub App installation is suspended”

The installation was disabled in GitHub’s settings. Re-enable it in the organization’s GitHub App settings.

”GitHub App does not have access to repository {repo}”

The app is installed but doesn’t have access to this specific repository. Update the installation’s repository permissions to include it (the app may be configured for “Only select repositories”).

”GitHub App authentication failed”

The app_id in settings.toml or the GITHUB_APP_PRIVATE_KEY vault secret is incorrect. Re-run fabro install on the server host or verify the values match your GitHub App.

Clone fails for private repositories

If you see Git clone failed ... If this is a private repository, configure a GitHub App, the GitHub App credentials are not configured. Run fabro install on the server host or verify with fabro doctor.