The report nobody could open
We write incident reports as single HTML files: inline CSS, an inline SVG timeline, no build step, one file you can read in ten years. They live in a private GitHub repository next to the code they are about.
Nobody could open them. GitHub shows an .html file as source, and the "download it, then double-click it" workflow is where reading stops for most people. GitHub Pages would render them, but Pages on a private repository is an Enterprise feature. GitHub's PDF viewer exists, but these are not PDFs.
The extension that wanted my token
There is a Chrome extension for exactly this problem. I read its code before installing it. It asks for a GitHub token (a personal access token, or an OAuth grant with full repo scope), sends that token to the author's server, and the server fetches your file and renders it on the author's domain.
That is a lot of trust for a preview button. An incident report is the most sensitive document a team writes, and the fix was to hand the key to the whole repository to a stranger's Lambda.
The other option, a userscript, keeps the token in the browser but resolves the short-lived tokenised link GitHub issues for a private file and passes it, as a URL parameter, to the author's website, which loads the file from there. The link to your private file lands in that site's address bar, history and hosting logs.
Both are honest projects, and the idea of a button beside Raw is theirs. I just did not want the file to leave the browser.
The Raw button already does the hard part
Click Raw on a private file and watch the network tab. GitHub answers github.com/<owner>/<repo>/raw/<ref>/<path> with your own session, then redirects to raw.githubusercontent.com with a token that expires in minutes. That host serves the file as text/plain, which is why it is never rendered, and sends Access-Control-Allow-Origin: *. And github.com's Content Security Policy allows connections to it.
So a script running on the file page can do what Raw does, and then fix the only thing that was wrong, the content type:
const tab = window.open('', '_blank'); // open inside the click, so popup blockers allow it
const response = await fetch(location.pathname.replace('/blob/', '/raw/'), { credentials: 'same-origin' });
const html = await response.text();
tab.location.href = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
That is the whole mechanism. The only network request is the one GitHub itself makes for Raw. No server, no token, no account, no site other than github.com in the path. The rendered page lives at a blob: URL in your browser, with no cookies and no access to GitHub.
80 lines instead
The result is unraw: a userscript that adds an unraw button next to Raw on every .html file page, and a bookmarklet for people who do not want to install anything. The button copies Raw's classes and data-* attributes so it matches GitHub's current styling, and a MutationObserver puts it back after GitHub's client-side navigation. The rest is the snippet above with error handling. Your userscript manager shows you all of it before installing; it is about 80 lines.
Two limits, both inherent to the approach. Pages that reference relative files (./style.css, img/x.png) render without them, because those paths do not exist at a blob: URL; self-contained pages render completely, and inlining is the right habit for documents you want to read from GitHub anyway. Inline scripts run inside the blob: page, isolated from GitHub, so treat a preview the way you would treat opening a downloaded copy.
The rule that produced the tool is the rule that keeps it small: the file may not leave your browser. Anything that would need a server or a token will not be added.
- Install page, with the bookmarklet: https://thangaram611.github.io/unraw/
- Userscript on Greasy Fork: https://greasyfork.org/scripts/600316-unraw
- Source, MIT, with a comparison table drawn from each tool's code: https://github.com/thangaram611/unraw

Top comments (0)