Skip to content

Instantly share code, notes, and snippets.

@eibrahim
Created March 9, 2026 17:52
Show Gist options
  • Select an option

  • Save eibrahim/d560982c0d66067ffd09ad40cc070c57 to your computer and use it in GitHub Desktop.

Select an option

Save eibrahim/d560982c0d66067ffd09ad40cc070c57 to your computer and use it in GitHub Desktop.
Claude Code /ship command - logs 'build in public' entries to Notion for social media content generation
#!/usr/bin/env python3
"""
Claude Code Ship Logger - posts "build in public" entries to Notion.
Reads a JSON payload from stdin with structured ship data and creates
a rich Notion page with properties + content blocks suitable for
social media content generation.
"""
import json
import sys
import os
import urllib.request
import urllib.error
from datetime import datetime, timezone
# -- Config ------------------------------------------------------------------
NOTION_TOKEN = os.environ.get("NOTION_TOKEN", "")
NOTION_DATABASE_ID = "8be59ed4-fc5c-42fe-9f2c-a333d2a872fa"
NOTION_API_VERSION = "2022-06-28"
# -- Notion API helpers ------------------------------------------------------
def notion_request(method: str, path: str, body: dict | None = None) -> dict:
url = f"https://api.notion.com/v1{path}"
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(
url,
data=data,
method=method,
headers={
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_API_VERSION,
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
raise RuntimeError(f"Notion API {e.code}: {err_body}") from e
def rich_text(content: str) -> list:
"""Split text into 2000-char Notion rich_text blocks."""
if not content:
return [{"text": {"content": ""}}]
blocks = []
for i in range(0, len(content), 2000):
blocks.append({"text": {"content": content[i:i+2000]}})
return blocks
def make_page_blocks(ship: dict) -> list:
"""Build Notion block children for the page body."""
blocks = []
# Summary section
summary = ship.get("summary", "")
if summary:
blocks.append({
"object": "block",
"type": "heading_2",
"heading_2": {"rich_text": [{"text": {"content": "Summary"}}]}
})
for paragraph in summary.split("\n\n"):
paragraph = paragraph.strip()
if paragraph:
blocks.append({
"object": "block",
"type": "paragraph",
"paragraph": {"rich_text": rich_text(paragraph)}
})
# Key Changes section
changes = ship.get("changes", [])
if changes:
blocks.append({
"object": "block",
"type": "heading_2",
"heading_2": {"rich_text": [{"text": {"content": "Key Changes"}}]}
})
for change in changes:
blocks.append({
"object": "block",
"type": "bulleted_list_item",
"bulleted_list_item": {"rich_text": [{"text": {"content": str(change)}}]}
})
# Impact section
impact = ship.get("impact", "")
if impact:
blocks.append({
"object": "block",
"type": "heading_2",
"heading_2": {"rich_text": [{"text": {"content": "Impact"}}]}
})
blocks.append({
"object": "block",
"type": "paragraph",
"paragraph": {"rich_text": rich_text(impact)}
})
# Technical Details section
technical = ship.get("technical_details", "")
if technical:
blocks.append({
"object": "block",
"type": "heading_2",
"heading_2": {"rich_text": [{"text": {"content": "Technical Details"}}]}
})
blocks.append({
"object": "block",
"type": "paragraph",
"paragraph": {"rich_text": rich_text(technical)}
})
return blocks
def create_ship_entry(ship: dict) -> dict:
"""Create a Notion page with properties and rich body content."""
tech_stack = ship.get("tech_stack", [])
if isinstance(tech_stack, str):
tech_stack = [t.strip() for t in tech_stack.split(",")]
changes_text = "\n".join(f"- {c}" for c in ship.get("changes", []))
properties = {
"Title": {
"title": [{"text": {"content": ship.get("title", "Untitled Ship")[:100]}}]
},
"Project": {
"rich_text": rich_text(ship.get("project", "unknown"))
},
"Date": {
"date": {"start": datetime.now(timezone.utc).isoformat()}
},
"Summary": {
"rich_text": rich_text(ship.get("summary", "")[:2000])
},
"Key Changes": {
"rich_text": rich_text(changes_text[:2000])
},
"Tech Stack": {
"multi_select": [{"name": t[:100]} for t in tech_stack[:10]]
},
"Type": {
"select": {"name": ship.get("type", "Feature")}
},
"Status": {
"select": {"name": ship.get("status", "Shipped")}
},
"Commit/PR": {
"rich_text": rich_text(ship.get("commit_or_pr", ""))
},
"Repository": {
"rich_text": rich_text(ship.get("repository", ""))
},
}
body = {
"parent": {"database_id": NOTION_DATABASE_ID},
"properties": properties,
}
# Add page content blocks
children = make_page_blocks(ship)
if children:
body["children"] = children
return notion_request("POST", "/pages", body)
# -- Entry point -------------------------------------------------------------
def main():
if not NOTION_TOKEN:
print("ERROR: NOTION_TOKEN env var not set", file=sys.stderr)
sys.exit(1)
try:
ship = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)
required = ["title", "summary"]
missing = [f for f in required if not ship.get(f)]
if missing:
print(f"ERROR: Missing required fields: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
try:
result = create_ship_entry(ship)
page_url = result.get("url", "")
print(f"Logged to Notion: {ship['title']}")
if page_url:
print(page_url)
except Exception as e:
print(f"Notion logger error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

Summarize this session as a "build in public" ship log entry, then post it to Notion.

Instructions

  1. Review everything accomplished in this conversation. Look at commits made, files changed, features built, bugs fixed, and deployments done.

  2. If in a git repo, run git log --oneline -5 and git remote get-url origin 2>/dev/null to get recent commits and repo URL.

  3. Generate a JSON object with ALL of these fields (be thorough - this will be used to generate social media content):

{
  "title": "Short, compelling title of what was shipped (max 80 chars)",
  "project": "Project name",
  "type": "Feature | Bug Fix | Improvement | Refactor | Infrastructure | Design",
  "status": "Shipped | Deployed | Fixed | Improved | In Progress",
  "tech_stack": ["Next.js", "Prisma", "etc"],
  "summary": "2-4 paragraph narrative. Write as if telling a developer audience what you built and why. Cover: the problem, the approach, key decisions, and the result. Be specific and concrete - mention actual technologies, patterns, and metrics where relevant. This is the primary content source for tweets, LinkedIn posts, and articles.",
  "changes": ["Added X", "Fixed Y", "Refactored Z"],
  "impact": "1-2 sentences about what this means for end users or the project",
  "technical_details": "Notable architecture decisions, tradeoffs, interesting technical aspects. Be specific enough that another developer would find this interesting.",
  "commit_or_pr": "Latest commit hash or PR URL",
  "repository": "Repo URL or working directory path"
}
  1. Pipe the JSON to the ship logger. Use a heredoc to preserve formatting:
cat <<'SHIP_JSON' | python3 ~/.claude/hooks/notion_ship_logger.py
<the-json-here>
SHIP_JSON
  1. Share the Notion page URL with the user after logging.

Guidelines

  • Write the summary in first person plural ("We built...", "We fixed...") for a build-in-public voice
  • Be specific: "Added OAuth2 with Google provider" not "Added authentication"
  • Include concrete details: file counts, performance numbers, specific tech choices
  • The summary should be interesting enough that a developer would want to read it
  • Don't be generic - every entry should feel unique to what was actually done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment