Ghost Publisher
by @machinesofdesire
Publish markdown articles to any Ghost 5 CMS site. The first reference implementation of Publisher Interface v1 -- a CMS-agnostic contract for editorial work...
As a Python module (recommended for agents)
from publisher import GhostPublisherpub = GhostPublisher() # reads env vars
post_id = pub.createPost({
"title": "A quiet argument about obsolescence",
"excerpt": "Things that still run but no longer serve a purpose.",
"body_md": open("article.md").read(),
"tags": ["culture", "criticism"],
"author": "staff_1",
})
hosted = pub.uploadImage("https://cdn.example.com/hero.jpg", alt="Booth interior")
pub.updatePost(post_id, {"featured_image_url": hosted, "image_alt_text": "Booth interior"})
url = pub.publishPost(post_id, {"send_newsletter": True})
print(f"Live at: {url}")
All seven Publisher Interface v1 methods are available on the
GhostPublisher instance: createPost, updatePost, publishPost,
schedulePost, deletePost, uploadImage, getPost.
As a CLI
Full pipeline (most common)
python3 publisher.py create-publish article.md \
--title "Your Article Title" \
--excerpt "A 1-2 sentence summary for SEO and previews" \
--tags "ai,economics,analysis" \
--image-url "https://cdn.example.com/header.jpg" \
--image-alt "Description of the image" \
--upload-image \
--newsletter
Remove --newsletter to publish without emailing subscribers. Remove
--upload-image if the image URL is already on a CDN you trust and you
don't need Ghost's media store to host it.
Step-by-step
# 1. Create a draft (returns post_id)
python3 publisher.py create-draft --title "Title" --excerpt "Summary" --tags "tag1,tag2"2. Inject markdown body
python3 publisher.py update-content article.md3. Upload and attach a feature image
python3 publisher.py set-image https://cdn.example.com/hero.jpg --alt "Hero image" --upload4. Schedule it, or publish it now
python3 publisher.py schedule 2026-05-01T09:00:00Z
python3 publisher.py publish --newsletterOther interface methods
python3 publisher.py get
python3 publisher.py delete
python3 publisher.py upload-image ./local.jpg --alt "alt text"
Required environment variables
GHOST_URL -- your Ghost site URL, e.g. https://yoursite.com orhttps://yourpub.ghost.io
GHOST_ADMIN_API_KEY -- Admin API key from a Ghost custom integration.: To get your Admin API key
1. Go to Ghost Admin -> Settings -> Integrations
2. Click "Add custom integration"
3. Name it (e.g. "OpenClaw Publisher")
4. Copy the Admin API Key -- it is a 24-char key_id, a colon, and a 64-char hex secret (format: XXXXXXXXXXXXXXXXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX)
Optional config file (GHOST_PUBLISHER_CONFIG)
For non-secret settings (default author, agent-to-staff mapping, newsletter
ID, max image size), point GHOST_PUBLISHER_CONFIG at a JSON file:
{
"default_author_id": "ghost_staff_id",
"agent_author_map": {
"staff_1": "ghost_staff_id_1",
"staff_2": "ghost_staff_id_2"
},
"newsletter_id": "ghost_newsletter_id",
"max_image_size_mb": 2
}
The agent_author_map lets a caller pass {"author": "staff_1"} and have
the adapter resolve it to the right Ghost staff member without the caller
needing to know internal Ghost IDs. Omit the map and everything falls back
to default_author_id, or to the integration's default if that is also
unset.
Keep this file out of version control if it contains IDs you do not want public. No secret keys go in this file -- secrets come from env vars.
clawhub install ghost-publisher