diff --git a/cloudflare/workers-github-oauth.md b/cloudflare/workers-github-oauth.md
new file mode 100644
index 000000000..707d28846
--- /dev/null
+++ b/cloudflare/workers-github-oauth.md
@@ -0,0 +1,161 @@
+# GitHub OAuth for a static site using Cloudflare Workers
+
+My [tools.simonwillison.net](https://tools.simonwillison.net/) site is a growing collection of small HTML and JavaScript applications hosted as static files on GitHub Pages.
+
+Many of those tools take advantage of external APIs such as those provided by OpenAI and Anthropic and Google Gemini, thanks to the increasingly common `access-control-allow-origin: *` [CORS header](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
+
+I want to start building tools that work with [the GitHub API](https://docs.github.com/en/rest), in order to implement things like saving data to a [Gist](https://gist.github.com/)
+
+To do that, I needed to implement [OAuth](https://docs.github.com/en/apps/oauth-apps): redirecting users to GitHub to request permission to access their data and then storing an access token in their browser's `localStorage` to be used by JavaScript running on my site.
+
+There is just one catch: it currently isn't possible to implement GitHub OAuth entirely from the client, because that API depends on a secret that must be held server-side and cannot be exposed.
+
+This morning, I had an idea: my tools site is [hosted by GitHub Pages](https://github.com/simonw/tools), but it's served via my [Cloudflare](https://www.cloudflare.com/) account for the `simonwillison.net` domain.
+
+Could I spin up a tiny [Cloudflare Workers](https://workers.cloudflare.com/) server-side script implementing GitHub OAuth and add it to a path on that `tools` subdomain?
+
+The answer turned out to be yes.
+
+## Getting Claude to write me a Worker
+
+I prompted [Claude](https://claude.ai/) with the following:
+
+> `Write a Cloudflare worker that implements an oauth flow with GitHub to get a token scoped for gist read and write only`
+>
+> `Landing on the worker page redirects to GitHub for the oauth - GitHub sends back to the same page, which then outputs a script block that sets the access key in localstorage`
+
+This is the simplest possible design for an OAuth flow: send the user straight to GitHub, then exchange the resulting `?code=` for an access token and write that to `localStorage`.
+
+Here's [the full Claude transcript](https://gist.github.com/simonw/975b8934066417fe771561a1b672ad4f). Claude gave me almost *exactly* what I needed - the only missing detail was that it set the `redirectUri` to `url.origin` (just the site domain) when it should have been the full URL to the worker page.
+
+I edited the code to its final version, which looked like this:
+```javascript
+export default {
+ async fetch(request, env) {
+ const url = new URL(request.url);
+ const clientId = env.GITHUB_CLIENT_ID;
+ const clientSecret = env.GITHUB_CLIENT_SECRET;
+ const redirectUri = env.GITHUB_REDIRECT_URI;
+
+ // If we have a code, exchange it for an access token
+ if (url.searchParams.has('code')) {
+ const code = url.searchParams.get('code');
+
+ // Exchange the code for an access token
+ const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify({
+ client_id: clientId,
+ client_secret: clientSecret,
+ code: code,
+ redirect_uri: redirectUri
+ })
+ });
+
+ const tokenData = await tokenResponse.json();
+
+ // Return HTML that stores the token and closes the window
+ return new Response(`
+
+
+
+ GitHub OAuth Success
+
+
+
+
+
+ `, {
+ headers: {
+ 'Content-Type': 'text/html'
+ }
+ });
+ }
+
+ // If no code, redirect to GitHub OAuth
+ const githubAuthUrl = new URL('https://github.com/login/oauth/authorize');
+ githubAuthUrl.searchParams.set('client_id', clientId);
+ githubAuthUrl.searchParams.set('redirect_uri', redirectUri);
+ githubAuthUrl.searchParams.set('scope', 'gist');
+ githubAuthUrl.searchParams.set('state', crypto.randomUUID());
+
+ return Response.redirect(githubAuthUrl.toString(), 302);
+ }
+};
+```
+I find it hard to imagine a simpler implementation of this pattern.
+
+The GitHub API has been around for a very long time, so it shouldn't be surprising that Claude knows exactly how to write the above code. I was still delighted at how much work it had saved me.
+
+(I should mention now that I completed this entire project on my phone, before I got up to make the morning coffee.)
+
+## Deploying this to Cloudflare Workers
+
+There are four steps to deploying this:
+
+1. Configuring a GitHub OAuth application to get a client ID and secret
+2. Create a new worker with the code
+3. Set the three variables it needs
+4. Configure the correct URL to serve the worker
+
+I created the GitHub OAuth app here: https://github.com/settings/applications/new
+
+The most important thing to get right here is the "Authorization callback URL": I set that to `https://tools.simonwillison.net/github-auth` - a URL that didn't exist yet but would after I deployed the Worker.
+
+Then in my Cloudflare dashboard, I navigated to Workers & Pages and clicked "Create" and then "Create Worker".
+
+I deployed the default "Hello world" example and edited it to paste in the code that Claude had written for me.
+
+The Cloudflare editing UI was *not* built with mobile phones in mind, but I just managed to paste in my code with it!
+
+
+
+I used the "settings" page to set the `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` and `GITHUB_REDIRECT_URI` environment variables needed by the Worker.
+
+The last step was to configure that URL. I navigated to my `simonwillison.net` Cloudflare dashboard, hit "Workers Routes" and then added a new route mapping `tools.simonwillison.net/github-auth*` to the Worker I had just created.
+
+That final `*` wildcard turned out to be necessary to ensure that `?code=` querystring URLs would also activate the `/github-auth` worker - without that I got a 404 served by GitHub Pages for those URLs instead.
+
+## Using this from an application
+
+The OAuth flow works by setting a `github_token` key in `localStorage()` for the entire `tools.simonwillison.net` domain - which means JavaScript on any page can check for that key and make API calls to the GitHub Gist API if the key is present.
+
+Here's some more code [Claude wrote](https://gist.github.com/simonw/29efb202da39775761ab6ab498d942ca) for integrating the new auth mechanism into an existing tool:
+
+```javascript
+function checkGithubAuth() {
+ const token = localStorage.getItem('github_token');
+ if (token) {
+ authLinkContainer.style.display = 'none';
+ saveGistBtn.style.display = 'inline-block';
+ } else {
+ authLinkContainer.style.display = 'inline-block';
+ saveGistBtn.style.display = 'none';
+ }
+}
+
+function startAuthPoll() {
+ const pollInterval = setInterval(() => {
+ if (localStorage.getItem('github_token')) {
+ checkGithubAuth();
+ clearInterval(pollInterval);
+ }
+ }, 1000);
+}
+
+authLink.addEventListener('click', () => {
+ window.open('https://tools.simonwillison.net/github-auth', 'github-auth', 'width=600,height=800');
+ startAuthPoll();
+});
+```
+
+`authLink` here is a reference to a "Authenticate with GitHub" link on that page. Clicking that link uses `window.open` to open a popup showing my new `/github-auth` page, which then automatically redirects to GitHub for permission.
+
+Clicking the link also starts a every-second poll to check if `github_token` has been set in `localStorage` yet. As soon as that becomes available the polling ends, the "Authenticate with GitHub" UI is hidden and a new `saveGistBtn` (a button to save a Gist) is made visible.