Publish
Publishes text, Markdown, JSON, a file, a folder or a ZIP site to brewpage.app and returns a shar…
by kochetkov-ma · MIT
The skill
Publishes text, Markdown, JSON, a file, a folder or a ZIP site to brewpage.app and returns a shareable URL. Use when asked to publish or share content as a link. Triggers: publish, share link, host page, brewpage, опубликуй.
brewdoc:publish
Publishes HTML/Markdown, JSON, files and multi-file sites to brewpage.app (free, no sign-up) and returns the URL.
Prompt contract
$ARGUMENTS is one free-form RU/EN prompt; the content (text, file/dir/zip path) and the flags --ttl N,
--entry <filename>, --delivery-mode path|subdomain may sit anywhere in it.
- Mode: the content type of what is published — detected in Step 2, scored by the keyword table there.
- Scope: a sentence naming a file/dir resolves to that file/dir (e.g. "publish my report.md" ->
content_arg = report.md, resolved against cwd). Never guess, never silently fall back to an empty HTML page. Fired by Claude mid-task (Skilltool), itsargsis the prompt: the path, or the exact text to publish, never a summary of it. - Ask: a sentence with no path and no inline text/JSON has nothing to publish -> ONE
AskUserQuestionasking what to publish. Namespace and password -> the two dedicatedAskUserQuestioncalls in Steps 4-5; nothing else asks. Headless or unanswered -> the Step 4-5 defaults; nothing blocks.
Print once, before the first tool action, including filesystem probes and questions (labels literal,
values in the conversation language). Use unresolved for type/namespace/password until determined;
report resolved stats in Step 3.
PLAN — brewdoc:publish
INPUT: <arguments verbatim, or "(empty)">
MODE: <detected type — SITE|MARKDOWN|FILE|JSON|HTML> — <matched extension/shape | prose-resolved path>
SCOPE: namespace=<ns>; ttl=<days>; password=<set|none>; delivery-mode=<server-default|path|subdomain>
DO: <2-5 imperative bullets>
RESULT: <published URL>
Running commands
Every shell step is ONE command of the exact shape in the fences below: the single allowed-tools
rule pre-approves exactly that shape, so the skill never prompts, headless included.
- Fill each
'{…}'slot with the raw value and write every'inside it as'\''; nothing else is escaped. Never wrap a value in double quotes. An empty slot stays''(= omitted). - Never quote the script path, prefix or chain the command (
cd,VAR=…,;,&&,|,$(…)): any other shape falls back to the normal permission prompt. - Keep every command under 8,000 characters (longer ones are always refused); longer text goes
through
stage(Step 6). - The script prints one decisive line:
OK {url},OK {url} | Files: N,CONFIRM: …(exit 2, nothing uploaded) orFAILED: …. It removes its private run directory on every exit.
Workflow
Step 1: Parse Arguments
Extract from the prompt:
--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 = server default; reject other values before upload- Remaining text →
content_arg, per Prompt contract rules 2-3
Step 2: Detect Content Type
A content_arg that names a path -> probe it (read-only; prints TYPE:, the absolute PATH: to use
below, and the Step 3 stats) — EXECUTE using Bash tool:
bash ${CLAUDE_SKILL_DIR}/scripts/brewpage-run.sh probe '{path}'
| Input | Type | publish type + source |
API | EN keywords | RU keywords | Mutates? |
|---|---|---|---|---|---|---|
TYPE: site-dir |
SITE | site-dir + --path |
POST /api/sites (ZIP built from the dir) |
publish site, publish folder/directory | опубликуй сайт, опубликуй папку | yes |
TYPE: site-zip (.zip file) |
SITE | site-zip + --path |
POST /api/sites (archive upload) |
publish zip, publish archive | опубликуй архив, опубликуй zip | yes |
TYPE: markdown (.md/.markdown file) |
MARKDOWN | html + --path |
POST /api/html (format=markdown, content read from the file — renders styled, NOT a raw download) |
publish markdown, share this .md | опубликуй markdown, поделись .md | yes |
TYPE: file (any other file) |
FILE | file + --path |
POST /api/files (multipart) |
publish file, share this file | опубликуй файл, поделись файлом | yes |
No path (TYPE: none), content_arg starts with { or [ |
JSON | json + --text |
POST /api/json |
publish json, share this json | опубликуй json | yes |
| Anything else | HTML | html + --text |
POST /api/html (format=markdown) |
publish text, share link, publish this | опубликуй текст, поделись ссылкой | yes |
Step 3: Show Pre-Publish Stats
From the probe lines — SITE (dir): HTML, SIZE_KB, ENTRY. SITE (ZIP): BYTES, entry override. FILE and
MARKDOWN: BYTES + MIME. Text/JSON: char count.
For HTML/MARKDOWN/JSON/FILE:
Content: <type description> · <size> · <api endpoint>
TTL: <N> days
For SITE (entry: --entry > index.html > first .html alphabetically; ENTRY: none → fail with an explicit error, never guess):
Content: site · <N> files · <total_size> · POST /api/sites
Entry: <entry_file>
TTL: <N> days
These are source-tree estimates: report the ARCHIVE MANIFEST that Step 6 prints before upload; its
ENTRY: line is the entry scripts/publish.mjs resolved in the built archive.
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 public HTML, sites and files may permit indexing on either address; unlisted, protected or otherwise ineligible native responses retain `noindex, nofollow`. Indexing is not guaranteed.
Options:
1) public — eligible gallery discovery; eligible publications may permit indexing
2) {suggested 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).
Suggested slug: 3-16 chars of a-z0-9- named after the content (api-docs, report-q2,
pricing, user-config), else the project or directory name; never random strings or truncated filenames.
Resolution: 2, 4, empty, or no answer (headless) → suggested slug | 1 → public | 3 or 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) Use an existing local password file (min 4 characters)
3) Skip → no password
Reply with a number and, for option 2, the absolute local file path. Do not paste password contents.
Resolution: 1, 3, empty, or no answer → {password_file} = '' (no password; every password file,
stale ones included, is ignored). 2 + an existing local file path → {password_file} = that absolute
path: the script moves the bytes via private header files (never shell literals or curl argv) and rejects
control characters (header injection). Never Read, generate or print the password contents.
Step 6: Publish and Save Token (secure)
Delivery mode: 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, password-protected
included. Send it only as X-Delivery-Mode, never in authored JSON, text or multipart fields. A deduplicated
OLD or NEW winner may differ: keep its identity, cohort, mode and exact link. Missing/null OLD routing
metadata means Existing link; never infer mode from its address.
SECURITY: the ownerToken NEVER appears in conversation output: the script does curl, token parsing and history save in one run and prints only the URL; failures print no response body.
Every slot is re-validated by the script before anything is written or uploaded:
| Slot | Must match | Re-checked by |
|---|---|---|
{ns} |
^[a-z0-9-]{1,32}$ |
bp_validate |
{days} |
positive integer; '' = 15 |
bp_validate |
{entry} |
plain relative file name, no ..; '' = auto-detect; sites only |
bp_validate |
{delivery_mode} |
'', path or subdomain |
bp_begin |
{password_file} |
'' or an absolute path to an existing regular file |
bp_begin |
A value failing its pattern: re-ask; never clean it up or substitute it anyway. A slot error fails before
any run is touched, so staged text survives the retry. {type} comes from the Step 2 table.
SITE, MARKDOWN, FILE — {path} is the probe's PATH: — EXECUTE using Bash tool:
bash ${CLAUDE_SKILL_DIR}/scripts/brewpage-run.sh publish {type} --ns '{ns}' --ttl '{days}' --entry '{entry}' --delivery-mode '{delivery_mode}' --password-file '{password_file}' --path '{path}'
HTML text, JSON — {content} is the exact text or JSON, verbatim — EXECUTE using Bash tool:
bash ${CLAUDE_SKILL_DIR}/scripts/brewpage-run.sh publish {type} --ns '{ns}' --ttl '{days}' --entry '' --delivery-mode '{delivery_mode}' --password-file '{password_file}' --text '{content}'
Longer text or JSON (the command would pass 8,000 characters): split it at line ends into chunks
of at most 6,000 characters. The first stage call passes --run '' and prints RUN: brewpage.XXXXXX;
every later chunk and the final publish pass that id. The script rejoins chunks with one newline.
EXECUTE using Bash tool, once per chunk:
bash ${CLAUDE_SKILL_DIR}/scripts/brewpage-run.sh stage --run '{run_id}' --text '{chunk}'
Then EXECUTE using Bash tool:
bash ${CLAUDE_SKILL_DIR}/scripts/brewpage-run.sh publish {type} --ns '{ns}' --ttl '{days}' --entry '' --delivery-mode '{delivery_mode}' --password-file '{password_file}' --run '{run_id}'
Staged but not published (cancelled) — EXECUTE using Bash tool:
bash ${CLAUDE_SKILL_DIR}/scripts/brewpage-run.sh cleanup --run '{run_id}'
Step 7: Output Result
Success (the script printed OK {url}):
Published: {url from bash output}
Owner token saved to <project-root>/.claude/brewpage-history.md (mode 600, git-ignored)
Success for SITE (the script printed OK {url} | Files: {count}):
Published site: {url from bash output}
Entry: {entry_file} | Files: {count}
Owner token saved to <project-root>/.claude/brewpage-history.md (mode 600, git-ignored)
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 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.
Needs confirmation (the script printed CONFIRM: ..., exit 2; nothing uploaded): show the manifest
lines, name the flagged entries, ask ONE AskUserQuestion (publish anyway / cancel). Publish anyway ->
re-run the identical command with --confirmed appended. Cancel or no answer -> stop: the script already
removed its own run directory and never touches other runs, user source files or the shared tmp directory.
Error (the script printed FAILED: ...):
Publish failed: {the FAILED line, verbatim}
Notes
formatis a query param:/api/htmlreads only?format=from the URL; a bodyformatkey is ignored and the defaulthtmlstores markdown as raw text.- History:
<project-root>/.claude/brewpage-history.md(root:CLAUDE_PROJECT_DIR→ git toplevel → upward.git/.claudewalk (a.claudein HOME or in the profile parent, the dir whose.claudeIS the active config dir, is skipped; a.gitthere still counts) →PWD, viascripts/brewpage-lib.sh), mode600, added to the project.gitignore. Its header holds the delete command for the owner token. - Run dirs: private
brewpage.XXXXXXunder$TMPDIR(else/tmp), outside the protected.claude/; onlyscripts/brewpage-run.shcreates, fills and removes them, so the skill needs noWrite/Editgrant. - Permissions: model-invocable, so
allowed-toolsholds one scoped Bash rule that matches every step above, indefault,dontAskand headless runs alike (verified on CC 2.1.296). A script path containing spaces breaks the unquoted command shape. - History TTL = requested days; stored
createdAt/expiresAtare authoritative: a deduplicated winner keeps its original expiry, and ordinary PUT/update does not renew it. - Owner API calls use the returned
ownerLinkand stored identity metadata, never a NEW root hostname parsed for namespace/ID; OLD rows without metadata keep their existing API identifiers. - Update a site:
PUTthe new bundle to its apex owner API withX-Owner-Token; it replaces the file set and keeps the host. NoX-Delivery-Modeon ordinary PUT/update. - Hosting change, NEW public non-sites only (OLD, sites and non-public identities have no mode choice): owner-only
PATCH /api/{type}/{ns}/{id}/hostingwith{"deliveryMode":"subdomain","expectedVersion":1}from current identity metadata; then use the returned URL/version. - NEW password-protected links stay clean: never generate or append
?p=. API password requests useX-Password; OLD password behavior is unchanged. - SITE bundles are allowlisted:
packdrops dot-entries (.env,.git/,.DS_Store),node_modules/, symlinks and unknown types, and verifies the archive (unzip -t, name set) beforecurl, else deletes it and exits 1.inspectnever rewrites a supplied ZIP; it exits 2 when anything unexpected or sensitive is inside. - Keep
.linkexact (NEW subdomain roots keep/, OLD links their form); never reconstruct a host from IDs. Verify NEW native sites at the exact URL and asset paths in a browser; OLD sites may redirect via a landing page: check in a browser or fetch the explicit entry asset.
Use this skill
Connect your agent to the Skill Store MCP server and ask for it by ref: kochetkov-ma/publish-8122032125.
Details
- Publisher
- kochetkov-ma
- Category
- Software Development › DevOps & cloud
- License
- MIT
- Updated
- 2026-10-17
References
More from kochetkov-ma
- Agents Creates, improves, syncs Codex subagents.
- Convention Setup Installs project conventions and reversible loading guidance; extracts representative implementat…
- Rules Maintains project .codex/rules and the AGENTS.md rule index.
- Superreview Setup Generates a project-tailored deep-review skill: domain-expert routing + scope discipline (blast r…