Publishing AI Write-ups to a URL - Without Leaking the Client Ones | Brady StroudSkip to main content<br>All entries<br>Brady Stroud17 June 2026//ai, developer-tools, cloudflare, security, passkeys, productivity
Publishing AI Write-ups to a URL - Without Leaking the Client Ones
AI collaborationDrafted with AI support; ideas, experiences and opinions are mine.A lot of my work throws off short-lived write-ups - an investigation into a bug, a plan for a migration, a status report, a quick dashboard. Not documentation exactly; more the paper trail of figuring something out.<br>These days I have an AI agent turn each one into a single self-contained HTML file when the work's done, then publish it to a URL I can drop in a PR or send to someone. The "make it shareable" step is one command.<br>That worked great - right up until I noticed some of those write-ups were client work, sitting on a public URL.<br>This is the whole story: how I publish them, and how I rebuilt the publishing side to be private by default.<br>One command to publish<br>The starting point was a tiny CLI, publish-plan:<br>publish-plan ./q3-report.html<br># -> https://docs.stroud.dev/q3-reportIt uploads the HTML to a Cloudflare R2 bucket and prints a clean URL. That's it.<br>The reason it matters is that an agent can call it itself. I can end a task with "...and publish the report," and the agent hands me back a link. No copy-pasting markdown into a doc tool, no screenshots - a real rendered page at a real URL.<br>HTML plus a URL beats a wall of markdown in a chat window every time. It renders properly, it sticks around, and I can send it to someone who never opens a terminal.<br>The magic: it's a skill, not just a CLI<br>Here's the part that makes it feel automatic. publish-plan isn't only a command I run by hand - it's an agent skill . A short SKILL.md teaches the agent how to publish: the command, how the namespaces work, and the privacy rules.<br>So I never have to explain any of it. The agent finishes a job, decides the result is worth keeping, reads the skill, and runs the right command - dropping the write-up into the correct namespace with the correct visibility. "Publish this, and keep client work private" is knowledge the agent already has.<br>Which is exactly why the next part matters. If the agent is the one pulling the trigger, privacy can't be a polite suggestion in a prompt - it has to be enforced by the system itself.<br>The problem: everything was public<br>Under the hood it was the simplest thing that could work: an R2 bucket served on a custom domain. Upload an object, it's live at docs.stroud.dev/.<br>Two things bugged me:<br>I wanted the root to list everything I'd published. But R2 can't list objects from the CLI, and a bucket on a custom domain won't serve an index page - so the root just 404'd.
More importantly: anything I published was reachable by anyone with the URL. Not indexed by Google (I'd set noindex), but reachable. And some of those were client work.
"Reachable by URL" is fine for a plan I want to share. It is not fine for a client's work. I needed real access control, not security-by-obscurity.<br>A Worker as the gatekeeper<br>The fix was to stop serving the bucket directly and put a small Cloudflare Worker in front of it. Now every request runs through code that asks one question: is this person allowed to see this?<br>That one move turns dumb object storage into a real app:<br>the root can list the bucket and render a dashboard (the Worker can list, even though the CLI can't),
and nothing is served until it passes a visibility check.
Default answer: no . Private unless something says otherwise.<br>Privacy as config, sitting next to the work<br>I didn't want to think about privacy per file. I want to publish and have the right thing happen.<br>So privacy is declared once, in the repo. Each project - or a whole client folder - carries a docs.json:<br>// docs.json<br>{ "path": "client-work", "visibility": "private" }When I publish, the CLI walks up from the current directory, finds the nearest docs.json, and uses it. A client folder is marked private; my personal stuff is public. The doc lands under that namespace with that visibility - automatically.<br>cd ~/dev/some-client-project<br>publish-plan ./findings.html audits/2026/q3<br># -> https://docs.stroud.dev/client-work/audits/2026/q3 (private)The first path segment is the namespace, and the namespace is the privacy boundary. Client work can't end up public by accident, because the folder it's published from already says private.<br>This is the other half of the skill: the skill knows to publish, and docs.json decides where and how private. Between them, the agent does the right thing without me in the loop.<br>Three levels<br>Every doc resolves to one of three:<br>private - only me
public - anyone
link - anyone holding a share link
The Worker resolves it in order: a valid share link → a per-doc override → the namespace policy → otherwise deny. "Otherwise deny" is the important bit. A stray file with no policy is private, never public.<br>Signing in with a...