github-image-upload
Upload local images or video to GitHub and embed them in pull requests, issues or comments, without drag-and-drop and without committing them to the repo. Use when asked to attach, add or upload scree
SKILL.md.claude/skills/github-image-upload/SKILL.md
GitHubGitHub image upload
Both routes below put the file in the same place — GitHub's user-attachments store — and both
authenticate with an ordinary gh auth token. Pick by whether you need the URL in hand.
| You want | Route |
|---|---|
An image or video embedded in a body gh is writing |
gh --attach |
The URL itself — sized <img>, a body you patch yourself, a table |
scripts/upload-asset.sh |
Route A — gh --attach
Needs gh 2.99.0 or newer. Available on issue create, issue edit, issue comment,
pr create, pr edit, pr comment.
gh pr comment 13 --attach './login.png#The login error state'
Alt text follows the path after #; without it gh uses the filename. Video renders as a player
and takes no alt text, so giving it any is an error.
Attachments append to the end of the body. To place them yourself, reference the local paths in the body and attach those same files — each markdown reference is rewritten to point at the uploaded asset:
gh pr create --body-file report.md --attach ./before.png --attach ./after.png
Only markdown references are rewritten —  and [text](./before.png), inline
or reference-style, outside code fences. An <img src="./before.png"> is not a markdown reference,
so the file is appended instead and the tag stays pointing at a path that does not exist. Sized
images mean route B.
Refused with --web and --dry-run, and on GitHub Enterprise Server. Uploads cannot be undone, so
gh writes the body even when only some attachments succeeded.
Route B — upload, then embed
scripts/upload-asset.sh OWNER/REPO before.png after.png
# https://github.com/user-attachments/assets/<uuid> (one per line, in order)
Embed with <img src="…" width="460"> rather than  when the width matters, which it does in
a before/after table:
### Desktop
| Before | After |
|---|---|
| <img src="https://github.com/user-attachments/assets/UUID-A" width="460"> | <img src="…UUID-B" width="460"> |
### Mobile (390px)
| Before | After |
|---|---|
| <img src="…UUID-C" width="260"> | <img src="…UUID-D" width="260"> |
gh pr edit silently fails on repos still carrying classic Projects, so append to an existing body
through the API instead:
cur=$(gh api repos/OWNER/REPO/pulls/123 --jq .body)
printf '%s\n\n---\n\n%s\n' "$cur" "$NEW_SECTION" > /tmp/body.md
gh api -X PATCH repos/OWNER/REPO/pulls/123 -F body=@/tmp/body.md
-F body=@file avoids shell-quoting problems with markdown. Issues and comments are
repos/OWNER/REPO/issues/123 and repos/OWNER/REPO/issues/comments/<id>.
Constraints
- Extensions:
pngjpgjpeggifwebpsvgmp4movwebm - 10MB per image, 100MB per video, and the server holds video to the account plan below that
- Write access or better — read and triage both get a 404
- An OAuth token, PAT or fine-grained PAT. A GitHub Actions token cannot upload
- github.com and GHEC with data residency; not Enterprise Server
Gotchas
- Anonymous
curlon the asset URL returns 404. Assets inherit repo visibility. Authenticate (-H "Authorization: token $(gh auth token)") to download one, and verify a render by checkingnaturalWidth > 0on the page rather than by fetching the URL. - GitHub rewrites the URL at render time into a signed
private-user-images.githubusercontent.comlink. Store theuser-attachmentsone; it stays valid. - Encode video as H.264 (
ffmpeg -i in.webm -c:v libx264 -pix_fmt yuv420p out.mp4) so GitHub renders a native<video>rather than a download link.
When not to use this
If the images get regenerated often, or readers need them without repo access, commit them to a
branch and embed raw.githubusercontent.com URLs instead.