Attach & share
Attach & share
The three commands that get media onto a PR, an issue, or a plain URL.
Attach files to a PR #
attach is the command you'll use most. From a checked-out branch with an open pull request, it needs no arguments beyond the files:
β¦which lands on the pull request as one managed comment:
π Attachments
Run attach again with new files and this same comment updates in place, so your PR doesn't collect a trail of stale screenshot comments.
It uses the gh CLI to find the current repo and PR. If you're not on a PR branch, or you want a different target, say so explicitly:
Want the URLs and ready-to-paste Markdown without touching any GitHub comment (say, to drop an image into a PR description yourself)?
Stage before a PR exists #
Don't wait for a PR to capture something. As of issue #403, a bare put on a non-default git branch already stages by default β no --branch flag needed:
A bare uploads screenshot (no --pr/--issue/ --branch) stages the same way, carrying its derived path/ url/env/viewport metadata through promotion β so capturing before the PR exists no longer loses that context.
attach --branch is the explicit, multi-file form of the same thing β reach for it to upload several files at once with shared flags, or to force staging outside a plain upload:
See what's queued for the current branch, and whether it'll actually auto-attach, with uploads staged:
after.webp 94.2 KB staged 2026-07-22T14:00:00Z https://storage.uploads.sh/gh/you/app/branch/feat-nav/after.webp binding: self β these auto-attach when this branch's PR opens once the PR exists: uploads attach --promote
The binding line is the thing worth reading: self means these files will actually auto-attach when the PR opens; none or other means they won't (the repo isn't linked to this workspace, or it's linked to a different one), and unknown means the check itself couldn't run. Pass --format json for a machine-readable version β it always prints a valid document, even with zero files staged. See GitHub App: staging before a PR exists for what each binding state means and how to fix it.
Once the PR opens, the next attach against it promotes those staged files automatically β or run uploads attach --promote with no files to do it explicitly (--no-promote opts out). Promotion is copy-and-keep: the staged original is never deleted, so a URL you already embedded (in the PR body, say, before it was even opened) keeps serving.
Pair a before and after #
Tag a capture with --state before or --state after and the two render side by side in the comment, under Before and After headings, instead of stacking:
Two files pair when they share a --meta path value and one is before while the other is after. That's the form to use when a single comment carries several pairs β the path is what keeps them from crossing:
With no usable path, filenames are the fallback: stems that differ only by a before or after token pair up, so hero-before.webp goes with hero-after.webp. The token has to be its own word β beforehand.webp doesn't match. A group with two befores and one after is ambiguous and won't pair; it falls back to the normal stacked list. Only images pair.
Pairing isn't only a comment thing β a file's public page shows its counterpart next to it, with a link either way. --state also takes empty, error, and loading for the other states worth capturing; those are searchable with uploads find but don't pair.
Get a URL for any file #
put is the simpler building block: upload one file, get back a public URL and Markdown you can paste anywhere.
# on a non-default git branch, bare put stages for that branch
>> optimized 411.5 KB β 94.2 KB (shot.webp)
URL: https://storage.uploads.sh/gh/you/app/branch/feat-shot/shot.webp
MARKDOWN: 
note: staged for branch feat/shot β auto-attaches to this branch's PR when
it opens (or run: uploads attach --promote once it exists). Use --ref/
--prefix for a plain dated upload.On a branch, a bare put stages the file for that branch's future PR β same behavior as attach, above. Everywhere else it keeps the plain dated path shown next.
# on the default branch (or --no-git), the classic dated layout applies
>> optimized 411.5 KB β 94.2 KB (shot.webp)
URL: https://storage.uploads.sh/screenshots/app/2026-07-12/shot-9f2c1a.webp
MARKDOWN: A few useful variations:
By default, images are re-encoded to WebP (capped size, high quality) so GitHub embeds stay fast, and EXIF is stripped from the uploaded bytes. Use --keep-exif to keep it, or --no-optimize to skip all processing.
Before stripping, a small allowlist is read off the image and stored as searchable metadata: viewport, device, software, and captured. Those values are publicly visible on the file's page. GPS coordinates, serial numbers, and personal-name tags are never kept. Pass --no-auto to store none of it.
Each file also gets a shareable page on uploads.sh (under /f/β¦) that supports oEmbed β so chat apps, notes tools, and other unfurlers can embed the image when you paste the page link. Details are in Reference.
Details: any of --pr, --issue, --key, --ref, --prefix, or --destination opts a bare put out of branch staging, as does --no-git. Staged keys have no content-hash suffix (unlike the dated layout above) β they're stable per filename, so re-uploading the same file replaces it in place instead of minting a new URL.
Capture a screenshot #
Don't have the image yet? screenshot renders a URL or a local.html file and hosts the result in one step, with no browser install required.
# renders the page, then hosts the WebP in one step
>> captured via local backend
URL: https://storage.uploads.sh/gh/app/example/pull/123/app.example.webp
MARKDOWN: A few useful variations:
If a Chrome or Chromium is already on the machine, screenshot drives it directly (nothing to download); otherwise it renders on uploads.sh servers. Force either side with --via local or --via remote. Note thatlocalhost URLs are only reachable locally.
--out also writes a sidecar, <file>.uploads.json, recording this capture's derived metadata (path/url/env/viewport, plus --state if given) and a content hash. A later put or attach of that same file picks the metadata back up automatically β pass --no-sidecar to skip writing it.
When you shoot your own dev server, it hides known framework dev toolbars (Astro, Next, Nuxt, Vite) automatically (opt out with--no-hide-dev-tools) and takes --reduced-motion to settle animations. Hide any other overlay with --hide <selector>(repeatable), or run setup JS first with --eval <js> /--init-script <file> (local backend).