.agents/skills/forbotsake-publish/SKILL.md
Stage 8: SHIP. Two modes: POST (auto-post via Claude for Chrome) or COPY (format for copy-paste). Handles X/Twitter threads, LinkedIn posts, blog posts, and more. Logs everything for retrospective analysis. For auto-posting, the user needs the Claude for Chrome extension installed. Without it, the skill falls back to COPY mode automatically. Use when: "publish this", "format for twitter", "post to X", "ship this content", "format for blog", "send this email", "ready to publish", "auto-post", "post this to X", "publish to LinkedIn". Proactively invoke when the user has content in content/ and says they want to ship it.
npx skillsauth add forbotsake/forbotsake forbotsake-publishInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
From draft to published. Format, post, log.
FORBOTSAKE_HOME="${FORBOTSAKE_HOME:-$HOME/.forbotsake}"
mkdir -p "$FORBOTSAKE_HOME"
# Discover forbotsake install directory
_FBS_ROOT=""
for _FBS_CANDIDATE in "$HOME/.codex/skills/forbotsake" "$HOME/.agents/skills/forbotsake"; do
[ -d "$_FBS_CANDIDATE" ] && _FBS_ROOT="$_FBS_CANDIDATE" && break
done
if [ -z "$_FBS_ROOT" ]; then
echo "WARNING: forbotsake not found. Install: bash <(curl -fsSL https://raw.githubusercontent.com/forbotsake/forbotsake/main/bin/install.sh)"
fi
# Check for updates
_UPD=""
[ -n "$_FBS_ROOT" ] && [ -x "$_FBS_ROOT/bin/forbotsake-update-check" ] && _UPD=$("$_FBS_ROOT/bin/forbotsake-update-check" 2>/dev/null || true)
[ -n "$_UPD" ] && echo "$_UPD" || true
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
echo "BRANCH: $_BRANCH"
# Orchestrated mode (invoked by forbotsake-go, propagated via file flag)
_ORCH_FILE="${FORBOTSAKE_HOME:-$HOME/.forbotsake}/orchestrated-$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")"
FORBOTSAKE_ORCHESTRATED=$(cat "$_ORCH_FILE" 2>/dev/null || echo 0)
echo "ORCHESTRATED: $FORBOTSAKE_ORCHESTRATED"
# Check for strategy.md
if [ -f strategy.md ]; then
echo "STRATEGY: yes"
else
echo "STRATEGY: no"
echo "WARNING: No strategy.md found. Run /forbotsake-marketing-start first."
fi
# Check for content directory
if [ -d content/ ]; then
echo "CONTENT_DIR: yes"
ls -1 content/ 2>/dev/null | head -20
else
echo "CONTENT_DIR: no"
fi
# Check for published log
if [ -f published-log.md ]; then
echo "PUBLISHED_LOG: yes"
tail -5 published-log.md
else
echo "PUBLISHED_LOG: no"
fi
If output shows UPGRADE_AVAILABLE <old> <new>: read the forbotsake-upgrade SKILL.md
at $_FBS_ROOT/forbotsake-upgrade/SKILL.md (where _FBS_ROOT is the variable already
resolved in the preamble bash above) and follow the "Inline upgrade flow" section Step 1
only. If Step 1 results in "Yes" or "Always" (proceed with upgrade), continue through
Steps 2-7 of the inline flow. If Step 1 results in "Not now" or "Never" (declined),
skip Steps 2-7 entirely and continue with this skill immediately.
If output shows JUST_UPGRADED <old> <new>: tell user
"Running forbotsake v{new} (just updated from v{old})!" and continue.
If STRATEGY is no: "No strategy.md found. You need positioning and channel strategy before publishing. Run /forbotsake-marketing-start first."
Stop here unless user insists.
If CONTENT_DIR is no: "No content/ directory found. Run /forbotsake-create to write your first piece, or point me to the content you want to publish."
Ask the user directly in conversation to let the user provide content manually if they prefer.
After the preamble checks, detect whether Claude for Chrome browser tools are available. Try calling mcp__claude-in-chrome__tabs_context_mcp to check if the Chrome extension is connected. If it responds successfully, set CHROME_AVAILABLE to yes. If it errors or is not available, set CHROME_AVAILABLE to no.
If CHROME_AVAILABLE is yes: the user can choose POST mode (auto-post via Chrome) or COPY mode.
If CHROME_AVAILABLE is no: silently default to COPY mode. Mention once: "Auto-posting available with the Claude for Chrome extension. Using copy-paste mode."
Orchestrated mode (ORCHESTRATED is 1):
status: reviewed, status: revised, or status: reviewed-override in frontmatterchannel: frontmatter fieldstatus: reviewed (or revised/reviewed-override) to status: published. This prevents re-publish loops when forbotsake-go runs again.Interactive mode (ORCHESTRATED is 0):
Read strategy.md to understand the channel strategy and ICP.
Read all files in content/ to see what's available. Check each file's frontmatter for status field. Files with status: published in the log should be noted as already published.
Ask the user directly in conversation:
"Here's what I found in your content/ directory:
{list each file with a one-line summary and status}
Which piece do you want to publish? And which platform?
Auto-post (I post for you via Chrome):
- X/Twitter (threads + single tweets)
- LinkedIn (posts)
Copy-paste + open browser (I format it, open the compose page, you paste):
- Blog (Ghost, WordPress, Dev.to, Medium, Substack)
- Hacker News
- Product Hunt
Copy-paste only:
- Email (formatted for your ESP)
CLI:
- GitHub (PRs via
ghCLI)Pick a content piece and a platform."
If CHROME_AVAILABLE is no, present all platforms as "Copy-paste" mode only. Do not show the Auto-post tier.
If the user says "all" or "publish everything", loop through each unpublished content file and process them sequentially. Between posts to the same platform, wait 30-60 seconds. Maximum 5 posts to the same platform per session to avoid triggering anti-automation measures.
If CHROME_AVAILABLE is yes AND the selected platform is X/Twitter or LinkedIn, ask the user directly in conversation:
"How do you want to publish to {platform}? A) POST — I'll post it for you right now via Chrome B) COPY — just give me the formatted text"
For blog platforms, Reddit, HN, and Product Hunt: if Chrome is available, offer to open the platform's compose page in a new Chrome tab after formatting. This is a "COPY + deep link" flow — you format the content, open the compose URL, and the user pastes it in.
For Email and GitHub: always COPY mode (email ESPs vary too much for automation; GitHub uses gh CLI).
For each selected content file, check if visual assets exist alongside it:
# For a content file like content/2026-04-07-x-launch-thread.md
# Look for: content/2026-04-07-x-launch-thread-visual-*.png or *.mp4
ls content/*-visual-*.{png,jpg,mp4} 2>/dev/null
Also read the content file's frontmatter for visual metadata:
visual_treatment: none/text-card/ai-image/videovisual_status: generated/failed/pending/skippedvisual_alt: accessibility descriptionIf visual assets exist (visual_status: generated):
If visual_treatment is not none but no visual file exists (visual_status: failed/pending/skipped):
If visual_treatment is none: No visual handling needed, proceed normally.
Read the knowledge framework for thread structure:
_SKILL_DIR=$(dirname "$(find $HOME/.codex/skills -path "*/forbotsake-publish/SKILL.md" -type f 2>/dev/null | head -1)")
echo "SKILL_DIR: $_SKILL_DIR"
Read $_SKILL_DIR/../knowledge/frameworks/content-strategy.md for X thread format guidelines.
Format the content as a numbered thread following these rules:
Tweet 1 (The Hook): Must stop the scroll. Lead with the most surprising, contrarian, or specific claim. No preamble. No "I've been thinking about..." Start with the insight. Must be under 280 characters.
Tweets 2-N (The Body): One idea per tweet. Each tweet must stand alone if someone screenshots it. Use line breaks for readability. Keep each under 280 characters.
Final Tweet (The CTA): What should the reader do? Follow, reply, check out the product, share? Be specific. Include the link if there is one.
Output format:
---
THREAD: {title}
Platform: X/Twitter
Tweets: {count}
Character counts: verified
---
1/ {hook tweet}
[{character count}/280]
2/ {body tweet}
[{character count}/280]
...
{N}/ {CTA tweet}
[{character count}/280]
Verify every tweet is under 280 characters. If any exceeds the limit, split or rewrite it. Do NOT deliver a thread with tweets over 280 characters.
Format with full SEO structure:
---
BLOG POST: {title}
Platform: Blog
Word count: {count}
Reading time: {X} min
---
## SEO Meta
- **Title tag** (under 60 chars): {title}
- **Meta description** (under 155 chars): {description}
- **URL slug:** {slug}
- **Target keyword:** {keyword}
- **Secondary keywords:** {list}
## Headline Options
1. {headline option 1 - benefit-focused}
2. {headline option 2 - curiosity-driven}
3. {headline option 3 - specific/numbered}
## Featured Image Suggestions
- {description of image 1 - what to create or find}
- {description of image 2 - alternative option}
## Post Body
{formatted blog post with H2/H3 headers, short paragraphs, bold key phrases}
## Internal Links
- {suggest where to link to product, other content, etc.}
Format with multiple subject line options and clear structure:
---
EMAIL: {title}
Platform: Email
Word count: {count}
---
## Subject Line Options
1. {subject line 1} [{character count} chars]
2. {subject line 2} [{character count} chars]
3. {subject line 3} [{character count} chars]
## Preview Text
{the text that shows after the subject in inbox, under 90 chars}
## Email Body
{formatted email - short paragraphs, conversational, one clear ask}
## CTA
- **Primary CTA:** {button text} -> {URL or action}
- **Secondary CTA:** {text link or PS line}
## Send Time Suggestion
{best time to send based on audience from strategy.md}
Format for LinkedIn's algorithm and audience:
---
LINKEDIN POST: {title}
Platform: LinkedIn
Character count: {count}/3000
---
{hook line - must work as the "see more" preview}
{body - short paragraphs, line breaks between each, no walls of text}
{CTA or question to drive comments}
{3-5 relevant hashtags}
Ask the user directly in conversation to understand the platform's constraints (character limits, format, audience norms), then format accordingly.
Skip the kill switch (3a) if FORBOTSAKE_FAST env var is 1. Print: "FORBOTSAKE_FAST=1: skipping publish kill switch." Jump to 3b.
This is the last gate before content goes public. Not a deep review (that happened in /forbotsake-content-check). This is a final sanity check: "would I be embarrassed?"
Read the banned patterns files:
FORBOTSAKE_HOME="${FORBOTSAKE_HOME:-$HOME/.forbotsake}"
_SKILL_DIR=$(dirname "$(find $HOME/.codex/skills -path "*/forbotsake-marketing-start/SKILL.md" -type f 2>/dev/null | head -1)" 2>/dev/null)
_FBS_ROOT=$(cd "${_SKILL_DIR}/.." 2>/dev/null && pwd || true)
[ -n "$_FBS_ROOT" ] && [ -f "$_FBS_ROOT/knowledge/banned-patterns-defaults.md" ] && echo "DEFAULTS: found" || echo "DEFAULTS: not_found"
[ -f "$FORBOTSAKE_HOME/banned-patterns.md" ] && echo "USER_PATTERNS: found" || echo "USER_PATTERNS: none"
Read the banned patterns file(s). Then scan the formatted content for:
Embarrassment test: Read the final formatted content as if you're the ICP seeing it for the first time. Would you show this to your smartest friend? Any sentence that makes you cringe?
Factual claims: Any claims that could be verifiably wrong? Specific numbers, statistics, or comparisons that the founder hasn't verified?
AI tell scan: Check for patterns from the banned patterns files. Count matches. Each pattern match adds to a score. This is a contributing signal, not an automatic fail. 3+ pattern matches across the piece = HOLD.
Platform fit: Does the formatted version look right for the platform? Character limits respected? Thread structure correct? Links formatted correctly?
Scoring: Each dimension contributes to a GO/HOLD verdict.
If GO: Print "Publish Kill Switch: GO. Content cleared for publishing." Continue to Phase 3b.
If HOLD: This ALWAYS requires explicit user confirmation, even in orchestrated mode.
"Publish Kill Switch: HOLD
Last-gate check before this goes public: {for each issue:}
- {dimension}: {specific concern}
This content has not been published yet. Your call."
Ask the user directly in conversation: A) Publish anyway (I've reviewed the concerns) B) Go back and fix it
If A: log override to metrics, continue. If B: stop the publish flow. User returns to /forbotsake-content-check or manual editing.
Log result:
FORBOTSAKE_HOME="${FORBOTSAKE_HOME:-$HOME/.forbotsake}"
mkdir -p "$FORBOTSAKE_HOME"
echo '{"gate":"publish","ts":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","result":"RESULT","content_file":"FILENAME","override":BOOL}' >> "$FORBOTSAKE_HOME/review-metrics.jsonl" 2>/dev/null || true
Before delivering the formatted content, verify:
[YOUR_LINK_HERE] so nothing is missed.If any check fails, fix it before presenting to the user.
This phase splits based on the mode selected in Phase 1.
Present the formatted content to the user with:
"Here's your {platform}-ready content. Copy-paste ready.
{formatted content}
{If visual asset exists: "Attach this image:
{path to visual file}Alt text: {visual_alt from frontmatter}"}{If visual_treatment is video and video file exists: "Attach this video:
{path to video file}"}Before you post, double-check:
- [ ] Links are correct
- [ ] Handle/username is right
- [ ] {If visual: "Image/video is attached"}
- [ ] Timing is good (see send time suggestion if applicable)
Want me to format this for another platform too?"
For blog platforms, Reddit, HN, and Product Hunt: if Chrome is available, also open the platform's compose page in a new tab using mcp__claude-in-chrome__tabs_create_mcp so the user can paste directly. Tell the user: "I've opened {platform} in a new tab. Paste your content there."
Skip to Phase 5 (Log).
POST mode automates the entire posting flow via Claude for Chrome. It works best for X and LinkedIn, which have stable compose UIs. Other platforms may have lower reliability. COPY mode always works as a fallback.
Before posting anything, verify the logged-in account:
mcp__claude-in-chrome__tabs_create_mcp to open the platform's home page (x.com/home for X, linkedin.com/feed for LinkedIn).mcp__claude-in-chrome__read_page or mcp__claude-in-chrome__get_page_text to find the current username/handle on the page.If the page shows a login wall instead of the home feed, the user is not logged in. Fall back to COPY mode: "Looks like you're not logged into {platform} in Chrome. Here's the formatted text to copy-paste instead."
Read the publishing automation knowledge framework for platform-specific instructions:
_SKILL_DIR=$(dirname "$(find $HOME/.codex/skills -path "*/forbotsake-publish/SKILL.md" -type f 2>/dev/null | head -1)")
echo "SKILL_DIR: $_SKILL_DIR"
Read $_SKILL_DIR/../knowledge/frameworks/publishing-automation.md for the platform-specific posting flow. If this file does not exist, use the inline instructions below.
For X/Twitter:
mcp__claude-in-chrome__navigate to go to https://x.com/compose/postmcp__claude-in-chrome__read_page to verify the compose UI is presentFor LinkedIn:
mcp__claude-in-chrome__navigate to go to https://linkedin.com/feedmcp__claude-in-chrome__find to locate the "Start a post" buttonmcp__claude-in-chrome__computer to click itIf navigation fails or the compose UI is not found:
X/Twitter single tweet:
mcp__claude-in-chrome__upload_image with the visual file path to upload the image to the compose area.upload_image tool is not available: use mcp__claude-in-chrome__computer to click the media icon (camera/image button), then try attaching via the file dialog. If that fails, fall back to COPY mode with the image path noted.mcp__claude-in-chrome__computer to click the compose textareamcp__claude-in-chrome__computer to type the tweet textscreencapture -x /tmp/forbotsake-pre-post-$(date +%s).png and then use the Read tool to show it to the user. If screencapture fails (non-macOS), skip the screenshot and continue.X/Twitter thread:
mcp__claude-in-chrome__upload_image BEFORE typing text. If visual_count > 1, attach subsequent images to their corresponding tweets.mcp__claude-in-chrome__computer to click the compose textareascreencapture -x /tmp/forbotsake-pre-post-$(date +%s).png and use the Read tool to show it.Compose-time failure recovery: If thread composing fails (e.g., the "+" button is not found after tweet N):
Post-time failure recovery: If the "Post all" button is clicked but the thread only partially appears (platform error):
mcp__claude-in-chrome__read_page to check what was actually published.LinkedIn post:
mcp__claude-in-chrome__upload_image to upload the image into the LinkedIn compose modal.mcp__claude-in-chrome__computer to click the compose area in the modalscreencapture as pre-post evidenceBefore clicking Post, show the user a screenshot of the filled compose area and ask for confirmation:
"Here's what's ready to post on {platform}. {screenshot}
Ready to publish?"
Ask the user directly in conversation with options: A) Post it, B) Edit first (switch to COPY mode), C) Cancel.
If A (Post):
mcp__claude-in-chrome__computer to click the Post/Publish button.If the user specifies a time (e.g., "schedule for Monday 10am PT"):
After posting:
mcp__claude-in-chrome__read_page or mcp__claude-in-chrome__get_page_text to read the current page.x.com/{handle}/status/{id}).screencapture -x /tmp/forbotsake-published-$(date +%s).png and use the Read tool to show it.If URL capture fails (some platforms don't redirect immediately): Tell the user: "Posted successfully. The URL didn't load yet. Paste it into published-log.md when you see it."
If content verification fails (published content doesn't match source): Warn: "Published content may not match source. Please verify the post on {platform}."
If any step in Phase 4 fails, fall back to COPY mode and log the failure. Every error message must include:
Append to published-log.md in the project root. Create the file if it doesn't exist.
## {date} - {platform}
- **Content:** {title or first line}
- **Source file:** {path to content file}
- **Format:** {thread/blog/email/linkedin/other}
- **Mode:** POST
- **Link:** {captured URL}
- **Visual:** {visual_treatment} via {visual_provider} — {path to visual file or "none"}
- **Notes:** {any relevant context}
---
## {date} - {platform} (AUTO)
- **Content:** {title or first line}
- **Source file:** {path to content file}
- **Format:** {thread/blog/email/linkedin/other}
- **Mode:** POST (autonomous/cron)
- **Link:** {captured URL}
- **Scheduled:** {ISO 8601 from content-calendar.md}
- **Posted:** {ISO 8601 actual post time}
- **Result:** success
- **Notes:** Auto-posted by forbotsake-cron
---
## {date} - {platform} (AUTO/FAILED)
- **Content:** {title or first line}
- **Source file:** {path to content file}
- **Mode:** POST (autonomous/cron)
- **Result:** failed
- **Failure reason:** {specific error}
- **Notes:** Content status set to `failed`. Set back to `reviewed` to retry.
---
## {date} - {platform} (MISSED)
- **Content:** {title or first line}
- **Source file:** {path to content file}
- **Scheduled:** {ISO 8601 from content-calendar.md}
- **Result:** missed
- **Reason:** Laptop was asleep, catch-up policy: skip
- **Notes:** Content remains `reviewed`. Manually post or reschedule.
---
## {date} - {platform}
- **Content:** {title or first line}
- **Source file:** {path to content file}
- **Format:** {thread/blog/email/linkedin/other}
- **Mode:** COPY
- **Link:** pending
- **Visual:** {visual_treatment} via {visual_provider} — {path to visual file or "none"}
- **Notes:** Copy-paste delivered. Update link after posting. {If visual exists: "Attach image: {path}"}
---
## {date} - {platform} (PARTIAL)
- **Content:** {title or first line}
- **Source file:** {path to content file}
- **Format:** thread
- **Mode:** POST (partial)
- **Link:** {URL of first tweet if captured}
- **Posted:** {N}/{total} tweets
- **Notes:** Thread posting interrupted after tweet {N}. Remaining tweets delivered as copy-paste.
---
## {date} - {platform} (FAILED -> COPY)
- **Content:** {title or first line}
- **Source file:** {path to content file}
- **Format:** {format}
- **Mode:** POST (failed)
- **Failure reason:** {Chrome not detected | login wall | element not found | rate limited | content mismatch}
- **Fallback:** COPY mode delivered
- **Link:** pending
---
If published-log.md doesn't exist, create it with a header:
# Published Content Log
Track what was published, where, and when. Used by /forbotsake-retro for measurement.
---
{first entry}
After logging, update the source content file's frontmatter status based on the outcome:
status: publishedstatus: partialOrchestrated mode (ORCHESTRATED is 1): Skip this phase entirely. Confirm: "Formatted and logged {N} pieces to published-log.md." Then stop.
Interactive mode (ORCHESTRATED is 0): Tell the user:
"Logged to published-log.md. {If POST mode: 'Posted to {platform} as {handle}.'}
{If links are pending: 'Update the link in published-log.md after posting.'}
Track results and run
/forbotsake-retronext week to measure what worked.Want to publish to another platform? Or run
/forbotsake-createfor the next piece?"
For batch publishing, show a summary:
"Published {N}/{total} pieces:
- {piece 1}: {platform} ({mode}) {URL or 'pending'}
- {piece 2}: {platform} ({mode}) {URL or 'pending'} ...
Run
/forbotsake-retronext week to measure results."
testing
Upgrade forbotsake to the latest version. Detects install type (git clone vs vendored), runs the upgrade, and shows what's new. Use when: "upgrade forbotsake", "update forbotsake", "get latest version", "forbotsake update".
research
Stage 3: RESEARCH (competitors). Analyzes 3-5 competitors to find messaging whitespace and positioning gaps. Produces competitor-analysis.md with a messaging matrix showing what each competitor says, what's missing, and where you can win. Use when: "competitor analysis", "competitive research", "what are others doing", "market landscape", "who am I competing with", "spy on competitors", "messaging whitespace", "differentiation research". Proactively invoke when the user mentions competitors or asks how to differentiate.
development
Stage 4.5: SHARPEN. Takes a specific outreach target (person or organization) and produces a deep execution plan with contextual research, relationship mapping, angle selection, and a multi-touch sequence. Reads your founder profile and strategy to leverage warm paths and unfair advantages. Use when: "refine this plan", "go deeper on this", "sharpen execution", "how do I approach [person]", "outreach to [person]", "target [person/org]", "approach [person]", "engage [org]". Proactively invoke when the user mentions approaching a specific person or organization as part of their marketing strategy. Requires: strategy.md (from /forbotsake-marketing-start).
data-ai
Stage 9: MEASURE. Reviews what you published, analyzes performance data, and produces a retro report with evidence-based recommendations. Tells you what to double down on, what to drop, and what to try next. Use when: "what worked", "marketing retro", "measure results", "review performance", "which content performed best", "should I keep doing this". Proactively invoke one week after /forbotsake-publish was last run.