Social Publish
Publish social media posts to connected platforms via the Nosy Pandas API from Claude Code.
Configuration
Configuration is stored in ~/.pandas as simple key=value pairs:
api_url=https://nosypandas.com/api
api_key=your-key-here
media_folder=~/social-media
If this file doesn't exist or is missing values, the skill will walk the user through setup:
- Ask them to paste their API key (generated from the dashboard under API Keys)
- Write
~/.pandasautomatically
The user can also just paste their API key directly in chat at any time — the skill should detect it and offer to save it.
media_folder is optional and defaults to ~/social-media/.
Reading Configuration
# Read config values (use these instead of env vars in all curl commands)
PANDAS_API_URL="https://nosypandas.com/api"
PANDAS_API_KEY=$(grep '^api_key=' ~/.pandas 2>/dev/null | cut -d= -f2-)
PANDAS_MEDIA_FOLDER=$(grep '^media_folder=' ~/.pandas 2>/dev/null | cut -d= -f2-)
PANDAS_MEDIA_FOLDER="${PANDAS_MEDIA_FOLDER:-$HOME/social-media}"
Available Commands
| Command | Description |
|---|---|
| Publish | Create and publish a post to one or more platforms |
| History | View recent posts and their statuses |
| Detail | Check a specific post's full details |
| Retry | Retry a failed post |
| Delete | Delete a scheduled or failed post |
Publish Flow
Follow these steps in order:
Step 0: Permission Setup (first run only)
Check if the user's Claude Code permissions already include the skill's bash patterns:
grep -q "nosypandas.com" ~/.claude/settings.json 2>/dev/null && echo "CONFIGURED" || echo "NOT_CONFIGURED"
If CONFIGURED, skip to Step 1.
If NOT_CONFIGURED, explain to the user:
Quick setup: This skill runs bash commands to call the Nosy Pandas API, read your config, scan for media files, and move them after posting. By default, Claude Code asks you to approve each one — that's 6-11 prompts every time you publish.
I can add permission patterns to your Claude Code settings (
~/.claude/settings.json) so these commands auto-approve. The patterns are scoped narrowly:
- API calls only to
nosypandas.com- Config reads/writes only to
~/.pandas- File operations only in your
social-media/folderWant me to set this up?
If the user agrees, read ~/.claude/settings.json, merge these patterns into the permissions.allow array (avoiding duplicates), and write it back:
"Bash(cat > ~/.pandas *)",
"Bash(chmod 600 ~/.pandas)",
"Bash(grep * ~/.pandas *)",
"Bash(find * social-media *)",
"Bash(curl * https://nosypandas.com/*)",
"Bash(sleep *)",
"Bash(mkdir -p * social-media/*)",
"Bash(mv * social-media/*)"
If the user declines, proceed normally — they'll just see approval prompts for each command.
Step 1: Verify Configuration & Fetch Accounts
Read ~/.pandas and check that api_key is present.
If the file doesn't exist or is missing api_key:
- Ask: "Paste your API key" (tell them to generate one from the dashboard under API Keys)
- Write the values to
~/.pandas:
cat > ~/.pandas << 'EOF'
api_url=https://nosypandas.com/api
api_key=the-pasted-key
media_folder=~/social-media
EOF
chmod 600 ~/.pandas
If the user pastes what looks like an API key without being asked, detect it and offer to save it to ~/.pandas.
Once config is confirmed, read config and fetch accounts in a single call:
PANDAS_API_KEY=$(grep '^api_key=' ~/.pandas 2>/dev/null | cut -d= -f2-) && \
curl -s "https://nosypandas.com/api/accounts" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json"
Response:
{
"accounts": [
{ "id": 1, "platform": "twitter", "account_name": "@myhandle" },
{ "id": 2, "platform": "bluesky", "account_name": "@me.bsky.social" }
]
}
If no accounts are returned, tell the user to connect accounts via the web dashboard.
Step 3: Select Platforms
Show the user their connected accounts as a numbered list and ask which to post to:
Which platforms do you want to post to?
- Twitter/X (@myhandle)
- Bluesky (@me.bsky.social)
- LinkedIn (My Profile)
After selection, show the character limits and media requirements for the selected platforms using the Platform Reference Table below.
Step 4: Ask for Content
Ask: "What do you want to post?"
Show character limits for each selected platform so the user knows constraints before writing. If content exceeds a platform's limit and that platform supports threading (Twitter, Threads, Bluesky), note the content will be auto-split into a thread.
If content exceeds LinkedIn's 3000 character hard limit, warn the user and ask them to shorten it.
Step 5: Platform-Specific Fields
- If YouTube is selected, ask: "What's the video title?" (required, max 500 chars)
- If Pinterest is selected, ask: "What's the destination URL for this pin?" (required, must be a valid URL)
Step 6: Media Selection
Scan the configured media folder using find:
MEDIA_DIR="${PANDAS_MEDIA_FOLDER:-$HOME/social-media}"
find "$MEDIA_DIR" -maxdepth 1 -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.webm" \) 2>/dev/null
If files are found, list them (distinguishing images vs videos by extension) and ask which to attach. If no files found, ask: "Do you want to attach any media? Provide a file path, or skip."
Important checks before proceeding:
- If a selected platform requires media (Instagram, YouTube, Pinterest, TikTok) and no media is attached, warn the user and ask them to provide media.
- If a selected platform requires video (YouTube, TikTok) and only images are attached, warn the user that video is required.
- Check media counts against the Platform Reference Table limits below and warn about any violations.
- If a platform has
noMix: trueand both images and videos are selected, warn the user. - Accepted file types: jpg, jpeg, png, gif, webp (images); mp4, mov, avi, webm (videos).
Step 7: Schedule or Publish Now
Ask: "Publish now or schedule for later?"
If scheduling, ask for the date and time. Accept natural language like "tomorrow at 9am". Auto-detect the system timezone using:
readlink /etc/localtime | sed 's|.*/zoneinfo/||'
Use this value for the "timezone" field in the API payload. Do not ask the user for their timezone.
Step 8: Confirmation Summary
Show a summary with any applicable warnings:
Content: [first 100 chars...]
Media: [count] files ([list filenames])
Platforms: Twitter, Bluesky
Timing: Publish now
Title: [if YouTube]
Link URL: [if Pinterest]
Warnings:
- Twitter: Content will be split into a thread (exceeds 280 chars)
- Threads: Posts take a few minutes to fully process on Meta's platform — the skill will show "Submitted" after posting. Use the Detail command to confirm the final status.
Ask: "Look good? (yes/no)"
Step 9: Execute Post
This step has two parts: stage media (if any), then create the post.
Step 9a: Stage Media, Create Post & Check Status
Run all of media staging, post creation, and status checking in a single bash call. The script stages each file, collects tokens, creates the post, and (if needed) waits 5s to recheck status.
For posts with media:
PANDAS_API_KEY=$(grep '^api_key=' ~/.pandas 2>/dev/null | cut -d= -f2-) && \
PANDAS_API_URL="https://nosypandas.com/api" && \
TOKENS="" && \
for FILE in /path/to/file1.jpg /path/to/file2.png; do \
RESULT=$(curl -s -X POST "$PANDAS_API_URL/media/stage" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json" \
-F "file=@$FILE") && \
T=$(echo "$RESULT" | jq -r '.token') && \
TOKENS="${TOKENS:+$TOKENS,}\"$T\""; \
done && \
POST_RESULT=$(curl -s -X POST "$PANDAS_API_URL/posts" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d "{
\"content\": \"POST_CONTENT\",
\"platforms\": [ACCOUNT_ID_1, ACCOUNT_ID_2],
\"publish_now\": true,
\"media_tokens\": [$TOKENS]
}") && \
echo "$POST_RESULT" && \
POST_ID=$(echo "$POST_RESULT" | jq -r '.post.id') && \
HAS_PENDING=$(echo "$POST_RESULT" | jq '[.post.platforms[] | select(.status == "pending" or .status == "publishing" or (.status == "failed" and (.error == null or .error == "" or .error == "Publishing failed (no details provided by platform)")))] | length') && \
if [ "$HAS_PENDING" -gt 0 ]; then \
sleep 5 && \
curl -s "$PANDAS_API_URL/posts/$POST_ID" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json"; \
fi
For posts without media:
PANDAS_API_KEY=$(grep '^api_key=' ~/.pandas 2>/dev/null | cut -d= -f2-) && \
PANDAS_API_URL="https://nosypandas.com/api" && \
POST_RESULT=$(curl -s -X POST "$PANDAS_API_URL/posts" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"content": "POST_CONTENT",
"platforms": [ACCOUNT_ID_1, ACCOUNT_ID_2],
"publish_now": true
}') && \
echo "$POST_RESULT" && \
POST_ID=$(echo "$POST_RESULT" | jq -r '.post.id') && \
HAS_PENDING=$(echo "$POST_RESULT" | jq '[.post.platforms[] | select(.status == "pending" or .status == "publishing" or (.status == "failed" and (.error == null or .error == "" or .error == "Publishing failed (no details provided by platform)")))] | length') && \
if [ "$HAS_PENDING" -gt 0 ]; then \
sleep 5 && \
curl -s "$PANDAS_API_URL/posts/$POST_ID" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json"; \
fi
Optional fields (add to the JSON when applicable):
"title": "VIDEO_TITLE"— for YouTube"link_url": "https://example.com"— for Pinterest"scheduled_at": "2026-03-16T09:00:00Z"— for scheduled posts (omitpublish_nowor set tofalse)"timezone": "America/New_York"— timezone for scheduled posts
Stage response (201):
{
"token": "stg_abc123...",
"original_filename": "photo.jpg",
"type": "image",
"expires_at": "2026-03-16T14:00:00+00:00"
}
Tokens are valid for 2 hours.
Post response (201):
{
"post": {
"id": 42,
"content": "Hello world!",
"status": "published",
"platforms": [
{ "platform": "twitter", "status": "published", "url": "https://x.com/..." },
{ "platform": "bluesky", "status": "published", "url": "https://bsky.app/..." }
]
}
}
Some platforms (especially Threads) take time to process. The script automatically does one 5-second recheck if any platform has a non-terminal status (pending, publishing, or failed with no error details). The generic error string "Publishing failed (no details provided by platform)" also counts as non-terminal — it means the platform is still processing, not that it actually failed. Do not add additional retries — the server handles final status updates via background jobs and webhooks.
Step 10: Display Results
Show the result per platform using these rules, evaluated in order:
- Published — status is
published: "[Platform]: Published — [url]" - Pending/Processing — status is
pendingorpublishing: "[Platform]: Pending — check back shortly" - Threads submitted (not a real failure) — platform is
threads, status isfailed, AND error is null, empty, OR"Publishing failed (no details provided by platform)": "Threads: Submitted — this platform takes a few minutes to fully process. Use the Detail command to confirm the final status." - Failed with real error — status is
failedwith a specific, non-generic error message: "[Platform]: Failed — [error message]"
Critical rule: NEVER display "Failed" for Threads if the error is null, empty, or the generic string "Publishing failed (no details provided by platform)". Only show "Failed" for Threads when there is a specific, actionable error (e.g., "Account not found", "Content policy violation").
Step 11: Move Media
After successful posting, move all used media files to a posted/ subfolder in a single call:
MEDIA_DIR="${PANDAS_MEDIA_FOLDER:-$HOME/social-media}" && \
mkdir -p "$MEDIA_DIR/posted/" && \
mv "$MEDIA_DIR/file1.jpg" "$MEDIA_DIR/file2.png" "$MEDIA_DIR/posted/"
List all used files as arguments to a single mv command.
Post History Flow
Fetch recent posts:
curl -s "$PANDAS_API_URL/posts" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json"
Returns paginated posts with platform statuses. Display as a table showing post ID, content preview, status, and platform results.
Post Detail Flow
Check a specific post's full details:
curl -s "$PANDAS_API_URL/posts/POST_ID" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json"
Response:
{
"post": {
"id": 42,
"content": "Hello world!",
"title": null,
"status": "published",
"scheduled_at": null,
"created_at": "2026-03-15T12:00:00Z",
"platforms": [
{ "platform": "twitter", "status": "published", "url": "https://x.com/...", "error": null }
],
"media": [
{ "url": "https://cdn.example.com/file.jpg", "type": "image", "filename": "photo.jpg" }
]
}
}
Retry Flow
Retry a failed post:
curl -s -X POST "$PANDAS_API_URL/posts/POST_ID/retry" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json"
Response:
{
"post": {
"id": 42,
"status": "pending"
}
}
Only posts with failed status can be retried. If the post is not in a failed state, the API will return a 404.
Delete Flow
Delete a scheduled or failed post. Only posts with scheduled or failed status can be deleted — published posts cannot be removed.
Step 1: Identify the Post
Use the Detail flow to fetch the post and confirm its status is scheduled or failed. Show the post summary to the user and ask for confirmation:
Are you sure you want to delete this post? Content: [first 100 chars...] Status: scheduled Platforms: Twitter, Bluesky
Step 2: Send Delete Request
curl -s -X DELETE "$PANDAS_API_URL/posts/POST_ID" \
-H "Authorization: Bearer $PANDAS_API_KEY" \
-H "Accept: application/json"
Response (200):
{
"message": "Post deleted."
}
Step 3: Display Confirmation
Show: "Post #POST_ID has been deleted."
If the API returns 404, the post either doesn't exist or is in a non-deletable status (e.g., published). Inform the user accordingly.
Platform Reference Table
| Platform | Max Content | Threading | Requires Media | Requires Video | Max Images | Max Videos | No Mix |
|---|---|---|---|---|---|---|---|
| Twitter/X | 280 chars | Yes | No | No | 4 | 1 | Yes |
| Bluesky | 300 chars | Yes | No | No | 4 | 1 | No |
| Threads | 500 chars | Yes | No | No | 10 | 1 | No |
| None | No | Yes | No | 10 | 1 | No | |
| 3000 chars | No | No | No | 20 | 1 | No | |
| YouTube | None | No | Yes | Yes | 0 | 1 | No |
| None | No | Yes | No | 1 | 1 | Yes | |
| TikTok | None | No | Yes | Yes | 35 | 1 | Yes |
Notes:
- "Threading" means content exceeding the character limit is auto-split into a thread.
- "No Mix" means the platform does not allow images and videos in the same post.
- YouTube requires a
titlefield (max 500 chars). Pinterest requires alink_urlfield. - YouTube and TikTok require at least one video file — images alone will be rejected.
- Accepted file types: jpg, jpeg, png, gif, webp (images); mp4, mov, avi, webm (videos).
Error Handling
| Status | Meaning | Recovery |
|---|---|---|
| 401 | Invalid API key | Check api_key in ~/.pandas is correct |
| 403 | Subscription required | Subscribe at the dashboard |
| 404 | Post not found or not retryable | Verify the post ID and that its status is failed |
| 422 | Validation error | Show the specific error messages from the response body and help the user fix them |
| 500 | Server error | Try again later |