GitHub has no public API for uploading images to PR descriptions. The drag-and-drop in the web UI uses an undocumented endpoint (/upload/policies/assets) that requires browser session cookies – you simply can not call it with an API token or gh.
The GitHub CLI team explicitly closed this as “not planned”, with the explanation that the upload flow requires a browser to obtain temporary S3 credentials. Pretty understandable but still tough to live with, particularly in this agent era we live in.
After going through every option (committing images to the repo, external image hosts, browser automation via gh-attach, the undocumented upload endpoint), the cleanest CLI-only approach that works for private repos is GitHub release assets:
TAG="pr-${PR_NUMBER}-images"
# Create a prerelease with the images attached
gh release create "$TAG" /tmp/before.png /tmp/after.png \
--title "PR #${PR_NUMBER} screenshots" \
--notes "Image assets for PR." \
--prerelease
# Get the URLs
BEFORE_URL=$(gh api "repos/$REPO/releases/tags/$TAG" \
--jq '.assets[] | select(.name=="before.png") | .browser_download_url')
# Use in the PR body
gh pr edit "$PR_NUMBER" --body ""The browser_download_url works for anyone with repo access, so it renders correctly in private repo PR descriptions. Use --prerelease so these don’t pollute your real releases, and name the tag with the PR number for easy cleanup later (e.g. with something like gh release delete "$TAG" --yes --cleanup-tag).
I’ve packaged this into a Claude Code / Codex skill that can be installed with:
npx skills add mrshu/agent-skills/plugins/gh-pr-imageIt is a hack, but compared to other options it still makes it so the images stay on GitHub and you don’t need to worry about third-party hosting.