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

FieldValue
Command/brewdoc:publish <text|file_path|directory_path|zip_path> [--ttl N] [--entry filename]
ArgumentsInline text, a file path, a directory, or a .zip archive. --ttl N = days (default 15), --entry filename = entry file for site uploads
Modelhaiku
Invocationuser-only (disable-model-invocation: true)
ToolsRead, Bash, AskUserQuestion, Glob
Requiresjq 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

InputDetected typeEndpoint
Plain text or markdown stringHTMLPOST /api/html?format=markdown
Starts with { or [JSONPOST /api/json
Existing local file pathFILEPOST /api/files (multipart)
Directory or .zip fileSITEPOST /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

  1. 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.

  2. 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.

  3. 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}.

  4. 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.

  5. Upload via API

    Every block first gates on command -v jq and aborts with FAILED: jq required rather than half-publishing. For a directory site, scripts/publish.mjs pack builds and verifies the archive before anything is POSTed; for a supplied .zip, publish.mjs inspect checks it and flags unexpected entries for one AskUserQuestion confirmation instead of uploading silently. The owner token returned by the API is never printed — only appended to .claude/brewpage-history.md.

  6. 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 an ownerToken.

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

Use /brewtools:plugin-update to check and update the brewcode plugin suite in one command. See the FAQ for details.