How to use Osintgram: every command explained
Osintgram is an interactive shell bound to one target username: you launch it with python3 main.py <target>, then type one command at a time at a "Run a command:" prompt. The in-tool list command prints 23 commands, three more than the README documents, and every one of them is in the table below.
Osintgram is not a scanner you point at a username and walk away from. main.py takes one required positional argument, builds an API client, prints a banner, and then loops on a prompt until you type quit. Every command applies to that one target, and nothing happens until you type something.
Launch with python3 main.py <target>, then type commands at the Run a command: prompt. The in-tool list shows 23 commands; the README only documents 20. File output is off by default: type FILE=y or JSON=y first, or launch with -f / -j. The four contact-harvesting commands (fwersemail, fwingsemail, fwersnumber, fwingsnumber) issue one API call per follower with no backoff, and they are the ones that get you blocked.
How the shell works
This guide assumes a working clone and a populated config/credentials.ini; if not, start with how to install Osintgram. On Kali the same applies, with a few distro-specific traps.
Always launch from the repo root
src/config.py reads config/credentials.ini as a relative path, so python3 ~/Osintgram/main.py target from your home directory silently finds no credentials. cd into the clone first. Running it from elsewhere is also the documented cause of issue #105, ModuleNotFoundError: No module named 'src.Osintgram'.
On startup the classic backend prints the target banner. That is Logged as <you>. Target: <target> [<numeric id>], plus [PRIVATE PROFILE] when the account is private and either [FOLLOWING] or [NOT FOLLOWING]. The constructor prints it, which is why it appears before the ASCII logo. The HikerAPI banner is shorter: no login line, no follow state. Then you get the prompt: the literal string Run a command: , in yellow.
TAB completion is wired to the command dictionary through gnureadline on Linux and macOS, or pyreadline on Windows, so partial names complete. Anything the dictionary does not recognize prints Unknown command in red; an empty line just prints a blank line. quit and exit both print Goodbye! and leave, and Ctrl-C is trapped to do the same thing.
Run a command: FILE=y
Run a command: JSON=y
Run a command: info
Run a command: followers
Run a command: target
Run a command: quitYou are not stuck with the username you launched against. The target command prompts Insert new target username: , resolves the new account and re-prints the banner, with no new login. It has one wart: setTarget() appends the target name to the output directory every time it runs, so after a switch your files land in output/<first target>/<second target>/ rather than a sibling folder.
Three ways to launch it
The README documents three launch modes, and they differ in more than syntax.
# 1. interactive shell
python3 main.py <target username>
# 2. one command, then exit
python3 main.py <target username> --command info
# 3. HikerAPI backend, no Instagram login of your own
HIKERAPI_TOKEN=<hikerapi token> python3 main.py <target username> -c infoSingle-command mode is not just the shell with one turn taken for you. When -c is set, main.py skips printlogo() and the constructor suppresses the Attempt to login... line, so the output is much quieter, and photos stops asking how many to download and takes everything. The loop also breaks after exactly one iteration, so -c FILE=y toggles the flag and exits without running anything. Use -f and -j instead.
The third mode is the one that matters most in 2026. If config.getHikerToken() returns anything (from the hikerapi_token field of credentials.ini or the HIKERAPI_TOKEN environment variable), main.py instantiates HikerCLI instead of the classic Osintgram class, and no Instagram login happens at all. The startup line becomes Connect to HikerAPI.... Treat it as the path still designed to work rather than a guaranteed one: issue #2664 (2026-06-21) reports that branch mishandling alternate user-response formats, and HikerAPI is a paid third party you hand your targets to. Whether the classic username-and-password path authenticates for you at all is a separate question, covered in does Osintgram still work.
| Flag | What it does | Notes |
|---|---|---|
-c, --command | Run one command and exit | Suppresses the logo and the login line; photos stops prompting |
-f, --file | Write .txt output for the session | Identical to typing FILE=y |
-j, --json | Write JSON output for the session | Identical to typing JSON=y |
-o, --output | Help text says "where to store photos" | Actually replaces the whole output base directory, for every file type |
-C, --cookies | Clear the cached session before starting | Same effect as the cache command: config/settings.json is reset to an empty object |
Every Osintgram command
The README lists 20 commands. The cmdlist() function that the in-tool list calls prints 23. The three it adds are cache, commentdata and target, and none of them appears in the README's command block. doc/COMMANDS.md is further behind still: its header block drops fwersnumber and fwingsnumber (both have sections lower down), and it documents cache, commentdata and target nowhere at all. If you want the authoritative list, type list in the shell or read the commands dictionary in main.py.
| Command | What it returns | Status and notes |
|---|---|---|
addrs | GPS locations tagged in the target's posts | Usually empty. Counts only posts with lat and lng, then reverse-geocodes each one through Nominatim |
cache | Wipes the cached session file | Local only, no network call. Prints Cache Cleared. or Settings.json don't exist.; on HikerAPI it just says Cache is already empty. |
captions | Captions from the target's posts | Its JSON export writes to the wrong filename (see below) |
commentdata | Every comment on every post, with author id and username | Undocumented in the README. Its JSON export is malformed |
comments | Total number of comments across the posts | Walks the entire feed to count |
followers | Follower list: id, username, full name | No throttle handler: a rate limit here surfaces as a raw traceback |
followings | Accounts the target follows | Same missing handler as followers |
fwersemail | Public emails published by the target's followers | One extra API call per follower. The main throttle magnet |
fwingsemail | Public emails of accounts the target follows | Same one-call-per-user shape |
fwersnumber | Public phone numbers of the target's followers | Same shape, plus a misspelled JSON filename |
fwingsnumber | Public phone numbers of accounts the target follows | Same shape |
hashtags | Hashtags the target uses | Whole-feed pass |
info | Profile metadata as bracketed labels | One of the few commands that skips the private-profile guard. Ignores FILE=y: it only ever writes JSON |
likes | Total number of likes across the posts | Whole-feed pass |
mediatype | How many posts are photos versus videos | Whole-feed pass |
photodes | Alt-text descriptions of the photos | Dead. It calls the retired ?__a=1 web endpoint; the HikerAPI path replies Instagram has disabled this functionality. |
photos | Downloads posts as .jpg into the output folder | Prompts for a count; takes everything under -c. Enumerates the whole feed before applying the limit |
propic | Downloads the profile picture | Also skips the private-profile guard |
stories | Downloads currently active stories as .jpg or .mp4 | KeyError: 'media_count' is reported when the reel payload lacks the key |
tagged | Users the target tagged in their own posts | No explicit guard, but it reads the feed, so it still needs feed access |
target | Switches to a new target without restarting | Undocumented in the README. Nests the output directory on every switch |
wcommented | Users who commented on the posts, ranked by count | Whole feed plus a comment fetch per post, slow on active accounts |
wtagged | Users who tagged the target, ranked by count | Reads the tagged-in feed; its pagination loop switches to the target feed |
Four more dictionary entries are not data commands: list and help print that same list, quit and exit leave. Two inputs, FILE=y/n and JSON=y/n, are handled outside the dictionary altogether, which is why TAB completion never offers them.
One caveat applies to the whole table: nothing returns data if the backend cannot authenticate. Every row assumes you got past login, with either a session the classic client accepts or a HikerAPI token. The commands are not what breaks first in 2026.
Reading the info output
info is the command you will run first and the one most likely to mislead you. It calls the private endpoint users/{user_id}/full_detail_info/ and reads content['user_detail']['user'], then prints a fixed sequence of bracketed labels:
[ID],[FULL NAME],[BIOGRAPHY]: always printed[FOLLOWED],[FOLLOW]: always printed, and not what they look like[BUSINESS ACCOUNT], then[BUSINESS CATEGORY]only when the account is a business and has not hidden its category[VERIFIED ACCOUNT], then[EMAIL]only when a public email is set[HD PROFILE PIC]: always printed, as a URL[FB PAGE],[WHATSAPP NUMBER],[CITY],[ADDRESS STREET],[CONTACT PHONE NUMBER]: each printed only when the field is populated
The [FOLLOWED] / [FOLLOW] trap
[FOLLOWED] is the follower count and [FOLLOW] is the following count. The labels read like the opposite of what they mean. The JSON export settles it: the same two values are written under the keys edge_followed_by and edge_follow, the old web-API names for followers and following. Take the counts from the JSON export if you are transcribing them.
The HikerAPI backend prints an extra [MEDIA] line with the post count that the classic path never shows, so the same command yields a different field list per backend. And when the underlying call raises, the handler prints Oops... <target> non exist, please enter a valid username. and exits with code 2, a message you should not trust. Issue #1020 shows Instagram returning a non-JSON error body and the tool reporting it as a missing user.
Saving output, and the filenames that collide
Nothing is written to disk unless you ask. Type FILE=y for text output and JSON=y for JSON at any point in the session; the tool confirms with Write to file: enabled and Export to JSON: enabled, and FILE=n / JSON=n turn them back off. Launching with -f or -j sets the same booleans before the first command.
On current master, results land in a per-target subdirectory. On the 1.3 release tag they landed in a flat output/, which is why older guides show a different layout. The .txt file holds the raw PrettyTable ASCII table (file.write(str(t))), not CSV, so do not expect to open it in a spreadsheet.
output/
|-- dont_delete_this_folder.txt
`-- <target>/
|-- <target>_followers.txt
|-- <target>_followers.json
|-- <target>_propic.jpg
|-- <target>_<photo id>.jpg
`-- <target>_<story id>.mp4The naming is where it gets messy. Three commands write JSON under a filename that does not match the command you typed, one of those overwrites a previous result, and two more break the pattern their own way.
| Command with JSON=y | File it actually writes | Problem |
|---|---|---|
captions | <target>_followings.json | Overwrites the file followings wrote. Copy-paste bug in get_captions |
photodes | <target>_descriptions.json | Name does not match the command |
fwersnumber | <target>_fwerssnumber.json | Double "s", and the key inside the object is followings_phone_numbers |
commentdata | <target>_comment_data.json | Hand-written with a trailing comma and no separators between objects, so json.load() fails on it |
info | <target>_info.json only | There is no .txt writer at all; FILE=y does nothing here |
Do not run captions after followings in the same session
With JSON=y enabled, captions writes to <target>_followings.json. If you dumped the following list first, running captions replaces it with caption data and there is no prompt, no backup and no warning. Export captions in a separate session, or rename the followings dump first.
The commands that get you throttled
The four contact-harvesting commands share one architecture. get_fwersemail() first paginates the entire follower list, printing a running Catched N followers email counter. Only then does it ask Do you want to get all emails? y/n: . Then, for every follower it collected, it issues a separate user_info call and keeps the record only if a public email is set.
Two consequences follow. Your limit is applied after the full enumeration, so answering "n" and asking for 200 emails still walks the whole follower list first. And the limit only counts matches, so a target whose followers rarely publish an email gets enumerated almost end to end regardless. There is no sleep, no jitter and no backoff in those loops; issue #657 is a community patch that adds one.
Asking for fewer results does not make it cheaper
The follower list is enumerated in full before the prompt appears, and one profile call is issued per follower afterwards. On the classic backend that is request volume Instagram counts against you; on HikerAPI the same loop calls user_by_id_v2 once per user, so it is request volume you pay for. Neither backend gives you a cheap sample of a large follower list.
What a block looks like depends on the command. The four contact commands catch ClientThrottledError and print Error: Instagram blocked the requests. Please wait a few minutes before you try again. followers and followings have no such handler, so the same condition escapes as a raw traceback ending in urllib.error.HTTPError: HTTP Error 429: Too Many Requests, the shape reported in issue #394.
The handler does not save your data either. In issue #366 a user reached Catched 41643 followers email before the block, and the recovery path then crashed with TypeError: string indices must be integers, discarding every collected record. Issue #342 reports being cut off around 35,000 followers on a six-figure target. Those are the only hard numbers that exist: no reliable wall-clock threshold has been published, and any guide that gives you one in minutes is guessing.
photos, comments, likes, mediatype, hashtags, wcommented and commentdata all paginate the entire feed, and the last two fetch comments per post on top of that. They rarely trigger a block, but on an account with thousands of posts they are much slower than they look.
Commands that return nothing
A red Sorry! No results found :-( is not an error. It means the request succeeded and the target has no data of that kind. Most commands can emit it.
addrs is the usual suspect. It only counts posts where the location object carries both a latitude and a longitude, and geotagged posts have been rare since Instagram retired the photo map. Whatever does exist is then reverse-geocoded through Nominatim, which is itself rate-limited.
photodes is a different case: it is not empty, it is dead. It still fetches https://www.instagram.com/<target>/?__a=1 and reaches into graphql.user.edge_owner_to_timeline_media, an unauthenticated endpoint that no longer returns that JSON. On the HikerAPI backend the function has been reduced to a single line that prints Instagram has disabled this functionality. Treat it as removed.
stories returns nothing when there is nothing live, which is normal, but it also has a fragile parser: issue #1258 shows it aborting with KeyError: 'media_count' when the reel payload does not include that key.
Private targets, briefly
If the banner shows [PRIVATE PROFILE] alongside [NOT FOLLOWING], almost every command in the table stops before it makes a request. A single guard checks whether the account is private and unfollowed, prints Impossible to execute command: user has private profile, and offers Do you want send a follow request? [Y/N]: . The HikerAPI backend is blunter still: private alone blocks, with no follow-request prompt. No flag changes that. The full picture (which commands survive and what Instagram still exposes publicly) is in does Osintgram work on private accounts.
Doing the same job without the CLI
The command set is genuinely broad. What makes Osintgram awkward is everything around it: one target per session, a credentials file, throttling with no backoff, filenames that overwrite each other, and a login path that fails for many people before any of this matters. If you are running the CLI for real research, keep sessions short and export one dataset at a time.
If you only want the report, a hosted lookup skips the whole apparatus: there is no repo to clone, no Instagram account of your own to risk, and no rate-limit loop to babysit. It covers public profiles only, the same ceiling the CLI has.