Skip to content

Personal Access Token Hygiene: Expiry, Storage, Rotation and Revocation

Lesson 9 of 9Intermediate13 min readGit Security & DevSecOps · Repository SecurityVerified: GitHub fine-grained personal access token documentation and organisation policies, September 2026

Pillar 3 covers fine-grained tokens as an API credential — creating one, choosing permissions, using it against endpoints.

This page is narrower and blunter. A fine-grained token is a significant improvement over a classic one, and the improvement produces a specific risk:

A fine-grained token is still a secret. Scoping it well changes how bad a leak is. It does not change whether it can leak.

Every fine-grained token should have four properties, and the fourth is the one that gets skipped:

  1. A resource owner — you or one organisation, never more
  2. Selected repositories, never “all repositories” unless that is genuinely the job
  3. The narrowest permissions that work, each at Read unless Write is required
  4. An expiry — GitHub allows 1 to 366 days, or none; take the shortest that fits the purpose

And a fifth, outside the token itself: use a GitHub App instead if the thing consuming it is automation rather than a person.

Classic tokens use coarse scopes. The repo scope grants read and write to every repository the account can reach — personal, organisation, public and private — with no way to narrow it. In an organisation, that is usually hundreds of repositories to accomplish something in one.

Fine-grained tokens replace that with three independent axes:

AxisWhat it decides
Resource ownerWhose resources the token can touch: you, or one organisation
Repository accessAll, selected repositories, or public repositories only
PermissionsPer-permission, per-level: no access, read, or read and write

The permissions themselves fall into three groups: repository permissions such as contents, pull_requests and workflows; organisation permissions such as members and organization_secrets; and account permissions such as emails and gpg_keys.

The practical effect: a token for a deployment script becomes read contents on two repositories in one organisation, expiring in 90 days rather than everything, forever.

Fine-grained tokens do not yet cover everything classic tokens do. At the time of writing they cannot:

  • Contribute to public repositories where the user is not a member
  • Access more than one organisation with a single token
  • Call the Checks API
  • Access Packages, or user-owned Projects

If you find yourself reaching for a classic token, check whether one of those is the actual reason. Where it is, prefer a GitHub App if the operation is automated — Apps do not share these limits. Where you genuinely must use a classic token, give it the narrowest scope set that works and the shortest expiry, and treat it as a credential requiring a rotation date rather than a permanent fixture.

Expiry is the control that works without you

Section titled “Expiry is the control that works without you”

The strongest property of a fine-grained token is not its scoping. It is that it stops working.

An expiry converts an undetected leak from a permanent compromise into a bounded one. A token leaked into a log, a screenshot or a container image, with 90 days remaining, is dangerous for at most 90 days without any action from you. The same token with no expiry is dangerous until somebody notices — which, for an unused credential, may be never.

GitHub permits between 1 and 366 days, or none. Organisation and enterprise owners can enforce a maximum lifetime policy, which is the mechanism that makes this stick across a team: a policy capping token lifetime at 90 days means nobody can create a non-expiring token, regardless of intention or hurry.

Choosing a duration:

PurposeReasonable expiry
A one-off script or migration1 to 7 days
A local development convenience30 to 90 days
Something running on a serverUse a GitHub App instead
“I do not want to think about this again”This is the request the policy exists to refuse

Individual discipline does not scale. Organisation owners have three controls worth using.

Require approval. Organisation owners can require approval before any fine-grained token gains access to the organisation’s resources. Until approved, a token has limited capabilities. This makes token creation visible rather than silent, and it produces a list of approved tokens — which is usually the first accurate inventory an organisation has ever had.

Restrict access entirely. An organisation can block fine-grained tokens from accessing its resources. Blocked organisations do not appear in the resource owner list when a token is created. Combined with an App-based automation strategy, this is a coherent and defensible position.

Enforce a maximum lifetime. The policy that makes the previous section real.

Adopt these in that order. Approval first, because it produces the inventory that tells you what a restriction would break.

Choosing permissions without over-granting

Section titled “Choosing permissions without over-granting”

The permission list is long, and the path of least resistance is to grant whatever makes the error go away. A method that produces narrow tokens without a lot of trial and error:

Start from zero and let it fail. Create the token with no permissions, run the operation, and read the error. GitHub’s API responses name the permission that was missing, so each failure tells you exactly one thing to add. Three iterations usually gets you an exact set — and, importantly, an exact set you can justify later.

Read the verb, not the feature. Wanting to “manage pull requests” sounds like it needs pull_requests: write. Listing pull requests, reading their diffs and checking their status all work with pull_requests: read. Write is for creating, merging and modifying.

Watch for the permissions that imply more than they say:

PermissionAlso enables
contents: writePushing commits, creating and moving tags, creating releases
workflows: writeChanging what runs with the repository’s credentials
administration: writeRepository settings, including protection rules
actions: writeTriggering and cancelling workflow runs, deleting logs
organization_secrets: writeReading nothing, but replacing secrets other workflows consume

contents: write is the one that surprises people. It is the ordinary permission for pushing a commit, and it also lets the holder create a release and move a tag — which is why a token for a “just update the changelog” script is a token that can publish a version.

workflows: write deserves its own consideration entirely. A token holding it can rewrite CI, and CI runs with the repository’s credentials. Grant it only to something whose job is genuinely editing workflow files, and treat it as equivalent to code execution in the repository.

Prefer several narrow tokens to one broad one. Two tokens, each doing one job with two permissions, are easier to reason about, rotate and revoke than one token with six permissions doing two jobs. The cost is managing two tokens; the benefit is that revoking either one has a knowable blast radius.

A scoped token is still a token, and everything from Git Credentials applies.

Do: store it in an OS keychain via a credential helper; in a secret manager for a server; in an Actions secret if it must reach a workflow — with the strong preference for not needing one at all.

Do not: put it in ~/.git-credentials, in a remote URL, in a .env file that might be committed, in a shell alias, in a script, or in a chat message to a colleague who needed access urgently.

The last one deserves attention because it is the most common and the least likely to be recorded anywhere. A token pasted into a chat is a token in a searchable archive, in that colleague’s device backups, and in whatever integrations that chat system has. Treat sending a token as equivalent to publishing it, and rotate afterwards.

  1. Create the replacement before revoking anything, with the same scope and a fresh expiry.

  2. Update the one place it is used. If there is more than one place, you have learned something about how the token was scoped.

  3. Verify the new token works — a read call against the resource is enough.

  4. Revoke the old token from the account’s token list.

  5. Confirm the old one fails. A 401 is the evidence.

For an exposure rather than a routine rotation, invert the order: revoke first, then work out what broke. A working credential in someone else’s hands is worse than an outage you caused deliberately and can fix.

GitHub records when a token was last used, which is the most useful signal available for two questions: whether a token you are about to revoke is still in service, and whether a token you know has leaked has been used since.

Threat. A token is obtained by someone else and used against the GitHub API or a Git remote as its owner.

Attack surface. Wherever the string ended up. In practice: credential helper files, .env files, CI logs where a command echoed it, shell history, chat messages, screenshots, container images built from a directory that contained it, and the clipboard history of whoever created it.

Impact. Exactly the token’s scope, for exactly as long as remains before expiry. This is the whole argument for both fields — they are the only two things that determine the size and duration of the event, and both are set at creation time by someone in a hurry.

Control. Selected repositories rather than all. Read rather than write. The shortest expiry the purpose tolerates. Organisation policy so those are not individual choices. Storage the OS mediates.

Verification. Take a token you currently use and answer three questions without looking anything up: which repositories does it reach, what can it do to them, and when does it stop working? If you cannot answer all three, that is the finding.

The fourth question is worth asking of the whole set rather than one token: how would you know if one of these were being used by somebody else? For most organisations the honest answer is the “last used” timestamp, checked by a person who happened to look — which is why bounding the consequence matters more here than detection does.

The decision that matters most, and the one people default on.

Fine-grained PATGitHub App
IdentityA person’sIts own
LifetimeUp to 366 days, or noneInstallation tokens last about an hour
ScopeOne resource owner, selected repositoriesInstalled per organisation, selected repositories
Survives departureNo — it belongs to the personYes — it belongs to the App
Audit trailAttributed to the personAttributed to the App
Rate limitsThe user’sScale with installation size
Setup costA few minutesRegistration, a private key, a token exchange
RotationManualAutomatic, per use

Read the “Survives departure” and “Lifetime” rows together. A personal access token doing a machine’s job is a piece of production infrastructure attached to one person’s employment, holding a credential that outlives most incidents.

Use a PAT for something a person does occasionally: a local script, a one-off migration, exploring the API.

Use an App for anything that runs on a schedule, runs in CI, or that somebody else would need to fix. In GitHub Actions, actions/create-github-app-token mints an installation token per run, which is the shape you want: no standing credential anywhere.

GitHub gives its credentials distinctive prefixes — github_pat_ for fine-grained personal access tokens, ghp_ for classic ones, ghs_ for App installation tokens, gho_ for OAuth tokens.

This is a deliberate security design rather than a naming convention, and it buys two things.

Detection becomes reliable. A prefixed, fixed-shape credential is something a scanner can match with high confidence and few false positives. It is the reason secret scanning and push protection work well for GitHub’s own tokens and less well for a bespoke internal token format that looks like any other string.

You can grep for them. A credential you can recognise is a credential you can search logs, build output and repositories for — which is exactly the audit in the next section.

The corollary is worth acting on for anything you issue yourself: give your own service tokens an unmistakable prefix. It costs nothing at design time and it is the difference between a credential a scanner can find and one that only turns up after it has been used.

Tokens are the hardest identity to inventory, because a personal one belongs to an account rather than to the organisation and there is no complete organisation-wide list without policy in place.

What you can do:

Per person. The account’s token settings page lists every token, its scope, its expiry and when it was last used. A token unused for six months is a token to revoke.

Per organisation, with approval enabled. The approval list is an inventory. Without approval required, there is no equivalent view — which is the strongest practical argument for turning it on.

Indirectly, through the audit log. Actions taken by a token appear attributed to the person. Unusual activity attributed to somebody who was on holiday is the classic detection.

In your own repositories. Search for tokens that ended up in files:

Terminal window
{/* GitHub's fine-grained token prefix, in tracked files */}
git grep -n "github_pat_" -- . || echo "none found in the working tree"

Working-tree searches only cover today’s state. History is where committed credentials live — see Removing secrets from Git history, and rotate before cleaning.

Selecting “All repositories” out of convenience. It recreates the classic token’s blast radius inside the newer mechanism.

Granting Write where Read would do. The permission picker offers both for nearly everything, and Read is right far more often than it is chosen.

Setting no expiry. The single most consequential field on the form, and the one people skip because a date requires a decision.

One token used in six places. Rotation becomes an archaeology exercise, and the token’s scope is the union of six jobs.

A personal token running production automation. It leaves when the person does, and its actions are attributed to them in the meantime.

Treating “fine-grained” as “safe”. Narrow scope reduces the consequence of a leak. It does nothing about the likelihood.

Pasting a token into chat to unblock somebody. That token is now in an archive, several backups and any integration that chat system has. Rotate it.

A fine-grained token is a narrow, dated capability rather than an identity. It answers “what may the holder of this string do, and until when?” — which is why the two fields that matter most are the repository list and the expiry date.

  • Fine-grained tokens scope on three axes: resource owner, repositories, and per-permission level
  • One token cannot span two organisations, which is usually a signal to use an App
  • They cannot currently reach the Checks API, Packages or user-owned Projects
  • Expiry runs from 1 to 366 days or none, and organisation policy can enforce a maximum
  • Requiring organisation approval produces the token inventory nothing else provides
  • Expiry bounds an undetected leak; scope bounds the damage; neither prevents the leak
  • Rotation is create, switch, verify, revoke — reversed to revoke-first for an exposure
  • GitHub Apps beat personal tokens for anything automated: own identity, hourly tokens, survives departure
  • A token pasted into chat should be treated as published

Use a disposable repository and a token you will revoke at the end.

  1. Create a fine-grained token with the shortest expiry available, scoped to that repository, with only contents: read.

  2. Use it to read the repository: curl -s -H "Authorization: Bearer YOUR_TOKEN" https://api.github.com/repos/OWNER/REPO | head.

  3. Predict: can it read a different repository you own? Try. Read the error body, not just the status code.

  4. Predict: can it push? Try git push with the token as the password. Note which HTTP status comes back and how it differs from step 3.

  5. Add contents: write and try again.

  6. Look at the token’s page and find “last used”. Predict: does the failed attempt in step 3 count as use?

  7. Revoke the token. Repeat step 2 and confirm a 401.

  8. If you administer a test organisation, enable “require approval for fine-grained tokens” and create another. Observe what the token can do while pending.

Check your understanding

4 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.

Which two fields on the fine-grained token form matter most, according to the lesson's mental model?
Show answer

The repository list and the expiry date — A fine-grained token is a narrow, dated capability. The repository list bounds what it can touch; the expiry bounds how long an undetected leak lasts.

What does a narrow token scope do about a leak?
Show answer

Reduces the consequence of a leak, but nothing about its likelihood — Scope limits damage; expiry limits duration. Neither stops the string being copied somewhere it should not be.

A token was pasted into a chat channel to unblock someone. What should happen?
Show answer

Treat it as published: rotate it — Chat systems archive, back up and integrate. Deleting the message does not retrieve the copies; only rotation closes the exposure.

Which needs a GitHub App rather than a fine-grained token?
Show answer

Automation that spans two organisations — One fine-grained token cannot span two organisations. Apps also give automation its own identity, hourly tokens and survival past staff departures.

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

The permissions-by-task matrix for fine-grained tokens — 24 jobs, the exact permission each needs — is in the Professional Toolkit.