GHCR push denied - packages:write permission missing
Docker push to ghcr.io fails with 'denied: permission_denied' or '403 Forbidden'
Focused incident mode — strips away everything but this runbook.
Symptoms
- Docker push to ghcr.io fails with 'denied: permission_denied' or '403 Forbidden'
- Workflow step 'docker/build-push-action' fails at the push phase
- Error: 'insufficient permission: user does not have write access to package'
Evidence to grab
Collect these artifacts before you change anything — they prove (or rule out) each cause.
Docker login and push logs
gh run view <RUN_ID> --log | grep -E 'login|push|denied'Workflow permissions block
cat .github/workflows/<file>.yml | grep -A10 'permissions'
Diagnostic flow
Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.
Guided decision tree is ready.
Steps with commands, expected output, and pass/fail branches.
Ranked root causes
Evidence checklist
- Workflow uses actions: write but not packages: write
- permissions block restricts to contents: read only
Confirm before fixing
cat .github/workflows/<file>.yml | grep -A10 'permissions'Read the permissions block
Fix
sed -i '/permissions:/a\ packages: write' .github/workflows/<file>.ymlAdd packages: write to the permissions block
Resolution runbook
Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.
cat .github/workflows/<file>.yml | grep -A15 'permissions'Read current token permissions
gh run view <RUN_ID> --log-failed | grep -E 'denied|forbidden'Capture the exact error
sed -i '/permissions:/a\ packages: write' .github/workflows/<file>.ymlAdd packages:write to the workflow permissions
gh workflow run <file>.yml --ref <BRANCH>Re-run the workflow after the fix
Prevention
- Always include packages: write in the permissions block of workflows that push container images.
- Use GITHUB_TOKEN instead of PATs when possible to avoid secret rotation.
- Set GHCR package visibility to private and scope permissions to specific repos.
- Add a step that validates the token scope before attempting the push.
Related runbooks
Same stack, same failure family — worth a skim.
Error: Resource not accessible by integration
Step logs show empty string where a secret should be
Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.