Upload images and files to GitHub (gh-image)
GitHub has no public API for attachment uploads — the web UI uses an internal
endpoint that mints user-attachments URLs scoped to the repo's visibility.
gh-image (MIT, © drogers0) replicates
that flow as a gh CLI extension, so you can upload images or other files (PDF,
zip, log, …) from the terminal and get a ready-to-paste reference back — an
 embed for images, a bare URL for videos (GitHub renders it as an
inline player), or a [name](url) download link for other files.
This skill drives gh-image and then embeds the result into a PR/issue/comment.
Prerequisites — verify these before uploading
Run these checks; only act on the ones that fail.
ghCLI installed & authenticatedgh auth statusIf it fails, tell the user to run
gh auth login(do not attempt it unattended).The
gh-imageextension installed (idempotent — skip if already present)gh extension list | grep -q 'drogers0/gh-image' || gh extension install drogers0/gh-imageA GitHub session for the upload.
gh-imagedoes NOT use theghtoken for the upload (that endpoint rejects tokens); it needs the browseruser_sessioncookie. Resolution order (first match wins):--token <value>flag, orGH_SESSION_TOKENenv var (use this in CI / headless), or- the cookie store of a logged-in browser (Chrome/Brave/Chromium/Edge/Firefox/ Opera/Safari) — the default for local use. On macOS the first read may show a Keychain prompt; the user should click Always Allow.
⚠️ A
user_sessioncookie grants full account access (it is not scoped like a PAT). Treat it like a password; in CI use a dedicated bot account.💡 The user may hand you the session token directly (e.g. "you can use the user session token … to call it"). Do not echo it, print it, write it to a file, or commit it. When they do,
export GH_SESSION_TOKENfor the current shell session so subsequent uploads work without re-passing the token:export GH_SESSION_TOKEN="<session-token>"Note the export only persists for that shell session — it is lost when a new terminal/session starts, so re-export or pass
--tokenon later runs. (Do not append it to~/.bashrcor similar; storing session cookies there is discouraged.) It expires, so if an upload fails later, ask the user for a fresh one.
Step 1 — Normalize the file path
Use an absolute path. If a glob is given, resolve it first. Paths with spaces or Unicode (e.g. CleanShot's narrow spaces) work, but quote them.
Step 2 — Upload
# One or more files (images or PDF/zip/log/…); --repo is optional inside a repo
# working dir (inferred from the remote).
gh image "/abs/path/screenshot.png" --repo <owner>/<repo>
# With a user-provided session token (see note in Prerequisites). If you already
# exported GH_SESSION_TOKEN for the session, no --token/--repo is needed here:
export GH_SESSION_TOKEN="<session-token>"
gh image "/abs/path/screenshot.png" --repo <owner>/<repo>
gh image prints the reference to stdout — an image embed for images, a bare
URL for videos (GitHub renders it as an inline player), and a download link for
other files, e.g.:

https://github.com/user-attachments/assets/<uuid>
[report.pdf](https://github.com/user-attachments/files/<id>/report.pdf)
Capture that output — it is the embeddable reference. For multiple files it prints one line per file.
Step 3 — Embed into the PR / issue / comment
gh-image only prints the markdown; you embed it. Pick the target the user asked for.
Append to a PR description (preserves the existing body):
MD="$(gh image "/abs/path/shot.png" --repo owner/repo)"
BODY="$(gh pr view <pr> --repo owner/repo --json body -q .body)"
printf '%s\n\n## Screenshots\n\n%s\n' "$BODY" "$MD" \
| gh pr edit <pr> --repo owner/repo --body-file -
Post as a new PR comment:
MD="$(gh image "/abs/path/shot.png" --repo owner/repo)"
printf '## Screenshots\n\n%s\n' "$MD" | gh pr comment <pr> --repo owner/repo --body-file -
Add to an issue body / comment: same pattern with gh issue edit <n> --body-file -
or gh issue comment <n> --body-file -.
Always use --body-file - (not inline --body) so multi-line bodies and special
characters can't break shell quoting.
Step 4 — Verify
gh pr view <pr> --repo owner/repo --json body -q .body # confirm the URL is present
The user-attachments URL inherits the repo's visibility, so on a private repo
it renders only for authorized viewers (an anonymous fetch returns 404/403 — that is
expected, not a failure).
Sizing (optional)
To control display size, embed an HTML tag instead of the bare markdown:
<img width="800" alt="screenshot" src="https://github.com/user-attachments/assets/<uuid>" />
Troubleshooting
| Symptom | Cause / fix |
|---|---|
<org> enforces SAML SSO and your session is not authorized… |
The org requires SSO and your session isn't authorized. Open the https://github.com/orgs/<org>/sso URL from the message in a browser, authorize (lasts ~24h), then retry. Write access alone is not enough — this is not a permissions problem. |
uploadToken not found … do you have write access? |
The generic no-token case. Confirm you have write access; if the repo's org uses SSO, authorize at https://github.com/orgs/<org>/sso (the message includes this hint) and retry. |
No user_session cookie found |
Log into GitHub in a supported browser, or set GH_SESSION_TOKEN. |
| Windows + Chrome 127+ can't read cookies | Known cookie-library limitation — use another browser or GH_SESSION_TOKEN. |
| CI / headless run | Set GH_SESSION_TOKEN (dedicated bot account); the browser cookie path won't exist. |
gh: command not found |
Install the GitHub CLI (brew install gh, etc.). |