Contents

GitHub App Permissions — What Yoke Asks For, and Why

This is the user-facing rationale for the GitHub access Yoke requests: why the connection exists, the exact repository permissions the Yoke GitHub App declares, and what Yoke actually does with each one.

For the operator runbook (registration, private-key custody, rotation) see server GitHub App setup. For the connection model see GitHub connection layers. For the backlog↔issue mirror see GitHub Sync. The machine-checked source of truth for the permission set is yoke_contracts.github_app_installation_permissions.

Permission boundary

Yoke mirrors enabled backlog issues, manages pull requests/git delivery and starts CI/deploy workflows. The Product App declares nine baseline repository permissions. Administration and repository Webhooks belong to separately selected privileged authority; opting into a feature does not add an optional per-repository grant to the Product App. App permissions are registration-wide and presented to every installation; use a dedicated privileged binding and selected repositories for runner-fleet authority.

Authorize through GitHub's device/install ceremonies. Installation and project bindings store verified non-secret metadata; machine authorization retains an owner-only rotating refresh credential. App keys stay in control-plane secret custody, and operations use short-lived downscoped tokens.

The connection is three separate layers

They are independent — removing one does not remove the others. This is the model behind the "Connect a GitHub repository" screen.

Layer Screen section Answers Mechanism
1. Machine authorization GitHub identity Which GitHub user may this copy of Yoke act as? GitHub device flow (yoke github connect). A rotating refresh credential is stored owner-only under ~/.yoke/secrets/; short-lived access tokens stay in memory.
2. App installation App installations Which account/org installed the App, and which repos did GitHub grant it? Managed entirely by GitHub. Yoke stores only the verified installation id, permissions, and status.
3. Project ↔ repo binding Project repository bindings Which one of the App's repositories belongs to this Yoke project? One verified row (project_github_repo_bindings) — the sole authority for issue sync, PRs, Actions, and delivery.

"The repository is verified against your GitHub authorization" means: before storing a binding, Yoke uses your own user token to confirm GET /user → your installations → the exact repository is actually available to that installation, and that the repository owner matches the installation account. It never trusts caller-supplied metadata.

Two kinds of token

Every GitHub action runs under one of two short-lived, App-scoped tokens — never a long-lived personal token:

  • A user-to-server token from the device flow. This authorizes local work on the machine you authorized: git pushes over HTTPS, listing your repos/orgs during onboarding, and the optional repo-create step.
  • An installation token minted server-side from the App's private key (POST /app/installations/{id}/access_tokens). This authorizes automation REST calls (issues, PRs, Actions, secrets, variables). It is minted per operation and scoped down to just the permissions that operation needs.

Both are bounded by the App's declared permissions below.

Which one an operation runs under

An operation names the weakest authority that can perform it, and resolution honors that name. Only work attributed to a person genuinely needs the user-to-server token; work the installation is itself authorized to do falls through to an installation token when the machine authorization is unavailable, instead of refusing something the binding already covers. When neither path produces a token, the user-authorization failure is the one reported, because reconnecting is the repair on a machine bound to user authority.

Merging is where the distinction is load-bearing, so the merge engine settles it before any merge work rather than discovering it afterwards:

Merge route Authority Why
Direct merge — land in the checkout, publish the base branch, prove the pushed commit's checks Installation Nothing is attributed to a person; the machine's stored GitHub credential carries the push and Checks: read carries the proof
Pull-request merge — open a PR, wait on its checks, merge it User The pull request is opened as you

A merge whose checkout has no remote never reaches GitHub and needs no authority at all. Classifying up front is what keeps an unavailable authorization from surfacing only after the branch has already landed — a merge that succeeded while its verification reported failure.

Permissions at a glance

Authoritative source: yoke_contracts.github_app_installation_permissions.

Baseline — requested of every connection

Permission Access Purpose
Metadata read GitHub-mandated floor; repo/commit lookups + issue search
Issues write Mirror your backlog to GitHub Issues (issues, comments, labels, sub-issues)
Pull requests write Open a PR from the worktree branch into your default branch
Contents write Deliver code over git — push branches, merge the PR, commit, clean up
Workflows write Manage the CI/CD workflow files Yoke ships into .github/workflows/
Actions write Kick off (dispatch) the deploy/CI workflows Yoke orchestrates
Checks read Read a PR's CI results and merge only when green
Commit statuses read Read required legacy status contexts in the PR's check rollup
Secrets write Seed the encrypted deploy credentials your generated pipelines need
Variables write Set non-secret Actions config (CI-enable flags, runner routing)

Privileged — separate authority, outside the Product baseline

Permission Access Only if you…
Administration write …let Yoke create a repo for you, create a deploy environment, apply merge-queue declarations, or run a runner fleet
Webhooks (repository_hooks) write …run your own self-hosted GitHub Actions runner fleet

Merge-queue apply also requires Contents: read to fetch the gate branch's landed declaration before changing any rule. Preview may use lane content; real apply refuses content that has not landed.

A contract test actively enforces that Administration and Webhooks stay out of the baseline set.

What each permission does

Baseline

Metadata: read — The floor GitHub requires of every App; it cannot be dropped and it is the guaranteed minimum stamped on every token Yoke mints. Explicitly used for: issue lookup via GET /search/issues (backlog↔Issues sync, and the health check that hunts for stray Yoke-tagged issues — GitHub gates issue search under Metadata, not Issues); onboarding probes that read your repository and its latest commit to tell whether it exists / is empty / is populated before writing anything; and a cheap pre-merge auth check.

Issues: write — The backlog↔Issues mirror, Yoke's core value. Creating an issue per backlog item and epic-task, updating title/body, opening and closing on status changes, posting status-change comments, creating and applying the status:* / worktree:* labels Yoke tracks work with, linking epic-task issues as sub-issues under their parent, and deleting a mirrored issue (via GraphQL, since REST has no issue-delete). GitHub bundles comments and labels under the Issues permission, so this one grant covers the whole mirror — no extra permission needed. Only fires when a project has sync enabled; new projects default to disabled, which does zero issue writes.

Pull requests: write — Opening a pull request from the worktree branch into your default branch when you ship a work item (/yoke usher), plus the agent-driven "open a PR" command. This permission is specifically for creating PRs; merging the PR is authorized by Contents: write, and the find-PR and mergeability reads run at Pull requests: read. Issue searches request Issues: read, PR searches request Pull requests: read, and searches that include both types request both. Hidden or incomplete search results are unreadable, rather than proof that no matching work exists.

Contents: write — Delivering your code over git. Pushing the worktree branch, pushing the base branch when it is ahead, merging the PR (PUT …/pulls/{n}/merge), committing the done-state, deleting the now-merged branch, and pushing preview branches for push-triggered environments. Arming and disarming merge-when-ready, and dequeuing a pull request, request Contents: write together with Pull requests: write. Reading repository files (e.g. checking a deploy workflow file during setup) rides the same grant. Git talks to GitHub over HTTPS using a short-lived token (x-access-token), and the token is never written to your .git/config or the stored remote URL. Yoke's own remote operations carry it as a URL-scoped http.extraheader supplied per command; a URL-scoped credential helper installed into onboarded checkouts serves the git commands people run in their own shells. See GitHub connection setup.

Workflows: write — So the App may manage the GitHub Actions workflow files Yoke ships into .github/workflows/ (a CI workflow plus deploy/hotfix/smoke pipelines). GitHub specially protects workflow files: an app needs this dedicated permission, on top of Contents, before it may create or change anything under .github/workflows/. See the hygiene note below on the current exercised level of this grant.

Actions: write — Starting the workflows Yoke orchestrates for deploy/CI (POST …/actions/workflows/{workflow}/dispatches). GitHub classifies triggering a run as a write. Merely watching a run — its status, jobs, and logs — needs only read, and Yoke uses read-scoped tokens for exactly that. The write level is spent only at the moment a run is started. Only fires for projects with a GitHub-Actions deploy stage. Listing deployment environments also uses Actions: read; an unreadable listing is distinct from an empty one.

Checks: read — Before merging a PR, reading its CI check-runs on the head commit (GET …/commits/{sha}/check-runs) and merging only if they pass. It also reads GET …/check-runs/{id}/annotations to explain jobs that never started in failed-log, watcher output and CI QA evidence. Those reads request Actions and Checks at read level together. Strictly read-only — Yoke never creates or re-runs a check. Its consumers are the PR-merge gate and the post-push proof a direct standalone landing runs against the commit it just published; the second reads under installation authority, because a direct merge is authorized that way. (The deploy-branch CI advisory reads workflow-run status under Actions: read, not this permission.)

Commit statuses: read — Read legacy status contexts alongside check runs in the pull request's GraphQL check rollup. That combined query requests Pull requests, Contents, Checks and Commit statuses at read level. Missing access or a hidden response field is unreadable, never proof of passing checks. Grant the named permission on the GitHub App, approve the installation's updated permissions, and retry the landing read. Yoke never writes statuses.

Secrets: write — Seeding the SSH deploy credentials (key, host, user) that Yoke's generated deploy workflows need to reach your server. Each value is sealed with libsodium before upload; GitHub only ever accepts already-encrypted values and never lets anything read a secret back. Only fires when you set up the webapp/VPS deploy pipeline — a backlog + PR + CI user never writes a secret. Onboarding treats a missing Secrets grant as non-fatal: the binding still records; only the deploy step is gated.

Variables: write — Setting non-secret GitHub Actions variables (CI-enable flags, the runner-fleet routing variable). This write is only reached through the operator CI/runner-fleet arming command; a normal connect-my-repo user never triggers it. The read counterpart (runner status) needs only Variables: read.

Privileged (opt-in)

Administration: write — Only when you opt into heavier setup, on four write paths: creating a brand-new repository for you during onboarding (off by default, gated behind an explicit allow flag; otherwise Yoke blocks with manual instructions), creating the deployment environment on your repository for automated deploys (silently skipped if not granted), standing up a self-hosted runner fleet, and applying the declared merge-queue ruleset plus allow_auto_merge from .yoke/merge-queue.json via yoke github merge-queue apply (fails closed when Administration is absent). Doctor also needs Administration: write to read the full declared queue settings: GitHub hides bypass actors without this authority even on GET requests. Doctor remains read-only; hidden auto-merge or bypass fields fail as unreadable with an access recovery, rather than as drift or a pass. Without this grant Doctor still checks the readable enforcement and workflow with a metadata/contents token. Two other Administration touches — reading branch protection, reading runner status — are Administration: read, and both degrade to a warning when Administration is absent.

Webhooks (repository_hooks): write — Only if you run your own GitHub Actions runner fleet. Your infrastructure code (Pulumi) creates a repository webhook on the workflow_job event, secured with an HMAC signing secret, so your runners know when a job is queued. This is a repository webhook, distinct from the App's own global webhook. Never touched by issue sync, PRs, or deploys.

What Yoke deliberately never requests

Each of these is absent from the code, not merely unlisted:

  • No Releases API — Yoke never creates GitHub releases.
  • No Milestones — the Issues grant is never used for milestones.
  • No Deployments API — Yoke tracks delivery gates in its own database; it never creates GitHub deployment objects.
  • No Packages / GHCR on your repository — per-project images go to a cloud container registry; Yoke's own server image is separate.
  • No Pages, and no commit-status write — Yoke reads checks instead of writing commit statuses.
  • No organization, members, teams, or collaborator permissions — the entire contract is repository-scoped.
  • No email / user:email scope — the device flow reads only your login and numeric id.
  • Branch protection and queue policy are privileged — ordinary reads warn when required protection is absent; applying a declared merge-queue ruleset requires separately granted Administration authority.
  • No classic OAuth scopes at all — GitHub Apps derive access from installation permissions, not scopes. The device flow sends only a client id.

How your access and credentials are protected

  • Least-privilege token downscoping. The App installation holds the baseline grant, but every individual REST call mints a fresh token scoped to just that operation — an issue read carries issues:read, a workflow dispatch carries actions:write. There is no long-lived project token.
  • The private key stays in the control plane. For hosted deploys, CI relays typed calls through Yoke's control plane rather than ever holding the App key; the key lives only in a secrets manager, never in CI, runner disks, or logs.
  • Sync is off by default. New projects are disabled — the backlog lives only in Yoke's database and every Issues-write surface refuses until you explicitly enable sync.
  • You can unwind any layer independently — disconnect the machine identity, unbind one project, or uninstall the App in GitHub — without cascading.
  • Bindings are verified against your own GitHub authorization before they are stored, and re-verified server-side.
  • Secrets are sealed before upload and are write-only — GitHub never returns a secret value, and Yoke never logs one.

Registration review

Review requested and exercised authority separately. Workflows write permits workflow-file delivery even where a path currently reads through an App token and pushes through machine authorization. Secrets/Variables remain baseline registration grants, while their write consumers are deploy and CI/runner setup. A permission-tier change requires changing the registration contract and affected consumers together; runtime token downscoping does not shrink what a compromised registration key could request. The operator runbook owns registration, selected repository scope and privileged binding review.

Source of truth

  • Permission contract: yoke_contracts.github_app_installation_permissions
  • Operator runbook (registration, key custody, rotation): Server GitHub App setup
  • Connection model (identity / installation / binding): GitHub connection layers
  • Backlog↔issue mirror and the per-project sync switch: GitHub Sync
GitHub App permissions · Yoke