GitHub
Connecting GitHub lets coding agents clone, push to, and open pull requests on the repositories you pick. It runs through one GitHub App per box. You set it up in three steps:
- A platform admin creates the App, once per box.
- An operator connects their GitHub account under Tools.
- The operator picks the repositories agents may use.
Before you start
- You are a platform admin on the box, for step 1.
- The box's public address is set. The App sends GitHub logins back to
<PUBLIC_BASE_URL>/api/mcp/oauth/callback. WhenPUBLIC_BASE_URLis empty, that is the address you reach the box at, such ashttp://localhost:8080. To reach the box from elsewhere, set the address first (Remote access).
Step 1: Create the App
- Open Admin, Platform keys, and expand GitHub App.
- Click Create the GitHub App.
- Fill in the dialog, or leave it empty:
- App name: shown on GitHub, where it must be unique. 34 characters at most. The default is
Guilds (<host>). - Organisation: the GitHub organisation that owns the App. Empty creates it on your own account.
- App name: shown on GitHub, where it must be unique. 34 characters at most. The default is
- Click Continue on GitHub, and confirm the App on GitHub.
GitHub sends you back to Platform keys, and GitHub App reads On. If it shows Couldn't create the GitHub App instead, nothing was stored: start again.
The App asks for read and write access to Contents and Pull requests, and read access to Metadata. It is private and receives no webhooks.
Step 2: Connect GitHub
- Open Tools at the organisation, the guild, or the agent.
- On the GitHub card, click Connect.
- On GitHub, authorize the App.
- If the App is not installed on your account yet, GitHub asks next which account to install it on and which repositories it may reach.
GitHub sends you back with the card connected and showing the account. The App is installed once per GitHub account: a later Connect, from another guild or agent, reuses that installation. When you reach several installations, a page asks which account to use.
When the GitHub organisation needs an admin to approve the install, click Connect again once they have.
Step 3: Pick the repositories
On the GitHub card, pick each repository from From GitHub · <account>. The list holds the repositories the installation reaches. To see another one, give the App access to it on GitHub.
A guild or agent without its own login uses its organisation's repositories, and an agent its guild's. Connecting a login at the narrower scope lets it pick a different set.
The guild's coding agents clone these repositories at their next run (Repositories, Add a coding agent).
Registering the App by hand
Instead of step 1, you can register the App yourself on GitHub (Settings, Developer settings, GitHub Apps):
- Create an App with:
- Callback URL:
<PUBLIC_BASE_URL>/api/mcp/oauth/callback - Request user authorization (OAuth) during installation checked. Without it, GitHub never sends you back to the box after an install, and Connect never completes.
- Redirect on update checked
- Webhook inactive
- Repository permissions: Contents and Pull requests read and write, Metadata read
- Only on this account for where it can be installed
- Callback URL:
- Generate a client secret and a private key.
- On Platform keys, GitHub App, enter:
- App ID
- App slug: the last segment of
https://github.com/apps/<slug> - Private key (PEM): the file's content as is
- Client ID
- Client secret
- Click Save.
Troubleshooting
The GitHub window stays on GitHub after you validate. The App does not request user authorization during installation. On GitHub, open the App's settings, check Request user authorization (OAuth) during installation, and save. Then uninstall the App from your account and click Connect again.
GitHub sends you to a page that is not the box. The App's callback URL is not the box's address. Set PUBLIC_BASE_URL, and make <PUBLIC_BASE_URL>/api/mcp/oauth/callback the App's first callback URL (Remote access).
Replacing the App
An App with another App ID, or removing the App, disconnects every GitHub login installed with the old one. The form asks first, with the count. A new private key or client secret for the same App disconnects nothing.
Reference
Setup state
The GitHub App integration stores app_id, slug, private_key, client_id, and client_secret. Until all five are set, Tools marks the connector Not set up with the missing fields and disables Connect. A Connect sent anyway is a 422 with setup_state: unsupported (Platform keys). A replacing write without confirm_replace is a 409 with replaces_app: true and connections. The private key accepts one line with \n escapes too.
Manifest flow
POST /api/admin/instance/keys/github-app/manifest with { name?, owner?, return_to? } answers { action, manifest, state } and sets the guilds_github_manifest cookie (HttpOnly, SameSite=Lax, 20 minutes, path = the callback). The browser posts the manifest to action.
The manifest carries the settings above: public: false, request_oauth_on_install: true, setup_on_update: true, and the permissions contents: write, pull_requests: write, and metadata: read. It sets no setup URL. Its url is the box's address. Its callback URLs are <PUBLIC_BASE_URL>/api/mcp/oauth/callback and, when the admin reached the box at another address, that address's callback too. A box on the public internet also names an inactive webhook at <address>/api/github/webhook. A loopback or private address omits it: GitHub refuses a hook URL it cannot reach.
GitHub redirects to GET /api/admin/instance/keys/github-app/manifest/callback?code&state. The callback checks that the cookie's state matches a flow this process started, trades the code once at POST https://api.github.com/app-manifests/{code}/conversions, and stores the fields for the admin who started the flow. It redirects to return_to (a path on this app only) or to /admin?tab=instance&integration=github-app. A failure redirects there with setup_error=state|conversion|store and stores nothing (Security register, SEC-22).
Connect flow
Connect follows the pending-login path of the browser logins (OAuth). GitHub connects at the organisation, a guild, or an agent; any other scope is a 422.
- Connect creates a pending connection with its
oauth_stateand answers with user authorization at/login/oauth/authorize, with the callback URL and the state. The cookieguilds_github_oauth(HttpOnly, SameSite=Lax, 20 minutes, path/api/mcp/oauth/callback) keeps the state in case GitHub omits it after a later hop. Connect always starts with user authorization, never on the install page. - The orchestrator trades the
codefor a user token and keeps an installation only when that user reaches it: the callback'sinstallation_idwhen present, else the App's only installation on the token. Several open a picker (Installation picker). None redirects tohttps://github.com/apps/<slug>/installations/new?state=…, whose own authorization hop returns a newcodewith theinstallation_id, traded withoutredirect_uri. A setup redirect (setup_actioninstallorupdate) withinstallation_idand nocodestarts user authorization with the samestate. An id in the URL alone proves nothing. The user token is discarded. A refused callback deletes a new pending connection. - The connection stores the installation id, takes the account's login as
account_hint, becomesconnected, and writes its enabled binding.
An install awaiting an organisation admin returns setup_action=request. The box then shows Approval Required, deletes a new pending login, and asks to connect again once the admin approves.
Installation picker
When the user reaches several installations, the callback redirects to the picker page. Its state is the pending login's.
| Route | Does |
|---|---|
GET /api/mcp/oauth/github-installation?state= | The picker page |
GET /api/mcp/oauth/github-installations?state= | { installations: [{ id, account }], install_url }: the App's installations the user reaches, and the install page to add another |
POST /api/mcp/oauth/github-installation | { state, installation_id }: connects the login to that installation. Answers { ok, account } |
An expired or finished sign-in is a 404. An installation the user does not reach is a 400.
The connection lists no model tools: agents use git and gh in the sandbox. Tools shows it without an On/Off switch.
Repositories
GET /api/mcp/connections/:id/github/repositories lists what the installation reaches: name, full_name, owner, private, default_branch, url.
PUT /api/mcp/connections/:id/github/selected stores { repositories: [{ name, url, default_branch }] } in the login's settings as repositories. Each must be one the installation reaches, and is stored with GitHub's id for it (github_id), which a rename of the repository or its owner keeps. A guild-owned login mirrors its selection into guild_repositories.
Tokens
The App's private key and the installation id never leave the orchestrator. At each generation start of a coding agent, it signs an App token (RS256, 9 minutes) and mints, per connection, an installation token limited to the guild's repositories, valid one hour (Repositories).
guilds-git-credential is the credential helper for https://github.com with useHttpPath. It reads /run/guilds/<agent id>/git-tokens.json for the agent GUILDS_AGENT_HOME names. It is set in the image's system git configuration and again through GIT_CONFIG_COUNT, which git reads after every configuration file, so a helper the agent configures, such as the one gh auth setup-git writes, cannot replace it.
When GitHub refuses a token because the installation was removed or suspended (404 or 403), the connection becomes disconnected and the coding generation that needed it fails at start. Deleting the connection deletes the guild repositories added through it.