A GitHub App is the GitHub identity of muster's admins when one is
configured, and muster's only GitHub identity. The per-user OAuth connection it
replaced (GITHUB_CLIENT_ID, MUSTER_GITHUB_TOKEN, the Repos tab) has been
removed; see Removing the OAuth connection.
With the App configured:
- Agents get one-repository tokens. The
githubconnector is backed by the App. Each request an approved agent makes through muster's proxy (/connect/github/…, which is howccdclones, fetches and pushes) carries an installation token that GitHub issues for that agent's one repository. In the default write mode the token has exactlycontents: write,pull_requests: write,checks: readandstatuses: read; in read-only mode (MUSTER_GITHUB_APP_ACCESS=read) all four areread. Both add themetadata: readthat GitHub always adds. muster caches each token in memory until five minutes before it expires (GitHub issues them for one hour), and drops it early if GitHub answers 401. It is never stored, logged or given to the agent. - Other repositories are reached anonymously. With an approved grant, all
of the agent's git traffic to github.com goes through the proxy. A request
for any repository other than the agent's own carries no credential at
all, so a public dependency or submodule clones as it would for anyone and
a private one fails as it would for anyone. The App token is never attached
to it. muster compares the path's repository without case, after fully
decoding it. It refuses, before minting anything, any path whose repository
it cannot decide: a
.or..segment, a doubly encoded or backslashed path, a leading empty segment, or/repositories/<id>/…(use/repos/<owner>/<name>/…). API paths that name no repository (/graphql, for example) carry the token, which can reach only the agent's repository. - An agent with no repository gets no github access while the App is configured. A chief, for example, has no repository to scope a token to.
- You approve each repository once. The first agent's
githubrequest for a repository waits for you under Connectors; approving it is shared with every later agent of yours dispatched on that same repository (connectors.md), and never with an agent on another repository. The App changes where the credential comes from, not who decides. A request filed before the App was configured (when nogithubconnector existed) shows Approve once the App is configured. Approve it and the agent's next clone retry (every 30 s) succeeds, with no new dispatch. - A machine-job pool worker is never approved. Approving a
githubgrant (or one on any connector reaching github.com) for an agent in a worker pool is refused, and a worker holding one is skipped for jobs: a pool worker holds no GitHub credential (jobs.md). - The installation's repositories are the connected ones (for an admin):
the dispatch dialog's repository picker lists them, and they are what the
"connected repository" check that every claude-code dispatch and
task_dispatchapplies accepts. - Only installations on
MUSTER_GITHUB_APP_ACCOUNTScount, matched by the account's numeric id. The App has to be installable on "Any account", so anyone can install it on their own repositories. An installation on an account that is not on the list adds no repository, gets no token and passes no check. muster logs it once, by login. The match is on the id, never the login. A login can be renamed, and the freed name can then be registered by someone else, who could install the App under it. So an installation whose login matches but whose id does not is ignored. An installation whose id matches under a new login (your own rename) is still used, and muster logs the rename so you can update the entry. - The
githubconnector card under Connectors says it is provided by the App. There is no Rotate or Delete. An admin's storedgithubconnector (a personal access token) is hidden while the App is configured, and comes back unchanged if you remove the App. - Changing the source re-opens approvals. Each approval records whether it
was for the App or for a stored token. If you configure the App over a stored
githubtoken, or remove it from under approvals made for it, the affected agents'githubgrants go back to pending, and you approve them again. An approval never silently changes what it grants.
The App is the admins', not every user's. It belongs to the deployment.
On a multi-user muster, only admin-role users see it: their agents get the
App-backed github connector, and their dispatch checks and repository picker
list its repositories. For a member, muster behaves as if no App were
configured. Their agents do not see the App connector, their github requests
cannot be approved onto it (whoever clicks Approve), and they have no connected
repositories: a member cannot dispatch a claude-code agent into a repository.
Sign in to GitHub as the account that will own the App (a personal
account is fine). Go to Settings → Developer settings → GitHub Apps → New
GitHub App (https://github.com/settings/apps/new) and fill in:
| field | value |
|---|---|
| GitHub App name | Any unique name, e.g. muster-<yourname> |
| Description | Optional |
| Homepage URL | Required by the form. Your muster's URL or the repository URL. |
| Callback URL | Leave empty. muster does not sign users in with the App. |
| Expire user authorization tokens | Leave as is (it does not apply). |
| Request user authorization (OAuth) during installation | Unchecked |
| Enable Device Flow | Unchecked |
| Setup URL | Leave empty |
| Webhook → Active | Unchecked. muster uses no webhooks; the Webhook URL is then not required. |
| Repository permissions → Contents | Read and write |
| Repository permissions → Pull requests | Read and write |
| Repository permissions → Checks | Read-only |
| Repository permissions → Commit statuses | Read-only |
| Repository permissions → Metadata | Read-only (GitHub selects it, and it cannot be removed) |
| Every other repository permission | No access |
| Organization permissions | All: No access |
| Account permissions | All: No access |
| Subscribe to events | None |
| Where can this GitHub App be installed? | Any account. "Only on this account" cannot be installed on an organization that you, the owner, do not own. |
Click Create GitHub App. On the App's page:
- Note the App ID (under "About"). It is
MUSTER_GITHUB_APP_ID. - Under Private keys, click Generate a private key. GitHub downloads a
.pemfile. That file isMUSTER_GITHUB_APP_PRIVATE_KEY. Keep it only in the Secret below. If it leaks, generate a new one and delete the old one on this page.
muster only ever asks for exactly those four repository permissions (Contents and Pull requests read and write, Checks and Commit statuses read — or all four read in read-only mode), whatever the App was granted. If the App lacks one of them, GitHub refuses the token (a 422 on the agent's card). But give the App no more than this table, so that a leaked key cannot do more.
On the App's page, click Install App, then Install next to the organization (for example, the one whose repositories your agents work in). Choose Only select repositories and pick the repositories agents may work in. Click Install.
If you are not an owner of that organization, GitHub sends an installation request instead. GitHub notifies the organization's owners. One of them must approve it (the organization's settings, under Third-party Access → GitHub Apps) before muster can mint any token.
To add or remove repositories later, go to the organization's Settings → Third-party Access → GitHub Apps → → Configure → Repository access. muster's repository list is cached for a minute.
muster picks the installation whose account matches the repository's owner.
That account must be on MUSTER_GITHUB_APP_ACCOUNTS.
| variable | value |
|---|---|
MUSTER_GITHUB_APP_ID |
The App ID |
MUSTER_GITHUB_APP_PRIVATE_KEY |
The whole .pem file's contents. A value with its line breaks written as \n is accepted too. |
MUSTER_GITHUB_APP_ACCOUNTS |
Required. Comma-separated <login>:<id> entries for the accounts whose installations muster may use, e.g. example-org:12345,your-user:67890. Get each id with gh api users/<login> -q .id (it works for organizations too). An entry without an id refuses to boot. |
MUSTER_GITHUB_APP_ACCESS |
Optional. write (the default when unset) or read: what every agent token is minted with. See Read-only mode. Any other value refuses to boot, as does setting it without the App. |
MUSTER_GITHUB_APP_ACCESS=read makes every agent token muster mints ask for
contents: read, pull_requests: read, checks: read and statuses: read
(plus GitHub's implicit metadata: read). The repository-listing token is
metadata: read in both modes. With a read token, a git push or a
pull-request write through the proxy is refused by GitHub (403); muster
never mints a wider token in its place. Use it for a deployment whose agents
must only read GitHub, even when it shares an App with a write-mode deployment:
the mode is the deployment's, the App's own permissions stay as in the table
above. An officer instance's token is
read-only in both modes (connectors.Caller.ReadOnly): a write-mode
deployment mints write for its task agents and read for its officers.
In read mode, every place muster describes the connector says so: the
Connectors tab, the approve card and connector_requests_list
(approving_grants), the boot banner, the GitHub preset's help (which also
says that the setting does not limit a stored token), and a task agent's
close-out instructions, which tell it to report its branch instead of pushing.
The App also needs the connector store (MUSTER_CONNECTOR_ENCRYPTION_KEY,
connectors.md), because agents reach GitHub only through the
proxy, and their grants live in that store. muster refuses to boot if the App ID
is set without the key (or the other way round), if the key does not parse, if
the account allowlist is empty, or if there is no connector store.
On Kubernetes, keep all three in one Secret and load it with envFrom:
kubectl -n <muster-namespace> create secret generic muster-github-app \
--from-literal=MUSTER_GITHUB_APP_ID=<app id> \
--from-literal=MUSTER_GITHUB_APP_ACCOUNTS=<org>:<org id>,<your-user>:<your id> \
--from-file=MUSTER_GITHUB_APP_PRIVATE_KEY=./<app-name>.<date>.private-key.pemenvFrom:
- secretRef:
name: muster-github-appThe Secret's keys are MUSTER_GITHUB_APP_ID, MUSTER_GITHUB_APP_PRIVATE_KEY
and MUSTER_GITHUB_APP_ACCOUNTS.
At boot muster prints a github app: ON — … line, then lists the App's
installations in the background:
github app: App <id> authenticates and is installed on <accounts>: it works.github app: the boot check FAILED: GitHub refused the GitHub App's credentials (401) …: the ID and key do not belong to the same App, the key was deleted, or the server's clock is more than a minute off. The App's JWT is valid from 60 s before to 9 minutes after muster signs it.… is installed on NO account: install it (above) on an account inMUSTER_GITHUB_APP_ACCOUNTS.github app: RENAMED allowlisted account(s), still used by id: <listed>→<current> (<id>): an account you listed was renamed on GitHub. It still works. Update its entry to the new login.github app: ignoring the installation on "<login>": not on MUSTER_GITHUB_APP_ACCOUNTS: someone installed the App on an account you did not list. muster uses nothing from it.
A failure that blocks agents is also shown on the connector request card and on
each affected agent's card (GitHub: …). Examples are the credentials refused,
a rate limit, or a repository that is not in the installation (GitHub's 422).
A claude-code agent whose repository cannot be cloned yet holds its first turn.
It waits up to MUSTER_CHECKOUT_WAIT_TIMEOUT (default 24h) for your approval,
then goes to error. The wait is measured from the agent's last status change
(its updated_at). Recording the wait does not move that time. So raising the
variable and restarting muster also extends an agent that is already waiting:
its deadline becomes updated_at plus the new value.
The OAuth connection has been removed: the Repos tab, /github/connect,
/github/callback, /github/disconnect, the per-user token store and the
static-token connect at boot. Without an App, muster has no GitHub identity and
no repository counts as connected.
Nothing reads GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET,
MUSTER_GITHUB_ENCRYPTION_KEY or MUSTER_GITHUB_TOKEN any more. muster does
not refuse unknown variables, so a deployment that still sets them boots; drop
them from the deployment. The github_connection table was left in the schema, unread and
unwritten, so that change could be rolled back; migration 0059 drops it, and with it every
stored OAuth token (schema.md).