A free, low-maintenance way to host organized static HTML on a domain you already own, where “publish” is a single step driven by a coding agent — and access can be locked down later without rebuilding anything.
A free GitHub organization with private repositories stores the pages and acts as the deploy hook; Cloudflare Pages (free) watches each repo and redeploys on every git push; a domain already on Cloudflare gives each repo a clean subdomain with automatic SSL; a per-repo scoped token keeps the publish credential tiny; and Cloudflare Access (free) can gate any page later with zero code change.
git push (or one word to an agent). Live in ~1 minute.You want to turn a folder of hand-built (or agent-built) HTML pages into real, shareable URLs — fast, repeatable, and cheap — without standing up a server, a CI system, or a paid hosting plan. You also want the publishing action to be simple enough that an AI coding agent (or a kid, or you in a VR headset) can do it with a single command. And you'd like the option, later, to put a login in front of a page without re-architecting.
This design uses two free services you may already touch — GitHub and Cloudflare — and glues them together with their native git integration. Nothing custom runs anywhere.
A dedicated organization holds the publishing repos and nothing sensitive — no private source you care about lives in it. Each “space” (a themed collection of pages) is its own repository. Repos are kept private: the source, history, and config are not browsable, while the deployed site is public. Free GitHub orgs allow unlimited private repos.
The org is also where you set a token policy: require owner approval for fine-grained access tokens. That turns any new publish credential into a visible, approved event.
Each repo is connected once to a Cloudflare Pages project through Cloudflare's GitHub app. From then on, every push auto-deploys — that's the “hook.” For plain HTML there is no build command and no framework; the repo root is served directly. (Cloudflare's current dashboard may instead create a Worker with static assets at *.workers.dev rather than a Pages project at *.pages.dev — it connects to git, auto-deploys on push, serves raw files, and supports custom domains and Access the same way; everything here applies to either. This page runs on the Worker variant.) Relevant free-tier facts:
When the domain's DNS is already managed by Cloudflare, attaching a custom subdomain to a Pages project creates the DNS record and provisions SSL automatically — no second console, no manual certificate. Each space gets a tidy subdomain, e.g. space.yourdomain.com. (If the domain lives elsewhere, you can still point a CNAME at the Pages project; you just lose the automatic, gating-ready integration.)
Publishing is just git add + git commit + git push. To make it a single word for an agent, a small “publish” skill is stored inside the repo (e.g. under .claude/skills/). Because it travels with the repo, any machine that clones the repo gets the same publish behavior — no global setup. The skill typically: shows what changed and the exact live URL, asks for a yes/no confirmation, refuses if there is no index.html, and prints the URL after pushing.
The machine that publishes holds a fine-grained personal access token scoped to exactly one repository, with permission limited to Contents: Read & Write, and an expiry (e.g. 90 days). It is stored in the operating system's credential store (Keychain on macOS, Credential Manager on Windows, a credential helper on Linux) — never in a file in the repo. If that machine is ever compromised, the blast radius is “write to one public-output repo, expiring soon,” with no path to anything else.
Because the domain is already proxied through Cloudflare, you can put Cloudflare Access (Zero Trust, free up to 50 users) in front of any page or subdomain. It gates by email one-time-PIN or single sign-on (Google/Microsoft/etc.), per person, and is toggled in the dashboard — the static HTML never changes. Taking a page down or restricting it is a policy flip, not a redeploy.
author's machine GitHub (private org repo) Cloudflare
┌──────────────────┐ git push ┌───────────────────────┐ webhook ┌──────────────────────┐
│ edit index.html │ ───────────────▶│ repo: your-space │ ──────────▶│ Pages project build │
│ say "publish" │ (scoped token) │ main branch │ │ (no build; copy root)│
│ → commit + push │ └───────────────────────┘ └──────────┬───────────┘
└──────────────────┘ │ deploy (atomic)
▼
public, with auto-SSL: https://space.yourdomain.com/
(optionally gated by Cloudflare Access)
Total moving parts you run: zero. GitHub stores; Cloudflare builds and serves; the token only ever authorizes the push.
One repo per space. The root index.html is a landing page; each page/app/demo is its own folder with its own index.html, which maps to a clean path URL.
your-space/
├─ index.html → https://space.yourdomain.com/
├─ first-thing/
│ └─ index.html → https://space.yourdomain.com/first-thing/
├─ second-thing/
│ └─ index.html → https://space.yourdomain.com/second-thing/
├─ CLAUDE.md guidance for the agent working in this repo
├─ README.md
└─ .claude/
└─ skills/
└─ publish/
└─ SKILL.md the one-word "publish" skill (travels with the repo)
Generic steps. Replace your-org, your-space, and yourdomain.com with your own. An agent can run the gh/git parts; the Cloudflare and token steps are dashboard actions.
Note: Cloudflare's “Create” flow may produce either a Pages project (*.pages.dev) or a Worker with static assets (*.workers.dev). Both work identically here — the only difference is where you attach the custom domain in step 6: a Pages project uses its Custom domains tab; a Worker uses Settings → Domains & Routes.
Make a new organization (Free plan). Keep anything private/sensitive out of it. In the org's settings, enable fine-grained personal access tokens and require approval for them.
gh repo create your-org/your-space --private --add-readme
# add an index.html landing page + one folder per page, each with its own index.html
Put a .claude/skills/publish/SKILL.md in the repo that runs: show diff → confirm with the live URL → git add -A && git commit && git push. It travels with the repo, so every clone can publish the same way.
Add yourdomain.com to your Cloudflare account and switch its nameservers to Cloudflare (one-time). This is what makes subdomains + SSL + gating automatic.
Cloudflare dashboard → Workers & Pages → Create → Pages → Connect to Git → authorize the org → select the repo. Settings: Framework preset: None, Build command: empty, Build output directory: /. Deploy. You immediately get a your-space.pages.dev URL (the name is globally shared, so it may need a suffix — it doesn't matter, the custom domain is what you share).
In the Pages project → Custom domains → add space.yourdomain.com. Because the zone is on Cloudflare, the DNS record and SSL certificate are created for you. Live within a few minutes.
Generate a fine-grained personal access token: resource owner = your-org, repository access = only your-space, permission = Contents: Read & Write, with an expiry. Approve it in the org. On the publishing machine, clone over HTTPS and let the OS credential store hold the token:
git clone https://github.com/your-org/your-space.git
cd your-space
# edit a page, then:
git add -A && git commit -m "Publish" && git push origin main
# first push prompts for username + token; the OS credential store remembers it
Cloudflare Zero Trust → Access → add an application for space.yourdomain.com → add a policy listing the allowed emails (or “anyone who signs in with Google,” etc.). No change to the HTML; toggle on/off anytime.
The security story is “smallest possible blast radius, per space”:
Why a fine-grained token rather than an SSH “deploy key”? A write deploy key is effectively admin on its repo, never expires, and isn't tied to a user. A fine-grained token is repo-scoped, permission-limited, and expires — better properties for an unattended or shared machine.
| Decision | Why |
|---|---|
| Cloudflare Pages over GitHub Pages | Works from private repos; unlimited bandwidth; instant rollback; no commercial-use limit; and native, free access-gating via Cloudflare Access. GitHub Pages on a free org requires public repos and has no built-in access control. |
| Domain's DNS on Cloudflare | Subdomain + SSL + gating become automatic and rework-free. A CNAME from another DNS host works for plain hosting but not for one-click Access. |
| Per-repo fine-grained token | Smallest blast radius; expiring; permission-limited. Avoids broad account tokens and never-expiring deploy keys. |
| One repo per space | Clean URL mapping, independent credentials, and simple mental model. Each page is just a folder. |
| Publish skill inside the repo | Portable and version-controlled; any clone publishes identically with no global setup. |
| Static HTML, no build | Nothing to maintain, instant deploys, works from any device including a phone or VR headset. |
| Item | Cost |
|---|---|
| GitHub org + private repos | $0 (Free plan) |
| Cloudflare Pages hosting | $0 (unlimited bandwidth) |
| Cloudflare Access gating | $0 up to 50 users |
| Domain | only the registration you already pay |
| Total | ~$0/month |
*.pages.dev name is globally shared and can't be renamed after creation — but it's only the origin URL; you share the custom domain.