Social Media CLI: Schedule Posts from Your Terminal
wharf is PostWharf's social media CLI: one npm install, and you can publish or schedule a post to Instagram, TikTok, X, LinkedIn, YouTube, Facebook, Threads, Pinterest, Bluesky and Telegram from a terminal, a cron job or a CI pipeline.
It has zero dependencies and runs on Node 20 or later, signs in with a single workspace key, and writes to the same queue as the PostWharf dashboard, REST API and MCP server, so a post you queue from a shell is the same post your team sees in the browser. The CLI is part of every plan.
Start free to get the workspace key it needs. Opening an account takes no card, and the Starter plan costs nothing for its first 7 days.
Start freeInstall wharf
npm i -g postwharf wharf --version wharf help
The npm package is postwharf, and the command it puts on your path is wharf. wharf help prints the full usage, and when wharf turns something down it says what to change in a plain sentence rather than a status number.
Sign in with a workspace key
Every PostWharf workspace has one live key, and it starts with wf_live_. Copy it from the API keys screen in the dashboard, which is also where you rotate it if it ever ends up somewhere it should not. A key reaches every channel connected to its workspace and nothing outside it, so an agency running several brands keeps one key per brand. wharf takes the key in three ways, and only the last one needs a person at the keyboard:
# 1. From the environment: nothing is written to disk export POSTWHARF_API_KEY=wf_live_… # 2. Stored once without a prompt; piping keeps it out of shell history echo "$POSTWHARF_API_KEY" | wharf auth --key - # 3. Typed at a prompt wharf auth
The environment variable wins over a stored key, so the key a CI job is given takes precedence over any key saved on that machine. wharf auth writes the key to ~/.postwharf/config.json with file mode 600 and tests it straight away, printing authenticated with the number of channels it can see. Run it with no key and no terminal, as a script would, and it stops and tells you which flag to pass instead of waiting for input that never comes.
Before you queue anything, run wharf whoami. It shows the workspace and its plan, the posts used and left this month, whether file uploads are switched on, and every connected channel, with a warning beside any channel whose token is close to expiring.
Post and schedule
wharf post "Shipping today." --to x,linkedin wharf post "New reel" --to instagram,tiktok --media ./out/reel.mp4 --at "tue 9am" wharf post "Launch week starts now" --all cat caption.txt | wharf post --stdin --to linkedin,x
Every post says where it goes: --to with platforms or account handles separated by commas, or --all for every connected channel. A post with neither is refused, so a test line never reaches every account by accident.
--at accepts an ISO timestamp, "2026-11-03 09:00", or a weekday and a time such as "tue 9am" or "next monday 09:10". A 24-hour time means exactly that hour, and "tue 9am" typed on a Tuesday afternoon means the Tuesday after. A time written without an offset is read on the clock of the machine running wharf, so on a server or a CI runner whose time zone you have not checked, include the offset: --at "2026-11-03T09:00:00+04:00".
--media takes a path or an https address, and you can repeat it. Files go out in the order you type them, which on a carousel is the order people swipe through. A local jpg, jpeg, png, webp, gif, mp4, mov or webm file is uploaded for you, up to 25 MB for an image and 512 MB for a video, so nothing needs hosting first. An address you host yourself is checked once when it is registered, which turns a broken link into a refusal today instead of a failed post next week; --no-verify skips that check for a link that is not live yet. Put --alt "…" after a file to give it a description.
--text-for pinterest:"the short version" gives one network its own caption inside the same post. A long LinkedIn text and a 500-character pin stay one post, with one id to edit or cancel.
Check a batch before you queue it
wharf platforms wharf post "Same idea, two lengths" --to instagram,pinterest \ --text-for pinterest:"the short one" --dry-run wharf post "Day 3" --to x --idempotency-key sept-day03-x
wharf platforms prints what each network accepts, tightest first: its text limit, the lower limit a network applies once a file is attached where it has one, how many images and videos fit in one post, and whether the network publishes. Among the ten that publish, X at 280 characters and Bluesky at 300 are the tightest, so they usually decide how long a caption shared across networks can be. The character limits on every major network are collected on one page if you want them side by side.
--dry-run checks the post against every network the same way a real post is checked, then creates nothing and uploads nothing. It prints each channel's character count against its ceiling, names the tightest network and how many characters it leaves to spare, and shows how many posts would be left this month after it. A dry run does not count against your monthly posts.
--idempotency-key makes a second run safe. Give each post a key your script can generate again, up to 255 characters, and running the same command twice hands back the post the first run made instead of queueing a duplicate; wharf prints already queued under <key> — nothing was created. If a batch stops halfway, re-run the whole thing soon after and only the missing posts are created; keys are held for a retry window, not forever. Five scheduling mistakes that double-post or dead-post covers the failures this flag exists to stop.
Drafts, edits and cancelling
wharf draft "Half an idea" wharf drafts wharf publish post_abc123 --to linkedin --at "thu 14:00" wharf edit post_abc123 --at "fri 9am" wharf cancel post_abc123
wharf draft saves a post without sending it, and the channels can wait; a draft does not count against your monthly posts until it goes out. wharf drafts lists what is saved. wharf publish <post_id> sends a draft now or, with --at, queues it for later, and --to or --all on that command sets the channels for a draft saved without any. wharf edit <post_id> changes the text, time, channels or files of a draft or of a queued post that has not gone out, and leaves alone anything you do not name; --now removes the set time so the post goes as soon as it is in the queue, and --draft pulls a queued post back into your drafts. wharf cancel <post_id> takes a scheduled post out of the queue, or deletes a draft.
See what landed
wharf status --since 7d --metrics wharf events --limit 20 wharf analytics --days 7 --by post
wharf status lists recent posts with one line per network: the permalink once there is one, sent, no link yet while it is on its way, or a note that the network gives no link back. --metrics adds the figures the network reports under each delivery (views, reach, likes, comments, shares, saves). wharf events prints the delivery receipts. wharf analytics looks back 7 or 30 days, totalled per network or listed per post, with no cross-network total, since views on two networks come from two different audiences. Eight of the ten publishing networks report figures; Bluesky and Telegram do not, and wharf says so rather than showing a zero. For the wider question of proof, see how to verify that a social post really published.
Every command
| Command | What it does |
|---|---|
wharf auth | Saves a workspace key; --key skips the prompt, --key - reads the key from stdin |
wharf whoami | Workspace, plan, posts used and left this month, connected channels |
wharf platforms | Each network's text limit, files per post and whether it publishes |
wharf channels | Connected channels, each with its handle and id |
wharf post [text] | Publishes now, or schedules with --at |
wharf draft [text] | Saves a post without sending it |
wharf drafts | Lists saved drafts |
wharf edit <post_id> [text] | Changes a draft or a queued post before it goes out |
wharf publish <post_id> | Sends a draft now, or queues it with --at |
wharf status | Recent posts and the delivery state on each network |
wharf analytics | Figures for the last 7 or 30 days, by network or by post |
wharf cancel <post_id> | Pulls a scheduled post from the queue, or deletes a draft |
wharf events | Delivery receipts |
Every command except auth also takes --json for output a script can parse, and drafts, status, events and analytics take --limit, from 1 to 200 rows.
Run it from cron or CI
wharf is built to fail where a pipeline can see it. A refused command exits with code 1 and prints a sentence on stderr saying what to change, and wharf status exits with code 2 when any delivery in its window has failed, so a scheduled job goes red instead of passing quietly.
Store the key in your CI's secret settings and expose it to the job as POSTWHARF_API_KEY. Nothing prompts and no file is written:
# in a CI job with Node 20 or later and POSTWHARF_API_KEY set from a secret
npm i -g postwharf
wharf post --stdin --to linkedin --dry-run < release-post.txt
wharf post --stdin --to linkedin --idempotency-key "release-$RELEASE_TAG" < release-post.txt
The dry run fails the job before anything is queued if the text does not fit, and the idempotency key ties the post to the release, so re-running a failed job soon afterwards does not announce the same release twice. $RELEASE_TAG stands for whatever your pipeline calls the version it ships.
On a server, store the key once with wharf auth --key - as the user the cron job runs as, then schedule commands as you would any other:
# 08:00 every day: check the posts created in the last day; a failed delivery exits 2
0 8 * * * wharf status --since 1d || echo "A PostWharf delivery failed"
If cron cannot find wharf, give it the full path that which wharf prints. With --json, wharf status prints the deliveries for your script to judge rather than setting the exit code: each delivered row carries a sent field that is true when it went out, false when it did not, and null when wharf could not check.
Driving it from an AI agent
wharf needs no prompt once a key is set, refuses with a sentence that names the fix, and answers in JSON when asked, so an agent that can run terminal commands can use it the way a person would. Scheduling social posts from Claude Code in the terminal walks through that workflow from start to finish. If you would rather the agent call PostWharf as tools than as shell commands, the same workspace key connects it to the MCP server at https://mcp.postwharf.com/mcp; the pages for Claude Code and Codex show that setup, and the developer page puts the API, MCP and CLI side by side.
Networks you can post to from the CLI
Ten networks publish from wharf today. Reddit, Discord, Snapchat and WordPress are request-only: PostWharf cannot publish to them yet, so no channel is created for them until their connector is built, and their pages explain how to ask for it. Each publishing network's page carries its own examples and the rules that network enforces.
| Network | From wharf | Text limit (characters) | Reports figures | Page |
|---|---|---|---|---|
| Publishes | 2,200 | Yes | Instagram CLI | |
| TikTok | Publishes | 2,200 | Yes | TikTok CLI |
| X | Publishes | 280 | Yes | X CLI |
| Publishes | 3,000 | Yes | LinkedIn CLI | |
| YouTube | Publishes | 5,000 | Yes | YouTube CLI |
| Publishes | 63,206 | Yes | Facebook CLI | |
| Threads | Publishes | 500 | Yes | Threads CLI |
| Publishes | 500 | Yes | Pinterest CLI | |
| Bluesky | Publishes | 300 | No | Bluesky CLI |
| Telegram | Publishes | 4,096, or 1,024 with a file | No | Telegram CLI |
| On request | Request Reddit | |||
| Discord | On request | Request Discord | ||
| Snapchat | On request | Request Snapchat | ||
| WordPress | On request | Request WordPress |
The API checks these limits when a post is created, so an over-length post is refused at once rather than cut short on its way out. wharf platforms always prints the current figures.
Where wharf falls short
- Reddit, Discord, Snapchat and WordPress cannot be published to yet, from the CLI or from anywhere else in PostWharf.
- Bluesky and Telegram report no figures, so analytics stays empty for them.
- Analytics looks back 30 days at most, because networks stop updating a post's figures about a month after it goes out.
- It needs Node 20 or later on whichever machine runs it.
- One key reaches one workspace. Switching brands means switching keys, which an environment variable in front of a single command handles:
POSTWHARF_API_KEY=$OTHER_BRAND_KEY wharf status. - Files you upload are held while the post goes out and then cleared, so keep your originals.
What it costs
The CLI, the MCP server and the REST API come with every plan, and there is no limit on channels per workspace. Starter is $15 a month after its free week, or $10 a month billed yearly, for 200 scheduled posts a month in one workspace; the larger plans raise the post cap and the number of workspaces. Compare the plans.
Start free, copy your workspace key from the API keys screen, and send your first post from the terminal: the account needs no card, and Starter's first 7 days cost nothing.
Start free