Instagram skill for Claude Code, free and MIT licensed: Instagram Skill lets your agent turn an idea, a link, notes, your photos or a video into finished Instagram posts (carousels, single images, Reels and Stories), check every file against Instagram's current limits, show you the feed and the grid, and schedule them after you say yes. It also works in Codex, Cursor and OpenClaw.
Everything up to posting runs on your own computer with Python, Pillow and ffmpeg. There is no paid API. This post covers what the skill does, how to install it, the example in the repo, and then the parts that are code: the file checker, the preview, per-account versions and time zones, and the rules it uses to learn from your results.
npx skills add social-media-skills/instagram-skill -g -a claude-code -y
Repo: github.com/social-media-skills/instagram-skill
The example week in the repo, for "Earth in Numbers", a made-up science account. Figures from the USGS Water Science School; Earth photo NASA Blue Marble 2012 (no endorsement by NASA or USGS is implied). The Reel is the account's own 16-second animated short, made for the example from the same USGS figures.
What is an Instagram skill?
An agent skill is a folder with a SKILL.md that tells an agent like Claude Code how to do a job. Most Claude Instagram help stops at text: a hook, a caption, some hashtags, and "now design the post yourself". Then the post fails on details nobody checked: a PNG the API won't take, a 3:4 photo it rejects, a Reel title under the caption, a cover the grid cuts in half, six hashtags where Instagram now allows five.
This skill ships the scripts that make and check the posts, so the agent produces real files, not advice:
- carousels and single images rendered as 1080x1350 JPEGs, with a cover that survives the 3:4 profile grid;
- your own photos and screenshots converted and fitted (PNG, HEIC, 3:4, panoramas);
- your own videos finished as Reels: 9:16, H.264 and AAC, a chosen cover, and a check of what Instagram's caption and buttons cover;
- Story frames that tease a post before it goes out;
- one schedule for several accounts, each with its own version of the post, its own caption and its own time zone;
- results read back and ranked against your own posts of the same format.
Instagram MCP or Instagram API: how posting works
Posting through the Instagram API yourself means setting up a Meta app and its permissions. The skill posts through WoopSocial's Instagram MCP instead, so your agent gets Instagram tools without a Meta app of your own. Disclosure: the skill is maintained by Frank Heijdenrijk and the team behind WoopSocial. The free plan covers 2 social accounts. Setup for every agent is at woopsocial.com/agents.
An MCP alone gives the agent a "post" call. The skill adds what comes before it: making the media, checking it, the preview, per-account versions and the learning loop. Through WoopSocial it can post feed images, carousels, Reels (with a cover, and as Trial Reels when your account qualifies) and Stories, now or scheduled, and validate each post first.
Some things can't be set this way: collaborators, location, music, alt text and first comments. The skill writes them into a checklist per post so you add them in the app. WoopSocial doesn't write captions or edit media; the skill does that on your computer and you approve it. If you'd rather not connect anything, you get the files, captions and schedule to post yourself.
Instagram posting through any API needs a Business or Creator account. Switching is free in the app.
Install
Claude Code
npx skills add social-media-skills/instagram-skill -g -a claude-code -y
Codex, Cursor and other agents: run npx skills add social-media-skills/instagram-skill and pick your agent, or copy skills/instagram-skill/ into your agent's skills folder.
OpenClaw: openclaw skills install @social-media-skills/instagram-skill
Requirements: Python 3.9 or newer with Pillow, and ffmpeg for anything with video. pillow-heif is optional, for iPhone .heic photos. python3 scripts/check_setup.py prints what's missing and the install command for your system. CI runs the full pipeline on macOS, Windows and Ubuntu.
The example: a week of Instagram posts in two languages
The repo has one full run in examples/water-week/. "Earth in Numbers" is a made-up science account with an English profile (@earthinnumbers, New York) and a Spanish one (@latierraennumeros, Madrid). The brief, shortened:
You: A week of posts for @earthinnumbers (New York, followers online
around 12:30) and the Spanish account @latierraennumeros (Madrid,
13:30) about Earth's water. Real numbers. Our animated short about
the air is attached.
Agent: Mon: a carousel, "All of Earth's water in a 1-liter bottle": pour
out the oceans and 35 ml is left; every river is less than a
tenth of a drop. Spanish slides for the Spanish account. A Story
an hour before asks the question.
Tue: a single image, 55% of the water under our feet is salty.
Wed: your short as a Reel (English only), cover picked from its
sharpest frames; nothing sits under Instagram's caption or buttons.
Checks: everything passes. Preview: preview/index.html.
Each post carries its own fact, so nothing is posted twice. The carousel walks through the bottle: the cover, "35 ml is all that's left once you pour out the oceans", where those 35 ml are (ice 17.4, groundwater 16.9, permafrost 0.22, lakes 0.13, the rest 0.03 ml), "25 ml is fresh water: 17.4 ml ice, 7.6 ml underground", "0.002 ml for every river: less than a tenth of one drop", and a save ask. The single image says 55% of groundwater is saline (USGS: 12.87 of 23.4 million km³). The Reel, the account's own 16-second animated short, says 6 times more water floats in the air than flows in every river.
The folder holds the plan, the slides in both languages, the Reel cover sheets, the preview page, schedule.md and one WoopSocial request per post and account. Seven posts in all, and every figure comes from the USGS Water Science School with the source in the caption.
The checker: Instagram's limits as code
ig_check.py reads the plan and checks every file and caption. The limits sit in one dict at the top, with the date they were checked, so updating them is a one-line change:
# Checked 2026-11-04 against Meta's content publishing docs and Instagram's own announcements.
LIMITS = {
"image_min_w": 320, "image_max_w": 1440, "image_bytes": 8 * 1024 * 1024,
"feed_min_ratio": 0.8, "feed_max_ratio": 1.91, # width / height
"carousel_min": 2, "carousel_max": 20,
"reel_min_s": 3, "reel_max_s": 15 * 60, "reel_bytes": 300 * 1024 * 1024,
"story_min_s": 3, "story_max_s": 60, "story_bytes": 100 * 1024 * 1024,
"carousel_video_max_s": 60,
"fps_min": 23, "fps_max": 60,
"caption_chars": 2200, "hashtags": 5, "mentions": 20, "fold_chars": 125,
}
A few of these surprise people. The API takes JPEG only, so a PNG screenshot or an iPhone HEIC fails. Feed images must be between 4:5 and 1.91:1, so a 3:4 photo (which the app accepts) is rejected through the API. Every carousel item is shown at the first item's shape. Five hashtags is Instagram's cap since December 2025.
Videos go through ffprobe: container, codec, 8-bit pixel format, HDR, frame rate, length, audio and size. 10-bit and HDR phone video is a common cause of grey or failed Reels, so the checker flags it and re-encodes it. Screen recordings with a variable frame rate get a warning, because re-encoding them to a constant 30 fps avoids audio drift.
Captions are checked for length, hashtags, mentions, links (not clickable in captions), a first line longer than the roughly 125 characters the feed shows before "... more", and two accounts opening with the same line.
Fixes are new files
--fix never touches your originals. It writes fixed copies to fixed/, points the plan at them and keeps a plan.json.bak:
python3 scripts/ig_check.py plan.json # report
python3 scripts/ig_check.py plan.json --fix # fixed copies in fixed/, plan updated, originals kept
The exit code is 1 while errors remain, so the agent runs it until it gets 0. Anything it can't fix safely (a video that needs trimming, a photo too small to scale up) comes back as an error with the reason in plain words.
Your own landscape footage is framed, not cropped blind
The interesting case is landscape footage for a Reel. Filling 9:16 from 16:9 throws away most of the width, and padding with a blurred copy looks like a repost. So the checker refuses to guess once the loss is 12% or more:
if kind == "video" and fmt in ("reel", "story") and loss >= 0.12:
report("error", pid, label, f"this video would lose {loss:.0%} of its width to fill 9:16. Check reel.py frames, then: "
"reframe it on the subject in an editor; or --fit crop when the subject stays in the middle; or give the "
"post a \"frame\": {\"title\": \"...\"} for a framed Reel (video full width under a headline)")
A framed Reel puts the video full width on a plain 1080x1920 background in a colour sampled from the clip's edges, with the hook as a headline above it. The headline sits below the top bar (the top 14%) and the video above the caption area (the bottom 35%), the safe zones Meta publishes for Reels. Nothing is cut off.
Framing is only for your own footage, so the post also needs "own_footage": true. Without it the checker warns: a headline or border around someone else's clip still reads as a repost to Instagram.
reel.py handles the rest of a Reel: frames picks sharp, well-lit cover candidates spread over the video into one sheet, cover writes a 1080x1920 JPEG cover with an optional title, and zones shades what Instagram's caption, buttons and top bar will cover. The example's Reel cover was picked from that frames sheet.
The preview: the feed cut and the 3:4 grid, per account
preview.py writes preview/index.html and the same thing as one preview.png the agent looks at before you do:
python3 scripts/preview.py plan.json --out preview/ --grid current-grid.png
Three things in it are easy to get wrong by eye:
- The grid crop. The profile grid shows posts at 3:4 since January 2025, but the API only accepts 4:5 or wider. Each grid tile is the middle 3:4 of the post, so you see what the grid hides. The checker also warns when the grid would hide more than 12% of a cover's width.
- The "... more" cut. Each feed card cuts the caption at about 125 characters, where the feed does, so you can see whether the hook made it.
-
Per account. Every post is shown on every account with that account's own media and caption, and each account gets its own grid, newest first.
--gridadds a screenshot of your current grid below the new posts.
Reels are shown at the middle 4:5 the main feed commonly shows (creators observe this crop; Meta doesn't document it). Stories are full screen.
Per-account versions and time zones
Instagram's 2026 originality rules cover photos and carousels too, not only video, so the same media on a second account is likely seen as a repost of the first upload. In plan.json, a post has default media and a caption, and each target can replace them:
"targets": [
{"account": "en", "at": "2026-11-16T12:30"},
{"account": "es", "media": ["slides/bottle-es/slide-01.jpg", "..."],
"caption": "¿Cuánta agua de la Tierra hay en los ríos? ...",
"at": "2026-11-16T13:30"}
]
The checker warns when a second account has no media of its own, and when two accounts open with the same first line.
Each account has its own timezone and times, taken from its Insights. schedule.py gives every feed post a slot per account (one a day by default), converts the local time to UTC in the request and writes schedule.md. The example's Reel at 12:30 in New York becomes "scheduledFor": "2026-11-18T17:30:00Z". Without times it uses 12:00 and says so at the top of the schedule. It never claims a "best time".
Stories don't take a feed slot. A Story tied to a post with "with": {"post": "bottle", "minutes": -60} goes out an hour before that post on each account, so the example's teaser runs at 11:30 New York time and 12:30 Madrid time.
python3 scripts/schedule.py plan.json --start 2026-11-16
python3 scripts/schedule.py plan.json --bodies-only # after upload, with media IDs
The agent uploads every file, writes the IDs into the plan, rebuilds the request bodies, validates each one, and posts only after a clear yes on the preview.
Learning from your own numbers
log.py record logs every post after it's created. A few days later, log.py results reads the post metrics back (views, likes, comments, saves, shares and reach, plus an optional insights.csv for what only the app shows, like follows and Reel watch time) and writes results.md. The rules are in the script's docstring, so it doesn't mislead:
- Rates are per reach. A post without reach is listed but not ranked.
- Posts are compared only within the same format and the same account. Stories are left out.
- Each post is measured once, at the value collected nearest to day 7 (within days 3 to 14), and that measurement is saved in
posts.jsonl. Posts from different weeks are compared at the same age, and the comparison grows with every run. - A best post is named only once a format has 6 or more ranked posts.
- Posting-time advice needs 3 or more ranked posts in at least 2 time slots (weekday or weekend, morning, afternoon or evening).
- A missing number is "not reported", never zero.
Posts are ranked by sends, saves and follows per reach against the account's own median. Sends per reach is one of the main signals Adam Mosseri (head of Instagram) has named, with watch time and likes per reach. From that ranking the agent picks the next step from a small table in SKILL.md: same format with a new topic, add a checklist slide, end with a send-style ask, or change the cover first.
What the skill won't do
- Invent numbers, results, reviews or quotes.
- Post anything without your yes on the preview.
- Give a generic "best time to post". It uses your Insights, then your results.
- Post the same media and caption to several accounts.
- Edit your footage (cuts, captions, music), or crop or pad landscape video without asking.
FAQ
Can Claude post to Instagram?
Yes. With this skill and WoopSocial's MCP connected, Claude Code, Codex, Cursor and OpenClaw can post and schedule Instagram posts after you approve each batch. Without a connection, the skill still makes, checks and previews everything.
Is this an Instagram MCP?
No, it's a skill that uses WoopSocial's Instagram MCP to post. The skill adds the media, the checks, the preview, per-account versions and the results.
Can I schedule Instagram posts to several accounts?
Yes. One plan, one request per account, each with its own version of the post, its own caption and its own time zone.
Does it work with a personal Instagram account?
No app can post to a personal account through Instagram's API. Switch to a free Creator or Business account in the app.
Why 4:5 and not 3:4?
The grid shows 3:4, but the API only accepts 4:5 to 1.91:1. The skill renders 4:5 and keeps the cover text inside what the grid shows.
Try the Instagram Skill
npx skills add social-media-skills/instagram-skill -g -a claude-code -y
Then ask your agent for a week of posts about something you know well. The repo, the full example and every Instagram limit with its source: github.com/social-media-skills/instagram-skill. Issues and pull requests are welcome.
More free skills from the same team at social-media-skills.com.


Top comments (0)