handbill
Hand someone a page. One command turns a self-contained HTML file — or a markdown file — into an unguessable, immutable link on a domain you own.
$ handbill plan.htmlhttps://a3f9c1d4e2b8.yourdomain.dev- Yours. A Cloudflare Worker and an R2 bucket you deploy once. Your account, your domain, your token, free tier. Nothing phones home.
- Immutable. The URL is the content hash. A link never changes under its reader; a new version is a new link.
- Agent-native. Ships an installable skill so coding agents end a task with a link instead of a file.
Docs: handbill.dev/docs.
Install
npm i -g handbillNode ≥ 22. Two dependencies: effect and marked. Every night main is published as handbill@nightly if you want what has merged but not shipped. Then point it at a deployment — yours (below) — with ~/.config/handbill/config.json:
{ "endpoint": "https://api.yourdomain.dev", "token": "…" }or HANDBILL_ENDPOINT and HANDBILL_TOKEN in the environment.
Self-host in five steps
From a clone of this repository, after bun install, in apps/worker (wrangler reads the wrangler.jsonc there):
- In
wrangler.jsonc, change the three lines markedEDITto your zone. - Add two proxied DNS records on the zone:
apiand*. bunx wrangler r2 bucket create handbillopenssl rand -hex 32 | bunx wrangler secret put PUBLISH_TOKENbunx wrangler deploy
The full walkthrough — token scopes, verification curls, limits, troubleshooting — is in docs/SELF-HOSTING.md. Ten minutes the first time.
Use
| Command | What it does |
|---|---|
handbill plan.html | Publish. Prints exactly one line: the URL. |
handbill notes.md | Render markdown to a self-contained page, publish that. |
cat plan.html | handbill - | Publish from stdin. Add --markdown to render it. |
handbill plan.html --json | { "hash", "url", "created" } instead. Every command takes --json. |
handbill list | What you have published, newest first: date, URL, title. |
handbill remove <url|hash> | Unpublish. Idempotent. |
handbill alias plan <url|hash> | Point a name at a page: plan.yourdomain.dev serves it. Opt-in; alias list, alias remove. |
handbill doctor | Config, token, endpoint, token accepted, wildcard certificate — each with a one-line fix. |
handbill completions zsh | Shell completions (bash, zsh, fish). |
Errors are one sentence on stderr and a non-zero exit; stdout is only ever the result. --open on handbill <file> and handbill alias opens the URL in your browser after printing it.
How it works
hash = hex(sha256(bytes))[0:12] — the first 12 hex characters of the digest. The client computes it to form the URL; the server recomputes it and rejects a mismatch. Publishing the same bytes twice returns the same URL and stores nothing new.
The page is served from https://<hash>.<zone> — its own origin — with text/html; charset=utf-8, X-Robots-Tag: noindex, nofollow, and Cache-Control: public, max-age=31536000, immutable. Every path on that hostname serves the same document. The API lives at api.<zone> under /v1 and needs the bearer token for everything except /v1/health, the generated spec at /v1/openapi.json, and the reference that renders it at /docs.
Optionally, a living name: handbill alias plan <hash> makes plan.<zone> serve that page (cached for a minute, not a year) until you point the name elsewhere, while every hash link ever handed out keeps working. Names are guessable by construction, so the feature is off until you bind a KV namespace — docs/SELF-HOSTING.md has the trade-off and the setup.
One self-contained HTML file per link, 5 MB by default. No multi-file sites, no assets, no transforms on the server — a .md file is rendered to a page by the CLI, with a built-in light/dark stylesheet, before anything is uploaded.
The link is public, the token is yours
Anyone holding a link can read the page; the link is unguessable and not indexed, but it is not private and not encrypted. Publishing and unpublishing need the token, which only you hold. Details in SECURITY.md.
Agents
skills/handbill/SKILL.md teaches a coding agent when and how to publish with handbill: one file, one URL, what never to publish, how to list, name and unpublish, and what to do when it fails. Install it by symlinking the directory into the agent’s skills folder (~/.claude/skills/ for Claude Code, ~/.agents/skills/ for Codex); it needs only handbill on PATH and a configured endpoint and token.
Development
bun installbun run typecheck && bun run lint && bun testpackages/contract the HttpApi — schemas, errors, endpoints; the single source of truthapps/worker Effect on Cloudflare Workers; deploy with wranglerapps/cli the npm package; bun build → dist/cli.jsapps/web handbill.dev: Astro + Starlight, this README and docs/ rendered; deployed from mainskills/handbill the agent skilldocs/ PRD, self-hosting, releasingEffect 4 end to end, pinned to an exact release candidate. Conventions and invariants are in AGENTS.md; the product definition, user stories, and roadmap in docs/2026-08-28-prd.html. Work is tracked in the issues and the project board.
Roadmap
- 0.1 — self-host kit: Worker, CLI, skill (released)
- 0.2 — markdown input rendered in the CLI, living names on KV with
aliascommands,--open, OpenAPI +/docs, the site at handbill.dev (all onmain); 0.2.0 still to release - 0.3 — hosted mode: the same Worker with accounts instead of a single token
- later — inline local assets at publish time, expiring pages, encrypted pages
License
MIT