Cross post video to TikTok, Reels and Shorts from your agent: Cross Post Video Skill is a free, MIT-licensed skill for Claude Code that takes one finished video and gets it ready for all three. It checks the file against every platform before anything is uploaded, helps you pick a cover, has the agent write a separate text for each platform, asks the settings only the account owner may choose, and shows every post on one plan page. After one clear yes, it sends all accounts in one request with a child per account. It also works in Codex, Cursor and OpenClaw.

Everything up to the upload runs on your own computer with Python and ffmpeg, and works without signing up for anything. This post walks through the NASA example in the repo, one command at a time.

npx skills add social-media-skills/cross-post-video-skill -g -a claude-code -y 

Repo: github.com/social-media-skills/cross-post-video-skill

The plan page for one NASA clip: the cover, then a card each for TikTok, Instagram Reels and YouTube Shorts with its own text, character count and settings

The plan page from the example: a 14-second NASA clip about why astronauts exercise in space, planned for TikTok, Reels and Shorts (NASA footage, no endorsement implied).

Why one video breaks on TikTok, Reels or Shorts

One vertical MP4 can be valid on all three platforms. What differs is everything around it, and each of these is easy to find only after the upload:

  • Length. TikTok's maximum belongs to the account, not the platform. A Short is square or vertical and up to 3 minutes; longer and YouTube makes it a normal video.
  • The file itself. Instagram is the strictest: 300 MB, up to 1920 px wide, 23 to 60 fps, AAC sound, the index at the start of the file, and no HDR.
  • The text. TikTok and Instagram take a caption. YouTube wants a title of up to 100 characters and counts the description in bytes. Instagram caps hashtags at 5.
  • The settings. TikTok makes the account owner choose who can see each post and whether comments, duets and stitches are on, with no defaults. YouTube asks whether a video is made for kids.

The skill moves all of that in front of the upload. Scripts do the checking and counting; the agent writes the texts and asks the questions.

The prompt

Post clips/microgravity.mp4 to my TikTok, Reels and Shorts on 12 January at 6 pm New York time. It's my own video with the original sound, no AI, not promotional. TikTok: everyone can see it, comments on, duets and stitches off. Not made for kids.

That prompt already answers what the agent would otherwise ask in its first message: is this the original export you have the right to post, what the sound is, whether it is AI-made or promotional, and the settings per account.

Step 1: check the file

python3 <skill>/scripts/check_video.py clips/microgravity.mp4 
clips/microgravity.mp4: 720x1280, 13.9 s, 29.97 fps, h264 yuv420p, aac 48000 Hz, 3.1 MB, 1.82 Mbps TikTok: ok Instagram Reels: ok YouTube Shorts: ok 

This clip passes. When a file doesn't (4K, 120 fps, 5.1 sound, a MOV from a camera, HDR from a newer iPhone), --fix writes a re-encoded copy next to it as <name>.crosspost.mp4 and leaves the original alone. It keeps the picture size and only scales down a video over 1920 px wide. HDR is converted to standard colour when your ffmpeg has the zscale filter; when it doesn't, --fix writes nothing and tells you how to export a standard-colour file instead. It never trims, crops, pads or speeds up a video: length and shape stay your call.

Step 2: pick a cover

Without a cover, TikTok and Instagram show the first frame. cover.py makes a sheet of 12 frames with their times, with a dashed box on each for the middle 3:4, the part Instagram's profile grid shows.

python3 <skill>/scripts/cover.py clips/microgravity.mp4 

Twelve frames of the clip with their times; a dashed box on each marks the middle 3:4 that Instagram's profile grid shows

In the example, the frame at 8.7 seconds looked good full screen and cut the astronaut's head off in the grid crop. The frame at 12.2 seconds didn't:

python3 <skill>/scripts/cover.py clips/microgravity.mp4 --at 12.2 

The chosen cover full screen, and next to it the 3:4 crop the profile grid shows

The cover goes to TikTok and Instagram with the post. YouTube's cover is set in the YouTube app, so the plan and the by-hand page both say "Cover frame: 12.2 s" and you pick the same frame there.

Step 3: a text per platform, in one job file

The agent writes each text from what the video says, not from the other texts, and puts everything in crosspost.json: the accounts with their settings, the four answers, and the texts. Trimmed:

{ "timezone": "America/New_York", "accounts": [ {"name": "TikTok", "platform": "TIKTOK", "privacyLevel": "PUBLIC_TO_EVERYONE", "allowComment": true, "allowDuet": false, "allowStitch": false, "max_video_s": 600}, {"name": "Reels", "platform": "INSTAGRAM"}, {"name": "Shorts", "platform": "YOUTUBE", "privacy": "public", "madeForKids": false} ], "videos": [{ "id": "microgravity", "file": "clips/microgravity.mp4", "when": "2027-01-12 18:00", "own_video": true, "audio": "original", "ai_generated": false, "promotional": "no", "text": { "TIKTOK": "Why astronauts work out for hours every day\n...", "INSTAGRAM": "Astronauts on the space station exercise for hours every day.\n...", "YOUTUBE": {"title": "Why astronauts exercise for hours every day in space", "description": "...", "tags": ["space station", "ISS", "microgravity"]} } }] } 

The settings and the four answers are the owner's. If one is missing, plan.py lists it and the agent asks; it never fills one in. max_video_s comes from the connected TikTok account itself, because TikTok sets it per account.

Step 4: the plan page

python3 <skill>/scripts/plan.py crosspost.json 
3 posts planned -> plan.md, plan.html (0 errors, 0 warnings, 0 notes) by hand (1): by-hand/ requests: requests/microgravity.json next: show plan.html (or paste plan.md) and wait for a clear yes. ... 

plan.py checks the file per account, every text and every setting, and counts each text the way its platform does. For the example: the TikTok caption is 198 of 2,200 characters with 3 hashtags, the Reels caption 329 of 2,200 with 4, the YouTube title 52 of 100 characters and the description 227 of 5,000 bytes. It also flags the things that aren't errors but are worth knowing: a link that won't be clickable, an @handle reused on another platform, a first line too long to stand alone, a hashtag that says nothing about the video.

The result is plan.html (the screenshot at the top) and plan.md: every account, its text, settings, cover and time, plus a list of what to do in the apps after it's live. The agent shows it and waits for one yes that covers these posts, these accounts and these times.

In the example only TikTok and Instagram are connected, so Shorts is planned by hand. by-hand/microgravity/shorts.md has the title, description, tags, settings, the cover frame and numbered steps for YouTube Studio or the app.

Step 5: one request, a child per account

After the yes, the agent uploads each file once, and plan.py --media media.json fills in the media IDs. The request is one post with a child per connected account, each with its own text in contentOverride. Trimmed from requests/microgravity.json:

{ "schedule": {"type": "SCHEDULE_FOR_LATER", "scheduledFor": "2027-01-12T23:00:00Z"}, "socialAccounts": [ {"platform": "TIKTOK", "postMode": "DIRECT_POST", "privacyLevel": "PUBLIC_TO_EVERYONE", "allowComment": true, "allowDuet": false, "allowStitch": false, "isYourBrand": false, "isBrandedContent": false, "isAiGeneratedContent": false, "cover": {"type": "MEDIA_LIBRARY", "mediaId": "<cover>"}, "contentOverride": [{"text": "Why astronauts work out for hours every day\n...", "media": [{"type": "MEDIA_LIBRARY", "mediaId": "<video>"}]}]}, {"platform": "INSTAGRAM", "postType": "REEL", "isAiGeneratedContent": false, "cover": {"type": "MEDIA_LIBRARY", "mediaId": "<cover>"}, "contentOverride": [{"text": "Astronauts on the space station exercise for hours every day.\n...", "media": [{"type": "MEDIA_LIBRARY", "mediaId": "<video>"}]}]} ] } 

The 6 pm New York time became 23:00Z: plan.py stops when a clock time has no time zone, and never assumes UTC. The order is fixed: the yes, the upload, posts_validate with the exact request, posts_create, then plan.py --posted with the post ID, then posts_get for the status per account. The request is validated as a whole: if one child is invalid, nothing is created. If validation forces a change, the agent plans again and asks for a new yes.

Nothing goes out twice

--posted writes the video and every account in that request to posted.json. From then on, plan.py leaves those accounts out of every new request for that video and shows them as "already posted". That matters for the most common next step: you connect the third account next week, plan again, and the new request holds only Shorts. The same goes for a failure. If one account fails, plan.py --failed releases just that account, and the accounts that went through are never sent again.

A scheduled post can't be checked before its time. After it, "check my cross-post" makes the agent read posted.json, call posts_get for each recorded post and report one line per account: live with its link, still processing, or failed and why.

The free plan math, said up front

Disclosure: the skill is made by the team behind WoopSocial, and posting goes through WoopSocial. It posts through each platform's own API with WoopSocial's approved apps, so you don't need a developer app or an app review of your own. The free plan covers 2 social accounts, no card. TikTok, Reels and Shorts are 3, so in its first message the agent asks which you want: it posts 2 and gives you the by-hand page for the third, posts all of them on a paid plan, or prepares everything and you post all of them by hand with nothing to sign up for. The connect step for your agent is on the WoopSocial setup page for AI agents.

WoopSocial posts the video and the texts. It doesn't write captions or change the video: the agent writes the texts, and the skill's own script does any re-encoding. Some things can't be set through it, and the plan lists them under "After it's live": the YouTube cover, YouTube's altered content and paid promotion answers, and Instagram's paid partnership label. A sound picked from an app's library can't be added either; for TikTok the agent can send the video as a draft to your inbox, where you add the sound and post it.

Several accounts per platform

The job file takes as many accounts as you have, including two on one platform (a Dutch and an English TikTok, say). Each account then gets its own text with "text": {"<account name>": ...} and its audience written down, and the plan warns once per job with what that platform's own rules say about the same video on several accounts. The skill never offers to speed up, crop or filter copies to make them look different, and it posts the original export only: no copy saved from another app, and nothing added to the picture.

It also makes no reach promises. No platform has said that posting order, a gap between platforms, a time of day or a hashtag count changes how a video is shown, so the plan doesn't suggest any, and it doesn't add #Shorts, which YouTube doesn't need.

Try it

npx skills add social-media-skills/cross-post-video-skill -g -a claude-code -y 

Then tell your agent "post this video to TikTok, Reels and Shorts" or "cross-post clip.mp4 everywhere tomorrow at 6". It needs Python 3.9+, ffmpeg and Pillow; scripts/check_setup.py prints the install command for anything missing. The full example, with every file the skill made, is in examples/microgravity/. Issues and pull requests are welcome, and there are more free skills for social media from the same team.