<plugin-root>names the directory that holds this plugin's.codex-plugin/plugin.json. Resolve it once from where this file was loaded, then substitute it into every path below that starts with it. Arguments:<username> [--out DIR] [--limit N] [--since YYYY-MM-DD] [--no-videos] [--metadata]. Wherever<arguments>appears below, substitute the text the user typed after the skill name.
Instagram profile grab
Archive the media of one Instagram profile using the bundled grabber. The script does the work; this workflow exists to get the arguments right, to handle the two screens that need a human, and to report honestly on what came back.
Requirements
What this actually does
The collection strategy is capture-and-replay, and it matters that it stays that
way. The script drives a real logged-in browser to the profile, keeps one genuine
PolarisProfilePostsTabContentQuery_connection request the page issues on its
own, then walks the whole timeline by re-posting that request with only the
cursor changed. No doc_id, fb_dtsg or lsd value is ever reconstructed, so
Meta rotating them breaks nothing.
The background and every field name are in
<plugin-root>/skills/grabber-development/references/instagram-media.md.
Read it before changing the script, and especially before "simplifying" the collector
into a passive network listener: the newest 24 posts never appear in that query,
so a listener silently returns a truncated archive.
Step 1: Settle the target and the destination
Resolve the username from the request, stripping any @ or profile URL wrapper.
Pick a destination that will not be published by accident. A media dump belongs
outside anything a static host serves and outside anything git tracks. If the
repository has an established place for untracked source assets, use it and say
so; otherwise default to ig-<username> in the working directory and tell the
user where it landed.
State the plan in one line before running anything: profile, destination, and whether videos are included.
Step 2: Check the session
The grabber keeps a browser storage state at ~/.instagram-grabber/session.json.
If it exists, the run is unattended. If it does not, the run needs a person at
the keyboard, so say that before starting rather than leaving a browser window
waiting in silence.
Anonymous access is not an option and is not worth attempting. Both
web_profile_info endpoints answer 401 require_login, and a logged-out browser
is redirected to the login page before a single post reaches the DOM.
Credentials, when the user supplies them, go in the IG_USER and IG_PASS
environment variables of that one command. Never write them to a file, a script,
a manifest or a commit, and never type a password inline on a command line: a
shell records command lines to a history file on disk. Read it into the variable
instead. Say plainly that a password pasted into a conversation stays in that
conversation's log.
The stored session is itself a live credential. Until it is revoked it grants the
account without a password and without a second factor, so keep it outside the
repository and outside backups, never point --session at a path inside a
working tree, and revoke it from Instagram's active-sessions page once a grab
campaign is over.
Step 3: Dry run first
python <plugin-root>/skills/grabber-development/scripts/instagram_grab.py \
<username> --out <dir> --dry-run
The dry run collects the whole timeline and prints the counts plus the first 20 planned files, without spending bandwidth. Check three things in its output before continuing:
- The post count looks like the profile, not a fraction of it.
- The newest listed file matches the profile's most recent post. If the top of the list is years old, the collector fell back to passive sniffing and the archive would be truncated.
- Photos and videos are both present when both were asked for.
Step 4: Two screens may need a human
Both appear in the browser window the script opens, and both are one-time:
- The Meta cookie dialog renders over the login form and swallows clicks underneath it. The script dismisses it, but a new variant may need a manual click.
- The EU pay-or-consent flow,
/consent/?flow=ad_free_subscription, blocks every navigation until answered. This is a decision about advertising and payment on the user's own account, so never answer it on their behalf. The script parks and waits; tell the user to answer it in the window. Once answered, it is stored in the session and does not return.
If the run reports it could not reach the profile grid, one of these two is almost always the reason.
Step 5: Run it
Drop --dry-run and add the flags the request implies:
| Flag | Use |
|---|---|
--limit N |
the newest N posts only |
--since YYYY-MM-DD |
keep only posts newer than this date, though the whole timeline is still walked |
--no-videos / --no-photos |
narrow the media kinds |
--covers |
also keep each video's cover frame |
--metadata |
record captions, permalinks and alt text in the manifest |
--concurrency N |
parallel downloads, default 5 |
The run is resumable and idempotent. Files that already exist are skipped, so a
second run picks up only new posts, and an interrupted run continues where it
stopped. Downloads land through a .part file and are renamed only once
complete, so a killed run leaves no truncated media behind.
Step 6: Report
Give the user the counts the script printed: downloaded, already present, failed, total size, destination. If any file failed, name it and say why rather than rounding the number down to a success.
Two things are worth flagging without being asked:
- The manifest at
_manifest.jsonrecords what was actually fetched: post codes, dates, media types, and with--metadatathe captions and permalinks. Incrementality comes from the files already on disk, not from the manifest, but the metadata it carries exists nowhere else, so do not delete it to "clean up". - CDN signatures expire. A plan produced by a dry run hours earlier should be regenerated rather than reused.