Publish content to brewpage.app — free instant hosting for HTML pages, JSON documents, files, and multi-file sites. No sign-up required.
Workflow
Step 1: Parse Arguments
Extract from $ARGUMENTS:
--ttl N→ TTL in days (default:15)--entry <filename>→ entry file for SITE uploads (default: auto-detect)--delivery-mode path|subdomain→ optional creation header; omitted means server defaults. Reject other values before upload.- Remaining text →
content_arg
Step 2: Detect Content Type
| Input | Type | API |
|---|---|---|
content_arg is a directory (test -d) |
SITE | POST /api/sites (dir auto-zipped — primary path) |
content_arg ends with .zip AND file exists (test -f) |
SITE | POST /api/sites (pre-built archive upload) |
content_arg is a file path AND file exists (test -f) |
FILE | POST /api/files (multipart) |
content_arg starts with { or [ |
JSON | POST /api/json |
| Anything else | HTML | POST /api/html (format=markdown) |
Mode rule: directory/ZIP → SITE. Single file → FILE. POST /api/sites accepts ONLY a multipart archive=@file.zip — there is no raw-folder upload, so a directory is auto-zipped on the fly (the robust default; archive sealing keeps relative paths intact). Stats per type — SITE (dir): HTML count, total size, entry file. SITE (ZIP): file size, entry override. FILE: size + MIME via file --mime-type -b. TEXT/JSON: char count.
Step 3: Show Pre-Publish Stats
For HTML/JSON/FILE:
Content: <type description> · <size> · <api endpoint>
TTL: <N> days
For SITE: detect entry file using priority: 1) --entry flag, 2) index.html exists, 3) first .html file alphabetically.
Built-static guard (run BEFORE zipping). Publish BUILT output, never project sources:
- If the directory contains no
.htmlfile at all → FAIL with an explicit error: "No.htmlfound — build the site first, then point at the build output directory." Do not guess an entry. - If the directory looks like un-built sources (has
package.json+src/but no top-level.html) → warn and ask the user to point at the build output instead (dist/,build/,out/,_site/, orpublic/). Do not zip the source tree.
Content: site · <N> files · <total_size> · POST /api/sites
Entry: <entry_file>
TTL: <N> days
Step 4: Ask Namespace
Use AskUserQuestion:
Namespace controls discovery and appears in publication addresses on brewpage.app.
By default publishing is PRIVATE (unlisted): not in the public gallery and not indexed by search engines. The link is not secret, though — anyone who has it can open it (use a password to restrict access).
Choose `public` for eligible gallery discovery. Eligible Promotion publications may be indexed; Dedicated subdomain hosts use noindex.
Options:
1) public — eligible gallery discovery; eligible Promotion publications may be indexed
2) {auto-suggested 6-8 char slug} — private, link-only (default)
3) Enter custom namespace
4) Skip → use suggested slug (private)
Reply with a number or your custom namespace (1-32 lowercase letters, digits or hyphens).
Auto-suggest: generate a meaningful short slug (3-16 chars, lowercase alphanumeric + hyphens) from content context:
- File → topic/purpose of the file (e.g.
api-docs,login-page,report-q2) - Text/HTML → main subject or title (e.g.
pricing,team-intro,changelog) - JSON → data type or schema name (e.g.
user-config,metrics) - Fallback → project name or directory name if content is ambiguous Never use random strings or truncated filenames — the slug should be human-readable and describe what's being published.
Resolution:
2,4, or empty → suggested slug1→public3or any other string → use as-is
Step 5: Ask Password
Use AskUserQuestion:
Password protection (if set, page is hidden from gallery):
Options:
1) No password (default)
2) Random: {generated 6-char password, e.g. "kx7p2m"}
3) Enter custom password (min 4 chars)
4) Skip → no password
Reply with a number or your custom password.
Generate random password EXECUTE using Bash tool:
LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c6 2>/dev/null
Resolution:
1,4, or empty → no password2→ use generated random password3or custom text → use as-is
Step 6: Publish and Save Token (secure)
NEW sites and non-public namespaces require Dedicated subdomain (subdomain); explicit path is rejected.
NEW public non-sites default to Promotion (path) and may select subdomain, including password-protected uploads.
Set DELIVERY_MODE to the validated flag value or empty when omitted; send only X-Delivery-Mode, never authored-body fields.
Deduplication may return an OLD or NEW winner: retain its identity, cohort, mode and exact server link, even when the requested mode differs.
Missing/null OLD routing metadata means Existing link; never infer mode from its URL.
SECURITY: The ownerToken MUST NEVER appear in conversation output. The bash blocks below handle curl + token parsing + history save atomically; the LLM sees only the URL. Each block sets
PASS_Hfirst (empty array when no password) and uses"${PASS_H[@]}"quoted — passwords are never string-interpolated into the command. The site-dir zip excludes (.git/,.env*, etc.) are also a secret-leak safeguard — they keep credentials and VCS data out of the published archive.
6a. Init history file (run once, before the publish block) — EXECUTE using Bash tool:
HISTORY_FILE=".claude/brewpage-history.md"
if [ ! -f "$HISTORY_FILE" ]; then
mkdir -p "$(dirname "$HISTORY_FILE")"
cat > "$HISTORY_FILE" <<'HEADER'
# brewpage.app — Published Pages
> PRIVATE FILE — keep this out of version control and never share it.
> Owner tokens allow delete and in-place republish (html/json/kv/sites all support PUT).
> Delete html/json/kv: `curl -s -X DELETE "https://brewpage.app/api/{ns}/{id}" -H "X-Owner-Token: TOKEN"`
> Delete site: `curl -s -X DELETE "https://brewpage.app/api/sites/{ns}/{id}" -H "X-Owner-Token: TOKEN"`
> Update site (keep same URL): `PUT /api/sites/{ns}/{id}` with `X-Owner-Token: TOKEN` + the new bundle — the uploaded bundle fully replaces the file set (adds new files, removes absent ones, overwrites matching). The link never changes.
| Date | URL | Owner Token | TTL | Type | Owner API | Identity |
|------|-----|-------------|-----|------|-----------|----------|
HEADER
fi
Then run ONE of the following publish blocks based on detected type. Each assumes HISTORY_FILE already exists from 6a.
HTML/Markdown text — EXECUTE using Bash tool:
HISTORY_FILE=".claude/brewpage-history.md"
CONTENT=$(cat <<'BREWPAGE_EOF'
{content}
BREWPAGE_EOF
)
PAYLOAD=$(jq -n --arg c "$CONTENT" '{content: $c}')
DELIVERY_H=()
case "${DELIVERY_MODE:-}" in
'') : ;;
path|subdomain) DELIVERY_H=(-H "X-Delivery-Mode: $DELIVERY_MODE") ;;
*) echo "FAILED: delivery mode must be path or subdomain"; exit 1 ;;
esac
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/html?ns={ns}&ttl={days}&format=markdown" \
-H "Content-Type: application/json" \
"${PASS_H[@]}" "${DELIVERY_H[@]}" \
-d "$PAYLOAD")
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
OWNER_API=$(echo "$RESPONSE" | jq -r '.ownerLink // empty')
IDENTITY=$(echo "$RESPONSE" | jq -c '{id, namespace, type, routingCohort, deliveryMode, modeLocked, hostingVersion, managementLink}')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | html | $OWNER_API | \`$IDENTITY\` |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: publish rejected (no .link in response)"
fi
JSON — EXECUTE using Bash tool:
HISTORY_FILE=".claude/brewpage-history.md"
DELIVERY_H=()
case "${DELIVERY_MODE:-}" in
'') : ;;
path|subdomain) DELIVERY_H=(-H "X-Delivery-Mode: $DELIVERY_MODE") ;;
*) echo "FAILED: delivery mode must be path or subdomain"; exit 1 ;;
esac
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/json?ns={ns}&ttl={days}" \
-H "Content-Type: application/json" \
"${PASS_H[@]}" "${DELIVERY_H[@]}" \
-d '{original_json}')
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
OWNER_API=$(echo "$RESPONSE" | jq -r '.ownerLink // empty')
IDENTITY=$(echo "$RESPONSE" | jq -c '{id, namespace, type, routingCohort, deliveryMode, modeLocked, hostingVersion, managementLink}')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | json | $OWNER_API | \`$IDENTITY\` |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: publish rejected (no .link in response)"
fi
File — EXECUTE using Bash tool:
HISTORY_FILE=".claude/brewpage-history.md"
DELIVERY_H=()
case "${DELIVERY_MODE:-}" in
'') : ;;
path|subdomain) DELIVERY_H=(-H "X-Delivery-Mode: $DELIVERY_MODE") ;;
*) echo "FAILED: delivery mode must be path or subdomain"; exit 1 ;;
esac
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/files?ns={ns}&ttl={days}" \
"${PASS_H[@]}" "${DELIVERY_H[@]}" \
-F "file=@/absolute/path/to/file")
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
OWNER_API=$(echo "$RESPONSE" | jq -r '.ownerLink // empty')
IDENTITY=$(echo "$RESPONSE" | jq -c '{id, namespace, type, routingCohort, deliveryMode, modeLocked, hostingVersion, managementLink}')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | file | $OWNER_API | \`$IDENTITY\` |" >> "$HISTORY_FILE"
echo "OK $URL"
else
echo "FAILED: publish rejected (no .link in response)"
fi
Site (directory) — EXECUTE using Bash tool:
HISTORY_FILE=".claude/brewpage-history.md"
DELIVERY_H=()
case "${DELIVERY_MODE:-}" in
'') : ;;
path|subdomain) DELIVERY_H=(-H "X-Delivery-Mode: $DELIVERY_MODE") ;;
*) echo "FAILED: delivery mode must be path or subdomain"; exit 1 ;;
esac
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
TMPZIP=$(mktemp /tmp/brewpage-site-XXXXXX.zip) || { echo "FAILED: cannot allocate site archive"; exit 1; }
rm -f "$TMPZIP" || { echo "FAILED: cannot prepare site archive"; exit 1; }
# Exclude VCS, secrets, deps, editor + OS junk and sourcemaps — publish only built static assets.
if ! (cd "{directory_path}" && zip -r "$TMPZIP" . -x '.git/*' '*/.git/*' '.env' '.env.*' '*/.env' '*/.env.*' 'node_modules/*' '*/node_modules/*' '.DS_Store' '*/.DS_Store' 'Thumbs.db' '.idea/*' '*/.idea/*' '.vscode/*' '*/.vscode/*' '.cache/*' '*/.cache/*' '*.map' '*.log') ||
[ ! -s "$TMPZIP" ] || ! unzip -tq "$TMPZIP" >/dev/null; then
rm -f "$TMPZIP"
echo "FAILED: site archive creation or verification failed"
exit 1
fi
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/sites?ns={ns}&ttl={days}&entry={entry}" \
-H "User-Agent: ClaudeCode/1.0" \
"${PASS_H[@]}" "${DELIVERY_H[@]}" \
-F "archive=@$TMPZIP")
rm -f "$TMPZIP"
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
OWNER_API=$(echo "$RESPONSE" | jq -r '.ownerLink // empty')
IDENTITY=$(echo "$RESPONSE" | jq -c '{id, namespace, type, routingCohort, deliveryMode, modeLocked, hostingVersion, managementLink}')
FCOUNT=$(echo "$RESPONSE" | jq -r '.fileCount // "?"')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | site ($FCOUNT files) | $OWNER_API | \`$IDENTITY\` |" >> "$HISTORY_FILE"
echo "OK $URL | Files: $FCOUNT"
else
echo "FAILED: publish rejected (no .link in response)"
fi
Site (ZIP file) — EXECUTE using Bash tool:
HISTORY_FILE=".claude/brewpage-history.md"
DELIVERY_H=()
case "${DELIVERY_MODE:-}" in
'') : ;;
path|subdomain) DELIVERY_H=(-H "X-Delivery-Mode: $DELIVERY_MODE") ;;
*) echo "FAILED: delivery mode must be path or subdomain"; exit 1 ;;
esac
PASS_H=()
[ -n "$PASSWORD" ] && PASS_H=(-H "X-Password: $PASSWORD")
RESPONSE=$(curl -s -X POST "https://brewpage.app/api/sites?ns={ns}&ttl={days}&entry={entry}" \
-H "User-Agent: ClaudeCode/1.0" \
"${PASS_H[@]}" "${DELIVERY_H[@]}" \
-F "archive=@{zip_file_path}")
URL=$(echo "$RESPONSE" | jq -r '.link // empty')
TOKEN=$(echo "$RESPONSE" | jq -r '.ownerToken // empty')
OWNER_API=$(echo "$RESPONSE" | jq -r '.ownerLink // empty')
IDENTITY=$(echo "$RESPONSE" | jq -c '{id, namespace, type, routingCohort, deliveryMode, modeLocked, hostingVersion, managementLink}')
FCOUNT=$(echo "$RESPONSE" | jq -r '.fileCount // "?"')
if [ -n "$URL" ]; then
[ -n "$TOKEN" ] && echo "| $(date '+%Y-%m-%d %H:%M') | [$URL]($URL) | \`$TOKEN\` | {ttl}d | site ($FCOUNT files) | $OWNER_API | \`$IDENTITY\` |" >> "$HISTORY_FILE"
echo "OK $URL | Files: $FCOUNT"
else
echo "FAILED: publish rejected (no .link in response)"
fi
Step 7: Output Result
Success (bash printed OK {url}):
Published: {url from bash output}
Owner token saved to .claude/brewpage-history.md
Success for SITE (bash printed OK {url} | Files: {count}):
Published site: {url from bash output}
Entry: {entry_file} | Files: {count}
Owner token saved to .claude/brewpage-history.md
Share the server URL exactly as printed, including any trailing slash.
For a private (non-public) namespace, append one short line after the link (skip if the reply must stay ultra-brief): Unlisted link — anyone who has it can open it, but it's not in the gallery or search. Want it discoverable? Publish to public.
NEVER print the ownerToken in conversation. The token lives only in the history file.
Error (bash printed FAILED: ...):
Publish failed.
Notes
- Always use absolute file paths with curl
-F "file=@...". - Use
jq -n --arg c "$CONTENT" '{content: $c}'to safely encode text content.formatis a query param, not a body field —/api/htmlignores anyformatkey inside the JSON body and reads only?format=from the URL. Wrong location = server applies defaulthtmland stores your markdown as raw text. - TTL default is
15days. Namespace must match^[a-z0-9-]{1,32}$, default: suggested (private) namespace. - To delete a published page, find the owner token in
.claude/brewpage-history.mdand use the delete command shown in that file's header. - Use returned
ownerLinkand stored identity metadata for owner API operations; never parse NEW root hosts for namespace/ID. OLD history without metadata retains its existing API identifiers. - To update a published site,
PUTthe replacement bundle to its apex owner API withX-Owner-Token; retain the host and omitX-Delivery-Modeon ordinary PUT/update. - Only NEW public non-sites allow owner-only
PATCH /api/{type}/{ns}/{id}/hostingwith{"deliveryMode":"subdomain","expectedVersion":1}from current identity metadata. OLD, sites and non-public identities have no mode choice. Use the returned URL/version afterward. - NEW password-protected links stay clean: never generate or append
?p=. API requests still useX-Password; OLD password behavior remains unchanged. - Sites: directory is the primary input — it is auto-zipped (the only thing
POST /api/sitesaccepts), which seals relative paths. A pre-built.zipis the alternative input, uploaded as-is. Always publish BUILT output (dist/,build/,out/,_site/,public/), never project sources. - The auto-zip excludes
.git/,.env/.env.*,node_modules/, editor/OS junk, sourcemaps and logs — a deliberate secret-leak safeguard so credentials and VCS history never reach the public archive. - Entry file detection:
--entryoverride >index.html> first.htmlalphabetically. - Preserve
.linkexactly: NEW subdomain roots retain/; OLD links retain their existing form. Never reconstruct a host from IDs. - Verify NEW native sites at the exact returned URL and asset paths; a browser proves rendering. OLD sites may still use the landing-page redirect; verify those in a browser or fetch their explicit entry asset.
Powered by
| brewpage.app | Free instant hosting — HTML, JSON, files, sites, KV. No sign-up. |
| brewcode | Claude Code plugin suite — infinite tasks, code review, skills, hooks. |