Imported from magnus919/agent-skills (
ghost/SKILL.md). Install upstream withnpx skills add magnus919/agent-skills --skill ghost. Copyright stays with the author (MIT).
ghost — Ghost CMS content from the terminal
Drive a Ghost CMS site's Admin API (v5/v6, Accept-Version: v6.0): list posts by status including drafts, create and publish pages and posts, manage tags, and check site info. Drafts, scheduled posts, and published content are all visible here because every call authenticates with a per-request Admin JWT built from your id:secret integration key.
Setup
export GHOST_URL="https://your-ghost-site.com"
export GHOST_ADMIN_KEY="<RECORD_KEY>" # id:secret from Ghost Admin → Integrations
- In Ghost Admin → Settings → Integrations, create (or open) a Custom Integration.
- Copy its Admin API Key — one string, two colon-separated hex halves (
id:secret). The separate Content API key from the same screen will NOT let you see drafts; see Known Gotchas. - At request time the CLI signs a short-lived JWT per call: HS256 signature keyed by the secret half after hex-decoding it to raw bytes,
kidheader carrying the id half, audience/admin/,expfive minutes afteriat, sent asAuthorization: Ghost <token>. You never handle the token yourself. --helpand--dry-runwork without credentials (lazy auth).
Essential commands
Inspect
ghost site # title, url, description, version
ghost get-post POST_ID # full record incl. exact updated_at for edits
Browse (intent: find content)
ghost posts # latest 20
ghost posts --status draft # unpublished work queue
ghost posts --status scheduled # what publishes next
ghost posts --limit 100 --page 2 # paginate (max page size 100)
ghost posts --order "updated_at desc" # SQL-style ordering
ghost pages # static pages
ghost tags # tags with usage counts
Create and publish
ghost create-post --title "Notes" # safe default: draft
ghost create-post --title "Hello" --html "<p>Hi</p>"
ghost create-post --title "Launch" --status published --html "<p>We're live</p>"
ghost create-post --title "Later" --status scheduled \
--published-at "2026-09-01T09:00:00.000Z" # future ISO-8601 required together
ghost create-page --title "About" --html "<p>…</p>" --slug about
ghost create-tag --name "Engineering" --description "Technical posts"
Edit and remove
ghost update-post POST_ID --title "New title" \
--updated-at "<RECORD_UPDATED_AT>" # REQUIRED: latest updated_at, re-read first
ghost update-post POST_ID --status published --updated-at "<RECORD_UPDATED_AT>"
ghost delete-post POST_ID # permanent, 204-style removal
Pipeline recipes
Draft now, publish after review
ghost --json create-post --title "Release notes" > /tmp/post.json
id=$(jq -r '.post.id // .post_id // empty' /tmp/post.json)
ghost get-post "$id" # read fresh updated_at
ghost update-post "$id" --status published \
--updated-at "<exact string from get-post output>"
Never fabricate updated_at; copy it verbatim from a fresh read or Ghost rejects the edit with HTTP 409 UpdateCollisionError.
Review queue across statuses
for s in draft scheduled; do
ghost posts --status "$s" --json | jq -r '.posts[] | "\(.status)\t\(.title)\t\(.slug)"'
done
Complete export
Loop pages by meta.pagination.next (surfaced as .page.next) instead of trusting totals:
page=1
while :; do
ghost posts --limit 100 --page "$page" --json > "/tmp/posts-$page.json"
jq -r '.posts[].id' "/tmp/posts-$page.json"
next=$(jq -r '.page.next // empty' "/tmp/posts-$page.json")
[ -z "$next" ] && break
page=$next; sleep 0.2
done
JSON output and jq
--json works before or after the subcommand:
ghost --json posts # same as: ghost posts --json
JSON shapes worth knowing:
- Lists emit
{"total", "page": {pagination}, "posts": [...]}; detail/create emit the resource under its noun (post,page,tag,site). - Pagination mirrors the API:
.page = {"page", "limit", "pages", "total", "next", "prev"};next/prevare numbers ornull. --dry-run --jsonemits the executed plan instead of results:{"dry_run": true, "method", "url", "params"/"json"}— preview the exact request before running it live.- Errors exit non-zero with the API's own message plus code on stderr; JSON mode never wraps errors in stdout JSON.
Global flags
| Flag | Effect |
|---|---|
--json |
Machine-readable JSON (position-independent) |
--dry-run |
Print the planned API call (method, URL, payload) without executing |
--quiet |
Suppress diagnostics |
--verbose |
Debug logging |
Known gotchas
- Drafts need the Admin plane. The public Content API (that key-as-query-param API) serves published posts only and hides drafts silently — no error, just absent, even with a perfectly valid key. Its filters like
status:draftare ignored rather than rejected. Everything this CLI does goes through the Admin API precisely so drafts and scheduled posts stay reachable. - Two keys, same integration screen. The Content key is browser-safe but read-only-public; the Admin key (
GHOST_ADMIN_KEY) signs mutations and reaches drafts. Never point scripts at the Content key and expect draft visibility. - Five-minute tokens. Each JWT lives at most 300 seconds (
exp ≤ iat + 300) and the verifier caps token age too, so long batch jobs must re-sign per request (the CLI does). Skewed clocks break signing windows; keep NTP healthy. - HS256 only, decoded-secret keying. Tokens signed with HS512 are refused ("invalid algorithm"); signing without first hex-decoding the secret half produces "valid-looking" garbage that 401s. The CLI handles both rules.
Authorization: Ghost, not Bearer.Bearerscheme answers 401INVALID_AUTH_HEADER.- Edits require collision guards. PUTs without the post's current
updated_atfail with 409; relation arrays (tags,authors) replace wholesale rather than merge. --htmlrequiressource=htmland stays lossy. Every write carrying anhtmlpayload (create-post, update-post, create-page) must send the?source=htmlquery flag — the CLI attaches it automatically, and dry-run plans show it inparams— or Ghost parses the body as mobiledoc/lexical. Even with the flag, conversion is lossy: send proper Lexical, or wrap fixed markup in HTML card comments.- Pagination caps at 100 since Ghost 6 removed
limit=all; oversized limits silently return ≤100 rows, so always loop bynext. - Deletion is permanent and takes effect on the public site immediately.
When to use
Use this skill whenever the task is content workflow against a running Ghost site: browsing or exporting posts, drafting, publishing, scheduling, tag upkeep, page creation, or diagnosing those flows (auth errors, pagination, missing drafts).
When not to use
Do not use it to install, host, or operate a Ghost server (ghost install, nginx/SSL/systemd setup, upgrades, backups) — that is Ghost's official npm ghost-cli site-management tool, unrelated despite the shared name. Not for other publishing platforms (WordPress, Hugo have their own tooling), not for theme development, and not for site configuration better done once in the Admin dashboard (staff accounts, membership tiers).
Reference files
| File | Use it for |
|---|---|
| references/admin-auth-and-basics.md | Full JWT signing walkthrough, key format, audience/expiry rules, auth error table |
| references/content-vs-admin-api.md | Choosing between planes; the draft-visibility trap; diagnostic checklist |
| references/posts-pages-tags-endpoints.md | Endpoint map, field semantics, pagination loop, error envelope |
| references/worked-recipes.md | Copy-paste workflows: draft→publish, exports, scheduling, triage |
| references/gotchas-field-guide.md | Symptom-first incident lookup for auth, editing, volume problems |
Scripts and prerequisites
scripts/ghost— executable Python CLI (stdlib + requests only). Flags above; lazy auth; structured logging.scripts/test_ghost.py— offline test suite (mocked HTTP, zero network).- Python 3.8+,
requests. Nothing listens, nothing installs; scope limited to one configured site viaGHOST_URL.