GitHub Actions deploy fails with 403 after repo transfer because GITHUB_TOKEN lacks packages: write
GitHub Actions deploy fails with 403 after repo transfer because GITHUB_TOKEN lacks packages: write
The deploy job goes red immediately after a repository transfer. The push step logs 403 Forbidden or permission_denied: write_package, and the release is blocked while you compare settings between the old and new owner. A GitHub Actions 403 packages write error is not a credentials typo — it usually means the automatic GITHUB_TOKEN cannot write to the package namespace after the move.
What the 403 actually means
GITHUB_TOKEN is scoped to the repository running the workflow. Writing to GitHub Packages or GHCR requires the token to have packages: write, and the package must grant access to that repository. After a transfer, two things can change: the repository inherits the destination owner’s default workflow permissions, and the package may still belong to the old owner or namespace. [link needed]
If the workflow does not request packages: write, or the repository default is read-only, the push fails before Docker or npm ever authenticates correctly. [link needed]
Fix GitHub Actions 403 packages write failures
Use an explicit permission block instead of relying on repository defaults. Keep the scope minimal.
1. Add permissions to the job that publishes the package. If checkout runs in the same job, keep contents: read:
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# build and push step
2. Open Settings → Actions → General → Workflow permissions and confirm the default. You can leave the default read-only if every publishing job declares packages: write. Only switch the repository default to read/write if other workflows depend on it. [link needed]
3. In the package settings, grant the new repository Write access under the Actions access section. For GHCR, confirm the container package is linked to the new repository. [link needed]
4. Re-run the failed job. The 403 should disappear; if it does not, check whether the package is still owned by the old namespace.
Check package ownership after the transfer
A transfer moves the repository, not necessarily every package. If the image or npm package still sits under the old user or organization, GITHUB_TOKEN from the new repository may have no write path to it. Open the package’s settings, confirm the owner, and either grant the new repository access or transfer the package to the destination owner. Then repeat the permission check. Without this step, a token with packages: write can still receive a GitHub Actions 403 packages write error. [link needed]
When NOT to do this
Do not add packages: write if the job only builds and never publishes. Do not use permissions: write-all just to make the error disappear; it hides which scope was missing. If deployment already uses a PAT or GitHub App token with package scope, changing GITHUB_TOKEN permissions will not affect that auth path. Also check the registry: packages: write matters for GitHub Packages and GHCR, not Docker Hub.
Checklist
- [ ] Publishing job declares
packages: write - [ ] Required
contents: readis still present - [ ] Repository default workflow permissions checked
- [ ] New repository has Write access to the package
- [ ] Package owner and repository link verified after transfer
- [ ] Failed job re-run with the same commit