agentleFS
ciberjohnciberjohn/Hermes-Skills/social-poster/SKILL.md

social-poster

Skill17 starsChanged 2 months agoMITView on GitHub

Direct OAuth posting to social media — no Docker, no database, no redirect URI whitelisting. Generates OAuth URLs, exchanges user codes/PINs, stores tokens in a local vault, and posts via direct platform API calls. Supports 15 platforms across OAuth, webhook, and token-based auth.

  • Reads credentials

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 5 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Run python3 ~/.social-poster/social-poster.py vault:status to check which platforms have stored tokens

    line 60
  2. Distinctive:

    Check the vault that the requested platforms have tokens

    line 83
  3. Distinctive:

    Use the platform-specific API to post

    line 84
  4. Distinctive:

    Create a Hermes cron job that calls the posting function at the specified time

    line 91
  5. Very specific:

    Use cronjob action='create' with the posting instructions

    line 92
  6. Very specific:

    All platforms support environment variable fallback when config.json is absent.

    line 139
  7. Distinctive:

    When the agent is on a remote machine and the user is on a different machine (e.g., their desktop "Kratos"), OAuth redirect URLs need to …

    line 174
  8. Distinctive:

    When posting, respect per-platform limits:

    line 218

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Social Poster — Direct OAuth Posting

Overview

AttributeValue
Total platforms14
OAuth platformsX, LinkedIn, Instagram, Mastodon, Twitch, Reddit, Threads, Facebook, YouTube
Webhook/TokenDiscord, Slack, Telegram, GitHub
DirectBluesky
AuthOAuth 1.0a, OAuth 2.0, App Passwords, Webhooks, PATs

User Interaction Flow

1. "What networks can you post to?"

The agent should:

  1. Run python3 ~/.social-poster/social-poster.py vault:status to check which platforms have stored tokens
  2. Report the full list of 14 platforms, marking connected ones with ✅ and the rest as ⬜
  3. Offer: "Want me to guide you through setting up any of them?"

Example response:

14 platforms supported. Currently connected:
  ✅ X, LinkedIn, Bluesky
  ⬜ Instagram, Mastodon, Twitch, Reddit, Discord, Slack, Telegram, GitHub, Threads, Facebook, YouTube
Say 'connect [platform]' and I'll walk you through it."

2. "Connect [platform]" or "How do I set up [platform]?"

The agent should:

  1. Load the platform's setup guide from references/platform-setup-guides.md
  2. Walk the user through the numbered steps, one at a time
  3. For OAuth platforms: ask for credentials, save to config.json, generate auth URL, exchange code
  4. For webhook/token platforms: ask for the URL/token, save to config.json, verify it works

3. "Post this to [platforms]: [content]"

The agent should:

  1. Check the vault that the requested platforms have tokens
  2. Use the platform-specific API to post
  3. Report success/failure per platform
  4. Respect per-platform character limits (see Platform Character Limits below)

4. "Schedule this for [time]"

The agent should:

  1. Create a Hermes cron job that calls the posting function at the specified time
  2. Use cronjob action='create' with the posting instructions

Architecture

{{SOCIAL_POSTER_DIR}}/
├── config.json              — API credentials (key/secret per platform)
├── vault.json               — User access/refresh tokens
├── social-poster.py         — Unified CLI (vault status, auth URL gen, token exchange)
├── webhook-poster.py        — Webhook/Token poster (Discord, Slack, Telegram, GitHub)
├── auth-x-step1.py          — X OAuth step 1 (request token + auth URL)
├── auth-x-step2.py          — X OAuth step 2 (exchange PIN for access token)
├── oauth-callback-server.py — Callback HTTP server (Tailscale Serve)
└── last_code.txt            — Latest OAuth code captured (auto-generated)

Platform Setup Guides

For step-by-step instructions on creating developer apps and getting credentials, load references/platform-setup-guides.md. Each platform has:

  • Exact URLs to visit
  • Buttons to click and fields to fill
  • Where to find credentials
  • How the auth flow works

Quick Reference: Auth Method Summary

PlatformAuth MethodWhat User NeedsConfig Keys
XOAuth 1.0a PINAPI Key + Secretx.api_key, x.api_secret
LinkedInOAuth 2.0Client ID + Secretlinkedin.client_id, linkedin.client_secret
BlueskyApp passwordHandle + App Passwordbluesky.handle, bluesky.app_password
InstagramOAuth 2.0App ID + Secretinstagram.app_id, instagram.app_secret
MastodonOAuth 2.0Instance + Client ID/Secretmastodon.instance, mastodon.client_id, mastodon.client_secret
TwitchOAuth 2.0Client ID + Secrettwitch.client_id, twitch.client_secret
RedditOAuth 2.0Client ID + Secretreddit.client_id, reddit.client_secret
ThreadsOAuth 2.0App ID + Secretthreads.app_id, threads.app_secret
FacebookOAuth 2.0App ID + Secretfacebook.app_id, facebook.app_secret
YouTubeOAuth 2.0Client ID + Secretyoutube.client_id, youtube.client_secret
DiscordWebhookWebhook URLdiscord.webhook_url
SlackWebhookWebhook URLslack.webhook_url
TelegramBot tokenToken + Chat IDtelegram.bot_token, telegram.chat_id
GitHubPATPAT + Repogithub.pat, github.repo

Environment Variables

All platforms support environment variable fallback when config.json is absent. This is useful for CI/CD or ephemeral environments:

Env VarConfig Key
X_API_KEYx.api_key
X_API_SECRETx.api_secret
LINKEDIN_CLIENT_IDlinkedin.client_id
LINKEDIN_CLIENT_SECRETlinkedin.client_secret
INSTAGRAM_APP_IDinstagram.app_id
INSTAGRAM_APP_SECRETinstagram.app_secret
BLUESKY_HANDLEbluesky.handle
BLUESKY_APP_PASSWORDbluesky.app_password
MASTODON_INSTANCEmastodon.instance
MASTODON_CLIENT_IDmastodon.client_id
MASTODON_CLIENT_SECRETmastodon.client_secret
TWITCH_CLIENT_IDtwitch.client_id
TWITCH_CLIENT_SECRETtwitch.client_secret
REDDIT_CLIENT_IDreddit.client_id
REDDIT_CLIENT_SECRETreddit.client_secret
THREADS_APP_IDthreads.app_id
THREADS_APP_SECRETthreads.app_secret
FACEBOOK_APP_IDfacebook.app_id
FACEBOOK_APP_SECRETfacebook.app_secret
YOUTUBE_CLIENT_IDyoutube.client_id
YOUTUBE_CLIENT_SECRETyoutube.client_secret
DISCORD_WEBHOOK_URLdiscord.webhook_url
SLACK_WEBHOOK_URLslack.webhook_url
TELEGRAM_BOT_TOKENtelegram.bot_token
TELEGRAM_CHAT_IDtelegram.chat_id
GITHUB_PATgithub.pat
GITHUB_REPOgithub.repo

Cross-Machine OAuth (Reverse Proxy Required)

When the agent is on a remote machine and the user is on a different machine (e.g., their desktop "Kratos"), OAuth redirect URLs need to point somewhere the user's browser can reach.

You need a reverse proxy that makes a local port accessible via HTTPS at a URL you can reach from your browser. Options include:

  • Tailscale Serve — HTTPS on your tailnet, no public internet needed
  • Cloudflare Tunnel (cloudflared) — public or private tunnels
  • Nginx Proxy Manager — if you have a domain pointing to your VPS
  • ngrok — quick public URL for testing
  • Your VPS hostname — if it already has HTTPS

The URL does NOT need to be public on the internet — it just needs to be reachable by YOUR browser.

Setup (one-time)

# 1. Start the callback server (persistent, port 19876)
python3 ~/.social-poster/oauth-callback-server.py 19876 &

# 2. Use your reverse proxy to route paths to 127.0.0.1:19876
#    Example with Tailscale: tailscale serve --bg --set-path /oauth-callback 19876
#    Example with Cloudflare: cloudflared tunnel --url http://localhost:19876
#    Example with nginx: proxy_pass http://127.0.0.1:19876 in your site config

# The paths you need depend on which platforms you use:
# - /oauth-callback                          # For Mastodon, Twitch, Reddit, etc.
# - /integrations/social/linkedin            # If using LinkedIn
# - /integrations/social/instagram            # If using Instagram

How the Callback Server Works

The Python HTTP server (oauth-callback-server.py) listens on 127.0.0.1:19876. Your reverse proxy forwards HTTPS requests to it. When an OAuth redirect arrives with ?code=..., the server saves the code to {{SOCIAL_POSTER_DIR}}/last_code.txt.

The agent then reads the code and exchanges it for tokens. The server stays alive between sessions and handles any number of callbacks.

Platform Character Limits

When posting, respect per-platform limits:

PlatformMax LengthNotes
X/Twitter (non-premium)280 charsRequires who_can_reply_post: everyone
X/Twitter (premium)25,000 charsSame
Bluesky300 charsReturns 400 if exceeded
LinkedIn3,000 charsProfessional tone
Facebook63,206 charsLink for best reach
Instagram2,200 charsImage/video required
Threads500 charsShort and punchy
YouTube5,000 charsDescription only
Mastodon500 charsInstance-dependent
TwitchN/AChannel announcements
Reddit40,000 charsTitle max 300
Discord2,000 charsEmbeds supported
Slack40,000 charsBlock kit
Telegram4,096 charsHTML/Markdown
GitHubN/AIssues/gists

Pitfalls

  • Never use os.path.expanduser("~") directly in Hermes sessions — it resolves to the sandbox home. Scripts auto-detect and use pwd.getpwuid().
  • config.json and vault.json must be chmod 600 — NEVER committed to git.
  • LinkedIn scopes must NOT include org scopes unless the app has Community Management API.
  • LinkedIn MUST NOT use prompt=none for first-time authorization.
  • X/Twitter callback must be oob for PIN-based flow.
  • Instagram app MUST be Live (not Development) for OAuth.
  • Bluesky uses app passwords (format: xxxx-xxxx-xxxx-xxxx), not regular passwords.
  • Redirect URI MUST match exactly between auth URL and token exchange POST.
  • state parameter is REQUIRED for all OAuth 2.0 flows — built into all generators.
  • post.py is text-only — any post carrying a picture needs scripts/post_with_image.py (same vault/config, per-platform copy from <dir>/<platform>.md). Verified upload recipes for X, LinkedIn, Bluesky, Mastodon and Discord are in references/image-posts.md.
  • LinkedIn /rest/posts returns 500 for every active version on this app (202601-202608, 2026-09-15); post images via /v2/assets?action=registerUpload + /v2/ugcPosts with shareMediaCategory: IMAGE. A urn:li:image: URN from /rest/images in a ugcPost fails as "not owned by the author".
  • X returns 402 credits depleted regardless of code correctness once the Pay-Per-Use balance hits zero. Auth and media upload still succeed, so diagnose in that order before touching the code.
  • Platform setup guides may drift as developer portals update their UIs. If a user reports a step doesn't match, verify at the official developer portal URL and update the guide.
  • Discord's edge returns 403 to urllib's default User-Agent (Python-urllib/3.x) while accepting any explicit one. A health probe that sends no User-Agent therefore reports a perfectly live webhook as "deleted/invalid" and sends you off to recreate something that was never broken. post.py sends User-Agent: SocialPoster/1.0 and works fine. Set an explicit agent on every Discord-touching request, and before acting on a discord FAIL, confirm with an explicit-agent GET or a real POST rather than trusting the 403.
  • Store a Discord webhook URL without ?wait=true. post_discord treats any non-empty response body as failure, so ?wait=true (which makes Discord return the created message object instead of an empty 204) turns every successful post into a reported failure. If a Discord result looks like a failure, read the channel back before believing it.
  • A token can expire with no refresh path. An OAuth app may return an access token with a long lifetime and an empty refresh_token if programmatic refresh is not enabled on it. Record the expiry timestamp when you store the token and watch it, or the platform dies silently on a known date. A daily platform-state watchdog that alerts on change (including days-remaining) turns that into an advance warning.

References

  • references/platform-setup-guides.md — Step-by-step setup guides for all 15 platforms
  • references/platform-expansion.md — Full roadmap and implementation notes
  • references/oauth-flows.md — OAuth implementation details and token exchange reference

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.