Your script calls the GitHub API with a fine-grained personal access token and gets 404 Not Found on a repository you can open in the browser. The URL is right. The repository exists. You will spend the next twenty minutes checking the URL anyway. Stop. With fine-grained tokens, 404 usually means not permitted, not not there. Why 404 and not 403 GitHub does not disclose the existence of resources you are not authorised to see. A private repository your token cannot reach returns the same 404 as a repository that does not exist. That is deliberate — it stops a token from being used to enumerate private repositories — and it means the debugging order most people use is backwards. When a fine-grained token gets 404, check these before the URL: Is the repository in the token's access list? Fine-grained tokens name repositories individually (or "all"). A repository created after the token was, is not in a "selected repositories" list. Is the required permission set at all? Permissions are per resource and default to unset. A token with Issues: read and nothing else gets 404 from /repos/OWNER/REPO/pulls. Is the resource owner right? A token created under your personal account cannot see an organisation's private repositories, and the owner cannot be changed after creation. Create a new token. Has the organisation approved it? Organisations can require approval for fine-grained tokens. The token exists and does nothing until approved. Only then look at the URL. And 403 is often not permissions either The mirror image: a 403 from an endpoint that worked a minute ago is usually the rate limit, not a permission change. Check the response headers before you touch the token: gh api rate_limit --jq '.resources.core | "\(.remaining)/\(.limit) remaining, resets \(.reset | todate)"' Authenticated requests get 5,000 per hour. A --paginate run over a large org can eat a meaningful slice of that. There is no scopes header Classic tokens returned an x-oauth-scopes header you could inspect. Fine-grained tokens have no scopes and return no such header. The only ways to know what one can do are to read its configuration in account settings, or to test an endpoint. The fastest liveness check: gh api user --jq '.login' A 401 there means invalid, revoked, or expired — the third being the one that bites on a Monday, because tokens created in the same setup session all expire on the same day. Working out the permissions you actually need Do not guess. Start from the endpoints: List every endpoint the script calls. Look up each one's documented fine-grained permission. Take the union, at the weakest level each needs — read where read suffices. Create the token with exactly that set. Run it; fix any 403/404 by adding the specific permission the failing endpoint documents. The mappings that come up constantly: Task Permission Read repository metadata Metadata: read (added automatically) Read or write files, clone, push Contents: read / write Push commits, create tags and releases Contents: write — there is no separate releases permission Change a file under .github/workflows/ Contents: write and Workflows: write List, label, comment on issues Issues: write Request reviewers, merge pull requests Pull requests: write Read workflow runs, download logs and artifacts Actions: read Trigger, cancel or re-run workflows Actions: write Post a check run Checks: write Manage repository secrets Secrets: write Rulesets, branch protection, settings Administration: write Two of these are where over-provisioning happens. Workflows: write is only needed to change workflow files — people add it "to be safe" and it should not be there. Administration: write covers repository settings, and on a personal-account token that includes deletion; a script that only needs to update a description does not need it. If it runs unattended, it should not be a PAT at all A personal access token represents a person. When they leave, the automation stops; until then, every action is attributed to them. For anything on a schedule, in CI, or shared by a team, use a GitHub App: installation-scoped, hourly tokens minted from a private key, its own rate limit, and its own identity in the audit log. Inside GitHub Actions on the workflow's own repository, use neither — the job's GITHUB_TOKEN with least-privilege permissions: is already there. Rotation, when you do keep a PAT: create the replacement, update the secret store, verify, then revoke the old one. Reverse that order only for an exposure. The full lesson — creating the token, organisation approval, reading a token's own configuration, expiry failure modes, fine-grained versus classic by scenario — is here: Fine-grained personal access tokens. The security angle, including what a "narrow, dated capability" does and does not protect you from: Fine-grained tokens.