Goal
Scope a fine-grained GitHub PAT ("Token B") for the gh CLI that can do everything the GitHub MCP server's token
(Token A, ppat/homelab-ops-kubernetes-apps#3423) can read/write, plus: push to non-default branches including
force-push, create remote branches, create PRs (but never land them), trigger workflow_dispatch, and watch/fetch
run status, logs, and artifacts. This issue exists to preserve that research so it's never redone — it does not
itself change any token; the gh CLI keeps using its current classic PAT until this is acted on.
Proposed permissions
| Permission |
Level |
Why |
| Contents |
Read and write |
Push commits, create/force-push branches |
| Pull requests |
Read and write |
gh pr create |
| Actions |
Read and write |
Write needed for workflow_dispatch; Read alone would suffice for run status/logs/artifacts |
| Workflows |
Read and write |
Required to push commits that touch .github/workflows/** — something we do routinely |
| Metadata, Contents (read), Issues, Commit statuses, code/Dependabot/secret-scanning/security-advisory reads |
Read |
Same read set as Token A |
Hard limits of the fine-grained PAT model
These aren't implementation details to work around later — they're the reason this token needs a server-side
backstop before it's safe to create:
- "Push to branches but never
main" is not expressible in any PAT. Contents: write is all-or-nothing across
every branch in the repo. The only way to stop a push to main is branch protection / a repository ruleset on
the GitHub side.
- "Create a PR but never merge it" is not expressible either — and the reason is worse than it first looks.
PR merge is gated by Contents: write (not Pull requests), and this token needs Contents: write anyway
for pushing. So the merge button is already unlocked by a permission this token can't do without. Only a
server-side rule can stop merges — and since the token authenticates as the same identity as the human account,
there's no actor-level distinction GitHub can make between "the human clicked merge" and "a script using this
token clicked merge."
- User-owned Projects v2 and the notifications API are classic-PAT-only (same finding as Token A) — anything
depending on them cannot move to a fine-grained token.
- Use the
GH_TOKEN env var, not gh auth login --with-token, per GitHub's own guidance for fine-grained
tokens.
The Checks API limit is narrower than GitHub documents
Corrected 2026-07-28 by testing Token A directly, and it changes the migration cost:
gh pr checks (and any Checks API call) works on public repos — the baseline read-only public access every
fine-grained PAT carries is sufficient. It 403s only on private repos (Resource not accessible by personal access token, confirmed against homelab-ops-kubernetes-experiments). GitHub's GA "known gaps" list and its
support replies state the limitation unconditionally; in practice it is private-repo-only.
So for the public repos where most work happens, gh pr checks keeps working under Token B. On private repos,
fall back to gh run list --commit <sha> and gh run view --log, which use the Actions API and are unaffected.
What it replaces
The current classic PAT holds repo, workflow, write:packages, admin:public_key, gist, project, user, notifications, read:org, write:discussion — full read/write to every repo the account can see. Migrating to
Token B means losing Projects v2 and notifications access via gh (both classic-PAT-only, per above), and
Checks API access on private repos only — the main costs to weigh against the migration's security benefit.
Prerequisite
None of this is safe to create until direct pushes to main are blocked with "Require a pull request before merging" along with "Restrict who can push to matching branches". Additionally, within branch protection settings for main, configure merge restrictions so that the specific user or token role cannot merge. Ensure you do not add them to the "Bypass list" or "Allow specified actors to bypass" merge requirements, and restrict merge rights strictly to repository admins.
Not yet applied. As of 2026-07-28 both homelab-ops-kubernetes-apps and homelab-ops-kubernetes-clusters
still allow direct pushes to main (branch protection exists but sets no required_pull_request_reviews and no
push restrictions).
Cross-reference
Originates from, and shares its read-permission set and Projects/notifications findings with,
ppat/homelab-ops-kubernetes-apps#3423 (the GitHub MCP server's Token A).
Goal
Scope a fine-grained GitHub PAT ("Token B") for the
ghCLI that can do everything the GitHub MCP server's token(Token A, ppat/homelab-ops-kubernetes-apps#3423) can read/write, plus: push to non-default branches including
force-push, create remote branches, create PRs (but never land them), trigger
workflow_dispatch, and watch/fetchrun status, logs, and artifacts. This issue exists to preserve that research so it's never redone — it does not
itself change any token; the
ghCLI keeps using its current classic PAT until this is acted on.Proposed permissions
gh pr createworkflow_dispatch; Read alone would suffice for run status/logs/artifacts.github/workflows/**— something we do routinelyHard limits of the fine-grained PAT model
These aren't implementation details to work around later — they're the reason this token needs a server-side
backstop before it's safe to create:
main" is not expressible in any PAT.Contents: writeis all-or-nothing acrossevery branch in the repo. The only way to stop a push to
mainis branch protection / a repository ruleset onthe GitHub side.
PR merge is gated by
Contents: write(notPull requests), and this token needsContents: writeanywayfor pushing. So the merge button is already unlocked by a permission this token can't do without. Only a
server-side rule can stop merges — and since the token authenticates as the same identity as the human account,
there's no actor-level distinction GitHub can make between "the human clicked merge" and "a script using this
token clicked merge."
depending on them cannot move to a fine-grained token.
GH_TOKENenv var, notgh auth login --with-token, per GitHub's own guidance for fine-grainedtokens.
The Checks API limit is narrower than GitHub documents
Corrected 2026-07-28 by testing Token A directly, and it changes the migration cost:
gh pr checks(and any Checks API call) works on public repos — the baseline read-only public access everyfine-grained PAT carries is sufficient. It 403s only on private repos (
Resource not accessible by personal access token, confirmed againsthomelab-ops-kubernetes-experiments). GitHub's GA "known gaps" list and itssupport replies state the limitation unconditionally; in practice it is private-repo-only.
So for the public repos where most work happens,
gh pr checkskeeps working under Token B. On private repos,fall back to
gh run list --commit <sha>andgh run view --log, which use the Actions API and are unaffected.What it replaces
The current classic PAT holds
repo, workflow, write:packages, admin:public_key, gist, project, user, notifications, read:org, write:discussion— full read/write to every repo the account can see. Migrating toToken B means losing Projects v2 and notifications access via
gh(both classic-PAT-only, per above), andChecks API access on private repos only — the main costs to weigh against the migration's security benefit.
Prerequisite
None of this is safe to create until direct pushes to
mainare blocked with "Require a pull request before merging" along with "Restrict who can push to matching branches". Additionally, within branch protection settings for main, configure merge restrictions so that the specific user or token role cannot merge. Ensure you do not add them to the "Bypass list" or "Allow specified actors to bypass" merge requirements, and restrict merge rights strictly to repository admins.Not yet applied. As of 2026-07-28 both
homelab-ops-kubernetes-appsandhomelab-ops-kubernetes-clustersstill allow direct pushes to
main(branch protection exists but sets norequired_pull_request_reviewsand nopush restrictions).
Cross-reference
Originates from, and shares its read-permission set and Projects/notifications findings with,
ppat/homelab-ops-kubernetes-apps#3423 (the GitHub MCP server's Token A).