Skip to content
Back to skills

Brewpage Publish

BSecurity

Publish HTML, markdown, text, any file, or a multi-file site to brewpage.app — free hosting with no sign-up. Paste text, share a file, upload a site, or host a temporary page and get an instant public URL to share a link. Asks namespace and password, returns the public URL. Triggers: publish, publish HTML, share link, share a link, share a file, upload to brewpage, host page, host a temporary page, host a website, free hosting, paste text, instant public URL, no sign-up, brewpage, publish sit...

  • 32 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 5, 2026
ai-agentsshellbashnodegitapisecurity

Works with

  • vscode
  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro scans all 3 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add kochetkov-ma/claude-brewcode --skill brewpage-publish --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Brewpage Publish?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Brewpage Publish
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/kochetkov-ma-brewpage-publish-claude-brewcode/badge)](https://www.skillsdirectory.com/skills/kochetkov-ma-brewpage-publish-claude-brewcode)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: brewpage-publish
description: "Publish HTML, markdown, text, any file, or a multi-file site to brewpage.app — free hosting with no sign-up. Paste text, share a file, upload a site, or host a temporary page and get an instant public URL to share a link. Asks namespace and password, returns the public URL. Triggers: publish, publish HTML, share link, share a link, share a file, upload to brewpage, host page, host a temporary page, host a website, free hosting, paste text, instant public URL, no sign-up, brewpage, publish site, upload site, upload directory, deploy site, сделай публичную ссылку, опубликуй."
homepage: https://brewpage.app
user-invocable: true
---

# brewpage-publish

Publish content to **brewpage.app** — free instant hosting for HTML pages, files, and multi-file sites. No sign-up required.

## Workflow

### Step 1: Parse Arguments

Extract from the arguments string:
- `--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) |
| Anything else | HTML | `POST /api/html` (format=markdown) |

Mode rule: directory/ZIP → SITE. Single file → FILE. Everything else → HTML (markdown). `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: char count.

### Step 3: Show Pre-Publish Stats

For HTML/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 `.html` file at all → **FAIL** with an explicit error: "No `.html` found — 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/`, or `public/`). 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

Ask the user:

```
Namespace controls discovery and appears in publication addresses on brewpage.app. Eligible Promotion publications may be indexed; Dedicated subdomain hosts use noindex.

Options:
1) public — eligible gallery discovery (default)
2) {auto-suggested 6-8 char slug}
3) Enter custom namespace
4) Skip → use public

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`)
- Site → site title or directory name (e.g. `portfolio`, `docs-site`)
- 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:
- `1`, `4`, or empty → `public`
- `2` → suggested slug
- `3` or any other string → use as-is

### Step 5: Ask Password

Ask the user:

```
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 — run with the shell tool:
```bash
LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c6 2>/dev/null
```

Resolution:
- `1`, `4`, or empty → no password
- `2` → use generated random password
- `3` or 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 model sees only the URL. Each block sets `PASS_H` first (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.

History file is workspace-relative: `./brewpage-history.md`.

**6a. Init history file (run once, before the publish block)** — run with the shell tool:
```bash
HISTORY_FILE="./brewpage-history.md"
if [ ! -f "$HISTORY_FILE" ]; then
  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** — run with the shell tool:
```bash
HISTORY_FILE="./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
```

**File** — run with the shell tool:
```bash
HISTORY_FILE="./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)** — run with the shell tool:
```bash
HISTORY_FILE="./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: OpenClaw/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)** — run with the shell tool:
```bash
HISTORY_FILE="./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: OpenClaw/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 ./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 ./brewpage-history.md

Share the server URL exactly as printed, including any trailing slash.
```

**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. **`format` is a query param**, not a body field — `/api/html` ignores any `format` key inside the JSON body and reads only `?format=` from the URL. Wrong location = server applies default `html` and stores your markdown as raw text.
- TTL default is `15` days. Namespace must match `^[a-z0-9-]{1,32}$`, default `public`.
- To **delete** a published page, find the owner token in `./brewpage-history.md` and use the delete command shown in that file's header.
- Use returned `ownerLink` and 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**, `PUT` the replacement bundle to its apex owner API with `X-Owner-Token`; retain the host and omit `X-Delivery-Mode` on ordinary PUT/update.
- Only NEW public non-sites allow owner-only `PATCH /api/{type}/{ns}/{id}/hosting` with `{"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 use `X-Password`; OLD password behavior remains unchanged.
- **Sites: directory is the primary input** — it is auto-zipped (the only thing `POST /api/sites` accepts), which seals relative paths. A pre-built `.zip` is 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: `--entry` override > `index.html` > first `.html` alphabetically.
- Preserve `.link` exactly: 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](https://brewpage.app)** | Free instant hosting — HTML, files, multi-file sites. No sign-up. |
| **[brewcode](https://github.com/kochetkov-ma/claude-brewcode)** | Plugin & skill suite — infinite tasks, code review, skills, hooks. |

Files in this skill

  • LICENSE1 KB
  • README.md4 KB
  • SKILL.md12.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.