gh-pr-image — Upload Images to GitHub PRs
Upload local images (screenshots, diagrams, visual diffs) to a GitHub PR description without committing them to the repository. Uses GitHub release assets as a CDN — the only fully scriptable, CLI-only approach that works for private repos.
Why This Exists
GitHub has no public API for uploading images to PR descriptions. The drag-and-drop upload in the web UI uses an undocumented endpoint (/upload/policies/assets) that requires browser session cookies — it cannot be called with an API token.
The available workarounds and their trade-offs:
| Approach | Works from CLI? | Private repos? | Downsides |
|---|---|---|---|
| Commit images to repo | Yes | Yes | Bloats the repo, images live forever in git history |
| Drag-and-drop on web | No | Yes | Manual, breaks CLI workflow |
| External host (S3, imgur) | Yes | N/A | Requires separate service, images may expire |
| Browser automation (gh-attach) | Partially | Yes | Requires Playwright + browser login session |
| Release assets (this skill) | Yes | Yes | Creates a prerelease tag in the repo |
How It Works
- Create a prerelease with the image files attached as assets
- Extract the
browser_download_urlfor each asset - Embed the URLs in the PR body using standard markdown
Release asset URLs are accessible to anyone with repo read access, making them work correctly in private repo PR descriptions.
Usage
Upload images and update a PR description
# Variables
REPO="owner/repo" # or auto-detect from current directory
PR_NUMBER=137
TAG="pr-${PR_NUMBER}-images" # one tag per PR keeps things organized
# 1. Create prerelease with images attached
gh release create "$TAG" \
/tmp/before.png /tmp/after.png \
--repo "$REPO" \
--title "PR #${PR_NUMBER} screenshots" \
--notes "Image assets for PR #${PR_NUMBER}." \
--prerelease
# 2. Get the asset URLs
BEFORE_URL=$(gh api "repos/$REPO/releases/tags/$TAG" \
--jq '.assets[] | select(.name=="before.png") | .browser_download_url')
AFTER_URL=$(gh api "repos/$REPO/releases/tags/$TAG" \
--jq '.assets[] | select(.name=="after.png") | .browser_download_url')
# 3. Use in PR body
gh pr edit "$PR_NUMBER" --body "$(cat <<EOF
## Summary
Description here.
## Before

## After

EOF
)"
Upload a single image and get the URL
REPO="owner/repo"
TAG="pr-assets-$(date +%Y%m%d%H%M%S)"
FILE=/tmp/screenshot.png
gh release create "$TAG" "$FILE" \
--repo "$REPO" \
--title "PR screenshots" \
--notes "Image hosting for PRs." \
--prerelease
URL=$(gh api "repos/$REPO/releases/tags/$TAG" \
--jq ".assets[0].browser_download_url")
echo ""
Add images to an existing release tag
If you want to reuse a single release as a persistent image store:
TAG="pr-assets"
# First time: create the release
gh release create "$TAG" \
--repo "$REPO" \
--title "PR image assets" \
--notes "Persistent image hosting for PR descriptions." \
--prerelease
# Subsequent uploads: add to existing release
gh release upload "$TAG" /tmp/new-screenshot.png --repo "$REPO"
URL=$(gh api "repos/$REPO/releases/tags/$TAG" \
--jq '.assets[] | select(.name=="new-screenshot.png") | .browser_download_url')
Cleanup
Prerelease tags can be cleaned up after PRs are merged:
# Delete a specific PR's image release
gh release delete "pr-137-images" --yes --cleanup-tag
# List all image releases
gh release list | grep "pr-.*-images"
Requirements
ghCLI, authenticated (gh auth status)- Write access to the repository (to create releases)
Tips
- Use
--prereleaseso image-hosting releases don't appear as real releases - Name tags with the PR number (
pr-137-images) for easy cleanup - Asset names must be unique within a release — use descriptive filenames
- For private repos, asset URLs require GitHub authentication to download, which is automatic when viewed in a PR description by someone with repo access