GitHub Actions "ghcr.io push denied" (packages: write)
Pushing a container image to the GitHub Container Registry with GITHUB_TOKEN requires packages: write. Without it, the push is denied at the registry. This is a permission/config gap, not a transient failure.
What this error means
docker push to ghcr.io fails with a denied/permission error after a successful docker login using GITHUB_TOKEN.
denied: installation not allowed to Write organization package
Error: buildx failed: error pushing ghcr.io/owner/image:tagDiagnose 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
Missing packages: write permission
The job does not grant packages: write, so GITHUB_TOKEN can authenticate but not push.
Org package write policy blocks the repo
The organization package settings do not allow this repository to write the package.
How to fix it
Grant packages: write and log in correctly
- Add packages: write to the job permissions.
- Log in to ghcr.io with GITHUB_TOKEN before pushing.
- If the package already exists, ensure the repo has write access under the package settings.
publish:
permissions:
packages: write
contents: read
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}Grant 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 packages: write on any job that pushes to ghcr.io.
- Grant the repository write access on existing org-owned packages.