GitHub Fine-Grained Tokens: A Practical Permission Guide
If you've used GitHub's classic personal access tokens, you know the drill: you pick a broad scope like repo, and suddenly that token has read/write access to every repository you own. It works, but it's not great from a security standpoint. A leaked token has a large blast radius.
Fine-grained tokens fix this. You pick specific repositories, then grant granular permissions per resource type. Here's a practical breakdown of how to configure them for real workflows.
Classic vs. Fine-Grained: What's the Difference?
| Feature | Classic | Fine-Grained |
|---|---|---|
| Repo access | All repos | Specific repos only |
| Permission granularity | Broad scopes | Per-resource control |
| Expiration | Optional | Required (max 1 year) |
| Org admin approval | Not supported | Supported |
| Security posture | Lower | Higher |
Classic tokens are still needed for some older integrations, but for anything new, fine-grained is the better choice.
Workflow: Clone a Repo, Push Changes, Open a PR
This is the most common developer workflow. Here's the minimal permission set you need:
| Permission | Level | Why |
|---|---|---|
| Metadata | Read-only | Always required — can't be removed |
| Contents | Read and write | Clone the repo + push to branches |
| Pull requests | Read and write | Create PRs via API or UI |
That's it. Everything else can stay at No access.
Cloning with a Fine-Grained Token
Cloning is a git operation, not a REST API call, but it still authenticates through the token. With Contents: Read in place, you can clone over HTTPS:
git clone https://<your-token>@github.com/owner/repo.git
Restricting the Token to New Branches Only
You might want a token that can push branches and open PRs, but can't merge into main. This is where a lot of people get tripped up: token permissions alone can't enforce this.
The distinction matters:
- Token permissions — what the token is capable of doing in general
- Branch protection rules — what GitHub will allow on specific branches, regardless of token capability
To properly enforce this, keep the token permissions as-is and add branch protection rules on main under Settings → Branches:
- ✅ Require a pull request before merging
- ✅ Require approvals
- ✅ Restrict who can push to matching branches
- ✅ Do not allow bypassing the above settings
With these in place, the token can push to any new branch and open PRs — but GitHub will reject any direct push to main at the server level.
| Action | Allowed? |
|---|---|
| Clone the repo | ✅ |
| Create a new branch | ✅ |
| Push commits to that branch | ✅ |
| Open a PR from that branch | ✅ |
Push directly to main | ❌ |
Merge a PR into main | ❌ |
Adding Issues Access
A quick note on how GitHub Issues works: issues are per repository. Each repo has its own independent tracker, and issues share a number sequence with pull requests — so if issue #5 exists, the next PR opened will be #6.
To let a token read issues, add:
| Permission | Level |
|---|---|
| Issues | Read-only |
For creating issues too, bump it to Read and write.
The Issue Approval Problem
One thing GitHub doesn't support natively: approving issues before they go live. When a token holder creates an issue, it's immediately visible. There's no review queue.
The closest workaround is a GitHub Action that automatically closes and labels new issues as pending-review the moment they're created:
# .github/workflows/issue-moderation.yml
on:
issues:
types: [opened]
jobs:
hold-issue:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['pending-review']
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
});
The workflow becomes: token holder creates issue → Action closes it instantly with a pending-review label → you review and reopen it manually. The issue is still visible in the repo (just closed), so this isn't airtight — but it gives you a clear review gate without leaving GitHub.
If you need proper issue moderation, tools like Linear or Jira have this built in natively.
Full Permission Reference for This Workflow
Here's the complete token setup for clone + push + PR + issue review/creation:
| Permission | Level |
|---|---|
| Metadata | Read-only (auto-selected) |
| Contents | Read and write |
| Pull requests | Read and write |
| Issues | Read and write |
Pair this with branch protection rules on main and you have a solid, least-privilege setup for a contributor token.