Where GitHub image URLs come from
When you paste or drag an image into a README, issue, PR, wiki, or discussion, GitHub uploads it for you and hands back a link on user-images.githubusercontent.com (or camo.githubusercontent.com for external images it proxies). It's frictionless, so people treat it as an image host. It isn't one — it's an attachment feature bolted onto the comment box, and the moment your image needs to live anywhere other than a github.com page, the cracks show.
The three ways GitHub images break
1. Private-repo attachments are served behind an expiring token. Since May 2023, images uploaded in a private repo are served from private-user-images.githubusercontent.com with a short-lived signed token baked into the URL. Used inside github.com you never notice. But the token expires within minutes — so the instant that URL is fetched from anywhere else (a cached page, an RSS reader, a docs site, a mirror, or an LLM ingesting your repo), it returns "This private-user-images.githubusercontent.com page can't be found." The link looks permanent; it's anything but.
2. Flipping a repo private → public breaks every embedded image. This one catches people cold. Change a repo's visibility and the attachment URLs your README and issues depended on stop resolving — a wall of broken boxes across your whole project, with no warning and no automatic fix. It's one of the most-reported image issues in GitHub's own community forum.
3. The attachment CDN rate-limits and 404s intermittently. Even for public repos, user-images.githubusercontent.com and the Camo image proxy will throttle with 429 Too Many Requests under load, and images periodically 404 for reasons no one can debug from the outside — because you don't control that infrastructure. When a README on a popular project suddenly shows blank images "for some users," this is usually why.
The common thread: you don't own any of these URLs. They live on GitHub's terms, tied to a repo's visibility and GitHub's rate limits, and they were designed for one context — a logged-in user looking at a github.com page — not for the README that also renders on npm, on your project site, in someone's feed reader, and inside an agent reading your docs.
The fix: host the image on a URL you own
The reliable pattern is the same one that fixes broken images everywhere: put the image behind a permanent, immutable URL, then reference that from your Markdown. With PixelVault that's a single API call, and the URL you get back never expires, never depends on a repo's visibility, and serves from a global CDN with zero egress fees — so a README on a trending project costs the same to serve as one nobody reads.
const form = new FormData();
form.append("file", blob, "architecture.png");
const res = await fetch("https://api.pixelvault.dev/v1/images", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PIXELVAULT_API_KEY}` },
body: form,
});
// The URL is nested under `data` in the response envelope.
const { url } = (await res.json()).data;
// → https://img.pixelvault.dev/proj_abc/img_xyz.png
The URL is stable, so a diagram you host today keeps rendering in every place your README shows up — for years. And because PixelVault resizes on the fly from transform params (?w=700, &fmt=auto), you host the original once and ask for the size you need inline, instead of committing three exports of the same screenshot.
Prefer to just drag and drop, the way you would in the GitHub comment box? There's a free drag-and-drop image-to-URL tool with no signup — but be honest with yourself about what a README needs: those links are temporary and disappear within 24 hours, which is the same broken-box problem one day later. It's great for a quick share in a chat; for anything that lives in a README, use the permanent API or CLI flow above (both take a registered free key).
"Why not just commit the image to the repo?"
Committing images to an /assets or /docs folder and linking them with a relative path is a legitimate option — and for a small, permanent logo it's fine. But it has real downsides for anything that changes: every version of every screenshot lives in your git history forever, bloating clones (which is why teams reach for Git LFS and its own quotas). Relative paths also don't render everywhere the README does — npm's package page and many mirrors need absolute URLs, and a raw raw.githubusercontent.com link puts you right back on GitHub's rate limits. A hosted URL sidesteps all of it: it's absolute, it's not in your git history, and it renders identically everywhere.
Images that shouldn't be public
Sometimes the reason your image lived in a private repo is that it should stay private — a screenshot of an internal dashboard, a staging environment, a customer's data. GitHub's answer is the expiring-token URL that breaks in minutes. PixelVault's is a proper signed URL: upload with visibility: private and the image serves behind an HMAC signature — strip the token and the CDN returns 403 — but you control the lifetime (7 days by default, up to 30), and you can mint a fresh link any time with POST /v1/images/:id/sign-url. It's the same "not on the crawlable web" guarantee, without the link dying before a reviewer clicks it. The same mechanism powers private CI screenshots posted straight into a PR.
Where should README images live?
raw.githubusercontent throttlesFrom your coding agent
If a coding agent is writing your README — Claude Code, Cursor, Codex — it hits the exact wall this post describes: it generates a diagram or grabs a screenshot and has nowhere to put it. Point it at PixelVault and hosting becomes one step in its flow. There's an MCP server so the agent can host as a native tool call, an open-source upload skill, and the pixelvault-cli one-liner above. Generating the image with a model too? See how to host AI-generated images so those don't expire either.
Free to start
PixelVault's free tier includes 200 MB storage, 500 uploads/month, 1 GB bandwidth, and up to 100 private images — no credit card, zero egress. Enough to host the images for a stack of repos before you pay anything, and paid plans start at $9/month. Start from the API quickstart →