publish — share content, get link
Caution
Your content stays local until you explicitly run this skill. The skill asks for namespace and password interactively — nothing is uploaded until you confirm both prompts. Owner tokens (needed for deletion) are stored only in .claude/brewpage-history.md and never printed to conversation.
Password protection did not work before v5.0.0
Every upload block referenced a PASS_H array built from a $PASSWORD variable that nothing ever assigned. The password was resolved in conversation and each Bash call is a fresh shell, so the header expanded to nothing: the page was published unprotected while the skill still reported a password. Nothing surfaced the mismatch except opening the link logged out.
The blocks now carry a {password_header} placeholder the model substitutes before running, and the skill states the rule as mandatory: substitute -H "X-Password: <pass>" or delete the line, and never report a password you did not substitute. If you published a protected page with an earlier version, treat it as public — delete it with its owner token and publish again.
Site publishing produced a fake success — fixed
Publishing a directory as a site built its archive with mktemp, which leaves a 0-byte placeholder file. Info-ZIP reads that as a corrupt target and exits with status 3 — an exit code the old block never checked. The empty archive was uploaded anyway and reported as a success.
scripts/publish.mjs now owns the whole archive path: it starts from no file at all, checks zip’s exit status, verifies the archive with unzip -t, and requires a non-zero size before anything is handed to curl. Any failure prints FAILED: and stops — nothing is uploaded. The same script also stopped sweeping secrets into the bundle: .env and .git/config used to ride along inside published sites; the archive is now built from an allowlist of web-asset extensions, so dotfiles, .env, .git/, node_modules/ and unknown extensions never make it in.
Tip
brewpage.app is free instant hosting: HTML pages, JSON documents, files, and multi-file sites. Default TTL is 15 days — override with --ttl N. For the public gallery, visit brewpage.app.
Companion tools
brewpage-mcp
Official MCP server for BrewPage Sites API (v1.2.0, multi-file support). Install: claude mcp add brewpage npx -- -y brewpage-mcp
OpenAPI Spec
Full API contract, Scalar UI. Use when building custom integrations or calling the API directly.
REST API
Direct REST access — no client needed. Base URL: https://brewpage.app/api
Quick reference
| Field | Value |
|---|---|
| Command | /brewdoc:publish <text|file_path|directory_path|zip_path> [--ttl N] [--entry filename] |
| Arguments | Inline text, a file path, a directory, or a .zip archive. --ttl N = days (default 15), --entry filename = entry file for site uploads |
| Model | haiku |
| Invocation | user-only (disable-model-invocation: true) |
| Tools | Read, Bash, AskUserQuestion, Glob |
| Requires | jq on PATH; zip/unzip/node as well for directory or zip site uploads |
When to use
- Share a report instantly —
/brewdoc:publish report.md→ public link in seconds - Host a JSON dataset —
/brewdoc:publish '{"status":"ok","count":42}'→ browseable JSON page - Upload any file —
/brewdoc:publish screenshot.png --ttl 1→ direct download link, expires in 1 day - Deploy a static site —
/brewdoc:publish ./dist/ --entry index.html→ full multi-file site with routing - Password-protect a page — set a password at the interactive prompt; page is hidden from gallery
Examples
# Publish inline text or markdown
/brewdoc:publish "Hello, world!"
/brewdoc:publish report.md
# Publish a JSON literal
/brewdoc:publish '{"status": "ok", "count": 42}'
# Publish a file (any MIME type)
/brewdoc:publish screenshot.png --ttl 1
# Deploy a multi-file site from a directory
/brewdoc:publish ./dist/ --entry index.html
# Upload a pre-zipped site archive
/brewdoc:publish site.zip --ttl 14
Modes
| Input | Detected type | Endpoint |
|---|---|---|
| Plain text or markdown string | HTML | POST /api/html?format=markdown |
Starts with { or [ | JSON | POST /api/json |
| Existing local file path | FILE | POST /api/files (multipart) |
Directory or .zip file | SITE | POST /api/sites (archive built/verified by publish.mjs, then uploaded) |
SITE is checked first — directories and .zip files would otherwise match the FILE branch.
Entry file detection for sites: --entry flag → index.html if present → first .html alphabetically.
Flow
- Parse arguments
Extracts
—ttl N(default: 15 days),—entry filename(site mode), and the content argument. For a directory, counts HTML files and detects the entry file. - Detect content type
Classifies the input as SITE, FILE, JSON, or HTML — in that order. Computes size and shows a pre-publish summary before any interaction.
- Ask namespace
Interactive prompt offers:
public(gallery-visible), an auto-suggested meaningful slug, or a custom value (3–32 alphanumeric chars + hyphens). The slug becomes the URL prefix:brewpage.app/{namespace}/{id}. - Ask password
Choose: no password, a generated 6-char random password, or your own (min 4 chars). Password-protected pages are hidden from the gallery. The answer is then substituted into the
{password_header}placeholder of the upload block — substituting it is a mandatory step, not an optional one. - Upload via API
Every block first gates on
command -v jqand aborts withFAILED: jq requiredrather than half-publishing. For a directory site,scripts/publish.mjs packbuilds and verifies the archive before anything is POSTed; for a supplied.zip,publish.mjs inspectchecks it and flags unexpected entries for oneAskUserQuestionconfirmation instead of uploading silently. The owner token returned by the API is never printed — only appended to.claude/brewpage-history.md. - Return result
Prints the public URL (and file count for sites). On error, prints
Publish failed.and nothing else — the failure branch deliberately withholds the response body, which can carry anownerToken.
Internals
Security — owner token: The ownerToken field from the API response is captured inside the Bash block and written directly to .claude/brewpage-history.md. The LLM only sees OK {url} or FAILED: ... — never the token value, and never the raw response body on failure.
Security — password header: {password_header} is a placeholder in the same class as {content}, {ns} and {days} — the model rewrites it before the block runs. There is no $PASSWORD shell variable and never was one that got assigned; that is exactly what made the pre-5.0.0 version publish unprotected pages. Substituting is mandatory: -H "X-Password: <pass>" when a password was chosen, the whole line deleted when it was not.
History file format — each upload block creates .claude/brewpage-history.md with this header if it does not exist yet, then appends one row:
| Date | URL | Owner Token | TTL | Type |
|------|-----|-------------|-----|------|
| 2026-04-17 12:00 | https://brewpage.app/my-ns/abc123 | `tok_...` | 5d | html |Delete a published page:
curl -s -X DELETE "https://brewpage.app/api/{ns}/{id}" \
-H "X-Owner-Token: TOKEN"For sites use /api/sites/{ns}/{id}.
Update a published site (keep the same URL):
curl -s -X PUT "https://brewpage.app/api/sites/{ns}/{id}" \
-H "X-Owner-Token: TOKEN" \
-F "archive=@/absolute/path/to/new-bundle.zip"To update a published site, PUT the new bundle to the same site URL with your owner token — the uploaded bundle fully replaces the file set (adds new files, removes absent ones, overwrites matching). The link never changes; no DELETE-then-POST needed.
Namespace rules: lowercase alphanumeric + hyphens, 3–32 chars. Auto-suggestion is content-derived (api-docs, report-q2) — never a random string or truncated filename.
Site uploads: directory → scripts/publish.mjs pack --dir <dir> --out <archive.zip> builds the archive, removing any stale target first (a mktemp-created 0-byte file is what made zip exit 3 before this fix), then checks zip’s exit status, verifies the result with unzip -t, and requires a non-zero size — any failure deletes the archive and exits without touching curl. The archive is POSTed as archive=@$TMPZIP and the temp file removed after. User-Agent: ClaudeCode/1.0 header included.
Archive contents — allowlist, not denylist: only known web-asset extensions (.html .css .js .json .svg .png .woff2 .pdf .md …) are packed; dotfiles, .env, .git/, node_modules/ and symlinks are dropped before zip ever runs. A supplied .zip is inspected rather than rewritten — an entry matching a sensitive pattern (.env, id_rsa, secret, credential, password, token, .pem/.key/.p12/.crt/.map) blocks the upload until you confirm.
Tests: bash brewdoc/skills/publish/tests/run.sh — 108 checks across 3 suites (archive build, zip inspection, and the SKILL.md prompt itself), no network, no real upload.
md-to-pdf
Convert markdown to PDF before publishing — combine with publish for shareable PDFs.
GitHub source
Source code, SKILL.md, and API integration details.
Brewdoc overview
All brewdoc skills — docsync, memory, my-claude, and more.
Updating plugins
/brewtools:plugin-update to check and update the brewcode plugin suite in one command.
See the FAQ for details.