GitHub Actions checkout persisted credentials push 403
actions/checkout persists the GITHUB_TOKEN for later git commands. If that token is read-only, a push fails with 403. Retrying will not change the token scope.
What this error means
A push after checkout fails with 403 even though credentials are persisted, because the persisted token lacks contents write.
remote: Permission to octo/repo.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/octo/repo/': The requested URL returned error: 403Diagnose it: what token do you actually have?
Permission failures in Actions are almost never about your repository settings alone. Three things combine: the default GITHUB_TOKEN permission set for the repo or organization, the permissions: block in the workflow, and whether the event is a fork pull request, which downgrades the token to read-only regardless of everything else.
- name: Show the token scopes actually granted
run: |
curl -sI -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/ | grep -i "^x-oauth-scopes\|^x-accepted"
echo "event: ${{ github.event_name }}"
echo "fork PR: ${{ github.event.pull_request.head.repo.fork }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Common causes
Persisted token is read-only
checkout persists whatever token it used; if permissions are read-only, the push is denied.
Fork run token
On fork PRs the persisted token cannot push to the base repository.
How to fix it
Grant write or persist a stronger token
- Add permissions: contents: write so the persisted token can push.
- Or check out with a PAT that has write access.
permissions:
contents: write
steps:
- uses: actions/checkout@v4Grant the narrowest permission that works
Declaring a permissions: block switches the job from the repository default to exactly what you list, so an incomplete block is a common cause of a new failure right after someone tightened security. List every scope the job needs, not just the one that failed.
permissions:
contents: read # checkout
packages: write # push to GHCR
id-token: write # OIDC to a cloud provider
pull-requests: write # comment on or label a PR
checks: write # publish check runsHow to prevent it
- Set contents: write only on jobs that push.
- Do not rely on the persisted token for pushes from fork PRs.