blog-writer — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited blog-writer (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Channel-master skill for long-form blog content. Owns ideation → beats → draft → image → publish.
Architecture: beats-first (v0.5.0) + plugin-backed publish (v0.4.0). Each post lives in its own container with two sibling docs: a Beats doc (the structural commitments — beat list + title/preview/slug as B0) and a Draft doc (the voice-poured prose). Beats reshape regularly; draft re-pours via /authors-voice against a per-site anchor (voice/anchor-<site-slug>.md). Publish is a single post_to_blog MCP call against a site registered via add_blog_site — site-specific frontmatter (layout, author, prerender, date → publishedDate for Astro, etc.) lives on the github plugin's per-site config in ~/.openwriter/config.json, not in project config. Mirrors book-writer's discipline at post scale.
This skill obeys the shared writer contract at WRITER-CONVENTION.md. Brief shape and return shape match that doc. Sub-form values: long | short | tutorial | announcement.
OpenWriter pad mechanics are canonical in [/openwriter](../openwriter/SKILL.md) — read it, don't re-derive from tool descriptions. The load-bearing rule: populate_document is create-only, used ONCE; it re-sends the whole body, so calling it again to "fix" a doc appends a duplicate. All edits (rewrite / insert / delete) go through write_to_pad on fresh node IDs. Plus the read ladder (outline_doc → search_docs → peek_doc → read_pad).
Blog posts ship SEO-complete in the FIRST pass. A blog post almost always carries SEO — internal/cluster links, meta + slug + tags, snippet-targeted headings, FAQ/schema-eligible content all go into the initial draft (the minion brief + first populate_document), never bolted on after. Bolting SEO on later forces an edit (→ write_to_pad, never a 2nd populate). SEO-strategy-led posts (pillar / landing / comparison) front-end through /seo-writer if you have it installed (not bundled with OpenWriter).
Cover image is a publish gate (firm rule 12). Placement rules: no bottom-band text (the platform renders og:title there), keep the subject center-safe, ≤5–7 words of overlay text, do not echo the title/description. Palette / brand skin comes from the project's style_doc (e.g. recipebox.md). Generation, canonical size, and the tmp-first rule for text-overlay covers: docs/images.md.
ABOUT TO HAND-EDIT A PUBLISHED POST'S `.md`, ITS `image:` FRONTMATTER, OR DROP A COVER INTO THE REPO'S `public/`? STOP. The OpenWriter doc's blogContext.coverImage is the single source of truth, and post_to_blog is the ONLY writer to the blog repo. Edits flow doc → Accept → Publish — never doc and repo in parallel, because that drift is exactly what breaks idempotent republish (Publish rewrites <content_dir>/<slug>.md wholesale and your hand-edits vanish). Never hand-name a cover; never side-channel an image; never git push the post yourself. Canonical publish flow + the lastPublish-clobber footgun: docs/integrate.md.
| Mode | Trigger | What it does | Sub-doc |
|---|---|---|---|
setup | /blog-writer setup, "register blog repo", "add blog site" | One-time per blog: inspect_blog_repo clones the target, auto-proposes frontmatter_defaults + frontmatter_field_map from existing posts; add_blog_site persists the site config | docs/setup.md |
brainstorm | /blog-writer brainstorm, "brainstorm blog topics" | Open a Blog Ideas doc; propose 3-5 candidate angles with tone + length labels; hand off to beats when user picks | docs/brainstorm.md |
beats | /blog-writer beats, "extract beats", "blog beats" | Query-first beat extraction with 3-pass (short/announcement, 3-5 beats) or 5-pass (long/tutorial, 8-15 beats); 9 blog category tags (CLAIM/REFRAME/MECHANISM/EVIDENCE/DEMO/SCENE/OBJECTION/APHORISM/PIVOT) + HOOK/CTA positional roles; locks title + preview + slug as B0; dopamine arc layered with conversion arc | docs/beats.md + docs/titling.md |
draft | /blog-writer draft, "draft this post", "pour the beats" | Per-beat dispatch to /authors-voice Apply Protocol with site-specific anchor; cross-beat coherence pass; supports beat-level reshape loop (re-pour only the affected beats) | docs/draft.md + docs/voice-anchor.md |
images | /blog-writer images, "blog image", "featured image" | Read style doc, craft prompt, generate cover via insert_image (no docId → returns path → blogContext.coverImage); optional inline images at afterNodeId | docs/images.md |
integrate | /blog-writer integrate, "publish", "post to blog" | post_to_blog against the registered site — builds frontmatter from blogContext + site defaults, copies referenced /_images/... to image_dir, rewrites paths, commits, pushes; verifies pending decorations accepted first | docs/integrate.md |
pipeline | /blog-writer pipeline, "full blog workflow", "write and publish" | 7-step sequence (Setup → Brainstorm → Beats → Draft → Images → Accept → Publish → Verify) with explicit gates; reshape loop returns to Beats step | docs/pipeline.md |
Modes can chain (pipeline runs them in sequence) or stand alone. Beats reshape → draft re-pour is the inner loop the architecture is designed around.
Per-session. Read the project's CLAUDE.md and extract the optional ## Blog section. It can carry writing_rules, style_doc, content_driven, aspect_ratio. Used by images mode + as defaults for beats/draft. Schema: docs/project-config.md. If absent, defaults apply and the skill still runs.
Per-blog (one-time). Each blog repo must be registered with the github plugin via add_blog_site before integrate can publish to it. Run /blog-writer setup once per blog. The setup mode uses inspect_blog_repo to auto-propose frontmatter_defaults (layout, author, prerender — fields constant across the site's existing posts) and frontmatter_field_map (e.g. date → publishedDate for Astro sites). Sub-doc: docs/setup.md.
Per-post container structure (Beats + Draft sibling docs), the 10-step OpenWriter call-order for building one post, and the return/output contract live in docs/architecture.md. Two facts ride every build:
Beats — <Title> (content_type notes) holds structure; <Title> (content_type blog) is the publishable Draft — one per container. Reshape beats → re-pour only the affected beats, never the whole post.docId; the user watches the activity feed and navigates themselves. Only switch_document on an explicit user instruction ("open the Draft", "show me the Beats").beats first. Pouring prose without committed beats produces shapeless drafts that need full structural rework downstream.Beats — <Post Title> (content_type: notes) and <Post Title> (content_type: blog — the publishable doc). Beats reshape → draft re-pour. Don't mix prose into the Beats doc; don't put structural commitments in the Draft doc.beats mode. When draft mode runs: title mirrors to the Draft doc's title field via rename_item (the publish plugin reads title from there, not from blogContext.title); preview + slug mirror to blogContext via set_metadata. See docs/titling.md. The first paragraph of the draft must echo the title — can't echo what isn't locked.draft mode reads voice/anchor-<site-slug>.md where <site-slug> is the slugified site label from list_blog_sites. Silent fallback to voice/anchor.md if not present. Discovery is convention-based, NOT in plugin config. See docs/voice-anchor.md.add_blog_site before integrate can publish. Check list_blog_sites at session start — if the target repo isn't there, run /blog-writer setup first.## Blog in CLAUDE.md is for writing_rules, style_doc, content_driven, aspect_ratio. Frontmatter shape (layout, author, publishedDate vs date, etc.) lives on the github plugin's per-site config, NOT in project config./authors-voice Apply Protocol with the site-specific anchor. The skill owns shape; voice owns diction. Per-beat dispatch is the default for long/tutorial; collapse to single dispatch for short/announcement under 1000w.create_document (spinner) → populate_document (content). Never inline a 30s generation into one tool call.blogContext.date as YYYY-MM-DD in the project's configured timezone (default America/Los_Angeles). The plugin formats / renames per site config.post_to_blog reads the canonical doc on disk — pending changes don't ship. After agent writes, integrate must tell the user "click Accept All in the right rail, then I'll publish" rather than silently posting incomplete content. See docs/integrate.md.post_to_blog, the plugin writes blogContext.lastPublish = { publishedAt, publishedUrl, commit, file } on the Draft doc. File tree shows a green ✓; right-click menu surfaces "View Post." Same convention as tweets / articles / newsletters.integrates without a cover/OG image — a gate, not optional. Design per the Convention above (placement rules + style_doc palette; docs/images.md mechanics).draft without first running beats for the post — fails with "no Beats doc found in container"post_to_blog without first running setup for that repo — fails with "no blog site with id X"layout, author, prerender) into blogContext instead of the site's frontmatter_defaultspost_to_blog immediately after insert_image without waiting for accept — pending image stays in browser overlay, doesn't ship.md file directly into the target repo via Write/Edit tools — that's post_to_blog's jobdraft mode reads conventionally; no opt-in required/blog-pipeline, /blog-images, /blog-integrate, /blog-feature-images as separate skills (deprecated stubs that redirect here)mcp__openwriter__insert_image — Gemini image generation directly into the active OpenWriter doc (primary path; cover via path-return + blogContext.coverImage).webp (the github plugin copies PNGs unchanged; conversion is a project-specific concern before publish)Delegated / required:
draft delegates every beat dispatch).NOT covered (use your own tooling instead): project deploy pipelines · cross-channel announcements (Discord, X, etc.) · /newsletter-writer (different channel) · /x-writer (different channel) · /book-writer (multi-chapter books — global beat sheet, workspace management).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.