Claude Skill

share

Turn a BS report (or any analysis result) into ready-to-paste posts for X/Twitter, LinkedIn, Facebook, Reddit, Hacker News, or a newsletter issue — plus a branded image carousel (PNGs + PDF) for visual platforms. Use when the user wants to share, post, publish, or promote a repor

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download serhiikorniienko-bullshit-detector-skills_publishing_share-1a5955e.zip · 6 KB
Part of serhiikorniienko/bullshit-detector — 5 skills

Install

skills CLI npx skills add https://github.com/SerhiiKorniienko/bullshit-detector/tree/main/skills/publishing/share
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install serhiikorniienko-bullshit-detector@llmmart
Git git clone https://github.com/SerhiiKorniienko/bullshit-detector.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole serhiikorniienko/bullshit-detector collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

share

Take a finished report and produce platform-native content, ready to paste. Nothing generic: each platform gets its own format, length, and link etiquette.

Workflow

  1. Locate the report. A file the user points at, the report from this conversation, or — if there is none — offer to run the bullshit-detector skill first.
  2. Ask which platforms if not stated. Default set: X thread + LinkedIn post.
  3. Write the posts following the per-platform specs in PLATFORMS.md exactly — hooks, length limits, link placement. Output each as a separate fenced block the user can copy verbatim.
  4. Carousel (if requested or if the platform benefits): extract the report into a slides.json (schema below) and render:
uv run <this-skill-dir>/scripts/render_carousel.py slides.json -o carousel/

First run needs a one-time browser install: uv run --with playwright playwright install chromium. Output: slide-N.png (1080×1350, works on X, LinkedIn, Instagram) + carousel.pdf (LinkedIn document post).

slides.json schema

{
  "title": "Video/article title",
  "source": "Author · Platform · 1.16M views",
  "score": 5,
  "verdict_line": "Real tools, fantasy income math",
  "footer": "@their-handle · their-link (the sharer's, not the tool author's — see rules)",
  "slides": [
    { "type": "hook" },
    { "type": "claim", "n": "1/12", "claim": "Quoted or paraphrased claim, ≤200 chars",
      "verdict": "misleading", "evidence": "One-sentence reality, ≤160 chars" },
    { "type": "cta", "headline": "Run it on anything",
      "lines": ["the sharer's own links — ask, don't assume"] }
  ]
}

Verdicts: confirmed / plausible / misleading / false / unverifiable / not checked — the same six the report uses, and the renderer rejects anything else rather than guessing a colour. Pick 3–4 claim slides — the spiciest verdicts with the strongest evidence, not the first four. Hook and CTA slides bookend them.

A not checked claim renders, but think before using one: it carries no verdict and no evidence by definition, so it makes a weak slide and a reader may take the empty cell for a finding.

Rules

  • The report is the content; the tool is the footnote. Hooks lead with findings ("12 claims, 3 misleading"), never with "I built a tool".
  • Footer and CTA belong to the person sharing. Ask for their handle/newsletter/links (or omit those slides) — never default to the tool author's branding. Crediting the tool is welcome but optional: one line like "made with bullshit-detector" is plenty.
  • Numbers must match the report exactly — a fact-checking brand cannot round its own stats.
  • Stay honest in compression: if the report's verdict is nuanced ("competent hype, not a scam"), the post says that too. No rage-bait the report doesn't support.
  • Never fabricate engagement bait ("everyone is talking about this").
  • Each platform block must be paste-ready: correct length, line breaks, and links already positioned per PLATFORMS.md.
Files (bullshit-detector)
  • scripts
    • render_carousel.py 8 KB
      #!/usr/bin/env python3
      # /// script
      # requires-python = ">=3.10"
      # dependencies = [
      #     "playwright",
      #     "pillow",
      # ]
      # ///
      """Render a BS-report carousel from slides.json.
      
      Usage:
          uv run render_carousel.py slides.json [-o outdir]
      
      One-time setup (downloads headless Chromium):
          uv run --with playwright playwright install chromium
      
      Output: slide-1.png ... slide-N.png (1080x1350) + carousel.pdf (for
      LinkedIn document posts). If Chromium is missing, slides.html is still
      written so the slides can be inspected in any browser.
      """
      
      import argparse
      import html
      import json
      import sys
      from pathlib import Path
      
      W, H = 1080, 1350
      
      # Must stay in step with the verdict scale in SKILL.md, tally.py and
      # report-card's render_report.py. `not checked` was missing here until 0.8.1
      # and any carousel built from such a claim died with a KeyError.
      VERDICTS = {
          "confirmed": ("#00FF00", "CONFIRMED"),
          "plausible": ("#FFFF00", "PLAUSIBLE"),
          "misleading": ("#FF8800", "MISLEADING"),
          "false": ("#FF00FF", "FALSE"),
          "unverifiable": ("#F5F5F5", "UNVERIFIABLE"),
          "not checked": ("#FFFFFF", "NOT CHECKED"),
      }
      
      CSS = f"""
      * {{ margin: 0; padding: 0; box-sizing: border-box; }}
      body {{ background: #666; font-family: 'Helvetica Neue', Arial, sans-serif; color: #000; }}
      .slide {{
        width: {W}px; height: {H}px; background: #fff;
        border: 14px solid #000; padding: 80px 72px;
        display: flex; flex-direction: column; overflow: hidden;
        position: relative; margin: 24px auto;
      }}
      .kicker {{
        display: inline-block; align-self: flex-start;
        background: #000; color: #fff; font-weight: 900; font-size: 34px;
        letter-spacing: 4px; padding: 14px 28px; margin-bottom: 56px;
      }}
      .footer {{
        position: absolute; bottom: 44px; left: 72px; right: 72px;
        border-top: 8px solid #000; padding-top: 24px;
        font-weight: 700; font-size: 30px;
      }}
      .title {{ font-weight: 900; font-size: 66px; line-height: 1.15; margin-bottom: 36px; }}
      .source {{ font-size: 34px; font-weight: 700; color: #000; margin-bottom: 56px; }}
      .scorebox {{
        align-self: flex-start; background: #FFFF00; border: 10px solid #000;
        box-shadow: 20px 20px 0 #000; padding: 36px 56px;
      }}
      .scorebox .num {{ font-weight: 900; font-size: 150px; line-height: 1; }}
      .scorebox .verdict {{ font-weight: 700; font-size: 38px; margin-top: 16px; }}
      .chip {{
        display: inline-block; align-self: flex-start; border: 8px solid #000;
        font-weight: 900; font-size: 40px; letter-spacing: 3px;
        padding: 16px 30px; margin-bottom: 48px; box-shadow: 12px 12px 0 #000;
      }}
      .claim-n {{ font-weight: 900; font-size: 34px; letter-spacing: 4px; margin-bottom: 40px; }}
      .claim {{
        font-weight: 900; font-size: 58px; line-height: 1.25;
        border-left: 14px solid #000; padding-left: 40px; margin-bottom: 56px;
      }}
      .evidence {{ font-size: 40px; line-height: 1.45; font-weight: 400; }}
      .evidence b {{ font-weight: 900; }}
      .cta-head {{ font-weight: 900; font-size: 76px; line-height: 1.15; margin-bottom: 64px; }}
      .cta-line {{
        background: #F5F5F5; border: 8px solid #000; padding: 28px 32px;
        font-family: 'Courier New', monospace; font-weight: 700; font-size: 33px;
        margin-bottom: 32px; word-break: break-all;
      }}
      """
      
      
      def esc(s: str) -> str:
          return html.escape(str(s))
      
      
      def require(d: dict, key: str, where: str):
          """Fetch a required field, or exit saying which one is missing.
      
          Everything here used to be direct dict access, so a slides.json with a
          typo'd key produced a traceback pointing at an f-string rather than a
          sentence naming the field. Slide *type* already failed this way; the rest
          did not.
          """
          if key not in d:
              sys.exit(f"ERROR: {where} is missing required field {key!r}")
          return d[key]
      
      
      def verdict_style(value, where: str) -> tuple:
          """Colour and label for a verdict, or exit listing the valid ones.
      
          Deliberately not a neutral fallback chip. An unrecognised verdict is
          either a typo or a scale this renderer hasn't been taught, and guessing a
          colour would put a wrong verdict on a slide people share — the one failure
          a fact-checking tool cannot ship. Better to stop and say so.
          """
          key = str(value).strip().lower()
          if key not in VERDICTS:
              sys.exit(
                  f"ERROR: {where} has unknown verdict {value!r}.\n"
                  f"       Valid verdicts: {', '.join(VERDICTS)}"
              )
          return VERDICTS[key]
      
      
      def hook_slide(meta: dict) -> str:
          where = "the hook slide"
          return f"""
        <div class="slide">
          <div class="kicker">BS REPORT</div>
          <div class="title">&ldquo;{esc(require(meta, 'title', where))}&rdquo;</div>
          <div class="source">{esc(require(meta, 'source', where))}</div>
          <div class="scorebox">
            <div class="num">{esc(require(meta, 'score', where))}/10</div>
            <div class="verdict">{esc(require(meta, 'verdict_line', where))}</div>
          </div>
          <div class="footer">{esc(require(meta, 'footer', where))}</div>
        </div>"""
      
      
      def claim_slide(s: dict, meta: dict) -> str:
          where = f"claim slide {s.get('n') or '(unnumbered)'}"
          color, label = verdict_style(require(s, "verdict", where), where)
          return f"""
        <div class="slide">
          <div class="claim-n">CLAIM {esc(s.get('n', ''))}</div>
          <div class="claim">&ldquo;{esc(require(s, 'claim', where))}&rdquo;</div>
          <div class="chip" style="background:{color}">{label}</div>
          <div class="evidence">{esc(require(s, 'evidence', where))}</div>
          <div class="footer">{esc(require(meta, 'footer', where))}</div>
        </div>"""
      
      
      def cta_slide(s: dict, meta: dict) -> str:
          lines = "".join(f'<div class="cta-line">{esc(l)}</div>' for l in s.get("lines", []))
          return f"""
        <div class="slide">
          <div class="kicker">TRY IT</div>
          <div class="cta-head">{esc(s.get('headline', 'Run it on anything'))}</div>
          {lines}
          <div class="footer">{esc(require(meta, 'footer', 'the CTA slide'))}</div>
        </div>"""
      
      
      def build_html(spec: dict) -> str:
          parts = []
          for s in spec["slides"]:
              kind = s.get("type")
              if kind == "hook":
                  parts.append(hook_slide(spec))
              elif kind == "claim":
                  parts.append(claim_slide(s, spec))
              elif kind == "cta":
                  parts.append(cta_slide(s, spec))
              else:
                  sys.exit(f"ERROR: unknown slide type {kind!r}")
          return f"<!doctype html><html><head><meta charset='utf-8'><style>{CSS}</style></head><body>{''.join(parts)}</body></html>"
      
      
      def main() -> None:
          ap = argparse.ArgumentParser(description="Render BS-report carousel slides")
          ap.add_argument("spec", help="path to slides.json")
          ap.add_argument("-o", "--outdir", default="carousel", help="output directory")
          args = ap.parse_args()
      
          spec = json.loads(Path(args.spec).read_text(encoding="utf-8"))
          outdir = Path(args.outdir)
          outdir.mkdir(parents=True, exist_ok=True)
      
          html_path = outdir / "slides.html"
          html_path.write_text(build_html(spec), encoding="utf-8")
          print(f"wrote {html_path}")
      
          try:
              from playwright.sync_api import sync_playwright
      
              with sync_playwright() as p:
                  browser = p.chromium.launch()
                  page = browser.new_page(viewport={"width": W + 100, "height": H + 100})
                  page.goto(html_path.resolve().as_uri())
                  pngs = []
                  for i, el in enumerate(page.locator(".slide").all(), 1):
                      png = outdir / f"slide-{i}.png"
                      el.screenshot(path=str(png))
                      pngs.append(png)
                      print(f"wrote {png}")
                  browser.close()
          except Exception as e:
              print(f"\nERROR: could not render PNGs ({e})", file=sys.stderr)
              print("HINT: run `uv run --with playwright playwright install chromium` once, then retry.", file=sys.stderr)
              print(f"HINT: {html_path} is viewable in any browser meanwhile.", file=sys.stderr)
              sys.exit(2)
      
          # LinkedIn "carousel" = a PDF document post
          from PIL import Image
      
          images = [Image.open(p).convert("RGB") for p in pngs]
          pdf_path = outdir / "carousel.pdf"
          images[0].save(pdf_path, save_all=True, append_images=images[1:], resolution=96)
          print(f"wrote {pdf_path}")
      
      
      if __name__ == "__main__":
          main()
      
  • PLATFORMS.md 2.5 KB
    # Platform specs
    
    Every block you produce must be paste-ready for its platform. These are the rules.
    
    ## X / Twitter — thread (default)
    
    - Threads are free; **X Articles need Premium+ — never propose them unless the user confirms they pay for it.**
    - Tweet 1 = the hook: the single most surprising verified fact + the stakes. No links, no hashtags, no "🧵" begging. ≤280 chars.
    - Middle tweets: one claim per tweet — quote/paraphrase, verdict, one-line evidence. Number as `2/`, `3/`, …
    - Second-to-last tweet: the honest bottom line (including what the content got right).
    - Last tweet: CTA — repo link + one line on how to run it yourself. Links only here.
    - Attach carousel images to tweet 1 (max 4 PNGs).
    
    ## X / Twitter — single post
    
    - One verified finding + BS score + link. ≤280 chars including the URL (URLs count as 23).
    - Use when the user wants low effort or is quote-posting the original content.
    
    ## LinkedIn
    
    - Story format, first person, short paragraphs (1–2 sentences), blank line between each.
    - Hook in the first two lines — that's all the feed shows before "…see more".
    - **No links in the post body** (reach penalty). End with "link in first comment" and output the comment separately.
    - Attach `carousel.pdf` as a document post for the carousel variant — LinkedIn's native "carousel" IS a PDF document.
    - 1,300–2,000 chars total. Up to 3 hashtags at the very end, niche ones.
    
    ## Facebook
    
    - Casual register, shorter than LinkedIn, first person.
    - Links are fine in the body. Emojis fine. Groups: read the group's promo rules first, lead with the finding not the tool.
    
    ## Reddit
    
    - Title = the finding, not the tool ("I fact-checked a 1.1M-view 'make money with AI' video — 12 claims, here's what survived").
    - Body: report highlights in plain markdown, repo link at the bottom, transparent "I built this" disclosure — Reddit punishes stealth marketing harder than open self-promo.
    - Match the sub: r/ClaudeAI wants the skill mechanics; r/SideProject wants the build story; general subs want the findings only.
    
    ## Hacker News — Show HN
    
    - Title: `Show HN: <what it is, plainly>` — no clickbait, no emoji, no "AI-powered".
    - URL = the repo. First comment (write it too): why you built it, how it works in 3–4 sentences, what feedback you want, known limitations. HN rewards stated weaknesses.
    
    ## Newsletter (Kit)
    
    - Subject ≤50 chars, the finding not the tool.
    - Structure: what was checked → 3 best verdicts with evidence → honest bottom line → one CTA (repo or reply).
    - Plain, personal, no corporate "we". Sign off with a name.
    
  • SKILL.md 3.4 KB
    ---
    name: share
    description: Turn a BS report (or any analysis result) into ready-to-paste posts for X/Twitter, LinkedIn, Facebook, Reddit, Hacker News, or a newsletter issue — plus a branded image carousel (PNGs + PDF) for visual platforms. Use when the user wants to share, post, publish, or promote a report, asks for "a thread", "a LinkedIn post", "a carousel", or "format this for X".
    ---
    
    # share
    
    Take a finished report and produce platform-native content, ready to paste. Nothing generic: each platform gets its own format, length, and link etiquette.
    
    ## Workflow
    
    1. **Locate the report.** A file the user points at, the report from this conversation, or — if there is none — offer to run the `bullshit-detector` skill first.
    2. **Ask which platforms** if not stated. Default set: X thread + LinkedIn post.
    3. **Write the posts** following the per-platform specs in [PLATFORMS.md](PLATFORMS.md) exactly — hooks, length limits, link placement. Output each as a separate fenced block the user can copy verbatim.
    4. **Carousel (if requested or if the platform benefits):** extract the report into a `slides.json` (schema below) and render:
    
    ```bash
    uv run <this-skill-dir>/scripts/render_carousel.py slides.json -o carousel/
    ```
    
    First run needs a one-time browser install: `uv run --with playwright playwright install chromium`. Output: `slide-N.png` (1080×1350, works on X, LinkedIn, Instagram) + `carousel.pdf` (LinkedIn document post).
    
    ## slides.json schema
    
    ```json
    {
      "title": "Video/article title",
      "source": "Author · Platform · 1.16M views",
      "score": 5,
      "verdict_line": "Real tools, fantasy income math",
      "footer": "@their-handle · their-link (the sharer's, not the tool author's — see rules)",
      "slides": [
        { "type": "hook" },
        { "type": "claim", "n": "1/12", "claim": "Quoted or paraphrased claim, ≤200 chars",
          "verdict": "misleading", "evidence": "One-sentence reality, ≤160 chars" },
        { "type": "cta", "headline": "Run it on anything",
          "lines": ["the sharer's own links — ask, don't assume"] }
      ]
    }
    ```
    
    Verdicts: `confirmed` / `plausible` / `misleading` / `false` / `unverifiable` / `not checked` — the same six the report uses, and the renderer rejects anything else rather than guessing a colour. Pick 3–4 claim slides — the spiciest verdicts with the strongest evidence, not the first four. Hook and CTA slides bookend them.
    
    A `not checked` claim renders, but think before using one: it carries no verdict and no evidence by definition, so it makes a weak slide and a reader may take the empty cell for a finding.
    
    ## Rules
    
    - **The report is the content; the tool is the footnote.** Hooks lead with findings ("12 claims, 3 misleading"), never with "I built a tool".
    - **Footer and CTA belong to the person sharing.** Ask for their handle/newsletter/links (or omit those slides) — never default to the tool author's branding. Crediting the tool is welcome but optional: one line like "made with bullshit-detector" is plenty.
    - Numbers must match the report exactly — a fact-checking brand cannot round its own stats.
    - Stay honest in compression: if the report's verdict is nuanced ("competent hype, not a scam"), the post says that too. No rage-bait the report doesn't support.
    - Never fabricate engagement bait ("everyone is talking about this").
    - Each platform block must be paste-ready: correct length, line breaks, and links already positioned per PLATFORMS.md.
    
  • VERSION 7 B · in bundle

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related