Metabigor OSINT Recon
j3ssie/metabigor
Operates the metabigor CLI to map a target's network ranges, subdomains, ports, related domains, CDNs and archived URLs from free sources without API keys.
Social media monitoring, narrative tracking, and OSINT. An agent skill from jamditis/claude-skills-journalism.
$ npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install jamditis/claude-skills-journalism social-media-intelligence --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/jamditis/claude-skills-journalism.git skills-src && mkdir -p .claude/skills && cp -r skills-src/journalism-core/skills/social-media-intelligence .claude/skills/social-media-intelligence && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "social-media-intelligence" agent skill from https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/social-media-intelligence into .claude/skills/social-media-intelligence/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "social-media-intelligence", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/social-media-intelligenceType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install jamditis/claude-skills-journalism social-media-intelligence --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jamditis/claude-skills-journalism.git skills-src && mkdir -p .agents/skills && cp -r skills-src/journalism-core/skills/social-media-intelligence .agents/skills/social-media-intelligence && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "social-media-intelligence" agent skill from https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/social-media-intelligence into .agents/skills/social-media-intelligence/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "social-media-intelligence", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install jamditis/claude-skills-journalism social-media-intelligence --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jamditis/claude-skills-journalism.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/journalism-core/skills/social-media-intelligence .cursor/skills/social-media-intelligence && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "social-media-intelligence" agent skill from https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/social-media-intelligence into .cursor/skills/social-media-intelligence/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "social-media-intelligence", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/jamditis/claude-skills-journalism.git --path journalism-core/skills/social-media-intelligence--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install jamditis/claude-skills-journalism social-media-intelligence --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jamditis/claude-skills-journalism.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/journalism-core/skills/social-media-intelligence .gemini/skills/social-media-intelligence && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "social-media-intelligence" agent skill from https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/social-media-intelligence into .gemini/skills/social-media-intelligence/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "social-media-intelligence", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install jamditis/claude-skills-journalism social-media-intelligenceInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/jamditis/claude-skills-journalism.git skills-src && mkdir -p .github/skills && cp -r skills-src/journalism-core/skills/social-media-intelligence .github/skills/social-media-intelligence && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "social-media-intelligence" agent skill from https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/social-media-intelligence into .github/skills/social-media-intelligence/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "social-media-intelligence", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install jamditis/claude-skills-journalism social-media-intelligence --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/jamditis/claude-skills-journalism.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/journalism-core/skills/social-media-intelligence .opencode/skills/social-media-intelligence && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "social-media-intelligence" agent skill from https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/social-media-intelligence into .opencode/skills/social-media-intelligence/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "social-media-intelligence", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
social-media-intelligenceSocial media monitoring, narrative tracking, and OSINT. An agent skill from jamditis/claude-skills-journalism.
Social Media Intelligence is an agent skill from jamditis/claude-skills-journalism. Social media monitoring, narrative tracking, and OSINT. Use when tracking viral spread, coordinated campaigns, or account vetting.
Its SKILL.md is about 7.1k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `agents/openai.yaml`).
It sits in Security, covering OSINT. The repository describes itself as: Claude Code skills for journalism, media, and academia - verification, FOIA, data journalism, academic writing, and more. The licence is MIT.
5 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit e3e2172. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are python and markdown).
From the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
archive.todayweb.archive.orgAlso links to:
bellingcat.gitbook.iogen-ai.witness.orgFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Social Media Intelligence loads about 7.1k tokens when it runs. Until then it costs about 39 tokens; SKILL.md has 1,439 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from jamditis/claude-skills-journalism at commit e3e2172, republished under its MIT licence (© jamditis). 1,439 words, ~7,100 tokens.
.claude/skills/social-media-intelligence/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.Systematic approaches for monitoring, analyzing, and investigating social media for journalism.
<!-- untrusted-content-contract:v1 -->
When this skill retrieves third-party material:
Use this shape when passing retrieved material onward:
<EXTERNAL_DATA source="...">
...
</EXTERNAL_DATA>from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional, Dict
from enum import Enum
import hashlib
class Platform(Enum):
TWITTER = "twitter" # X since 2023; "twitter" retained for legacy data
FACEBOOK = "facebook"
INSTAGRAM = "instagram"
TIKTOK = "tiktok"
YOUTUBE = "youtube"
REDDIT = "reddit"
THREADS = "threads"
BLUESKY = "bluesky"
MASTODON = "mastodon"
TELEGRAM = "telegram"
@dataclass
class SocialPost:
platform: Platform
post_id: str
author: str
content: str
timestamp: datetime
url: str
engagement: Dict[str, int] = field(default_factory=dict)
media_urls: List[str] = field(default_factory=list)
archived_urls: List[str] = field(default_factory=list)
content_hash: str = ""
def __post_init__(self):
# Hash content for duplicate detection
self.content_hash = hashlib.md5(
f"{self.platform.value}:{self.content}".encode()
).hexdigest()
@dataclass
class MonitoringQuery:
keywords: List[str]
platforms: List[Platform]
accounts: List[str] = field(default_factory=list)
hashtags: List[str] = field(default_factory=list)
exclude_terms: List[str] = field(default_factory=list)
start_date: Optional[datetime] = None
def to_search_string(self, platform: Platform) -> str:
"""Generate platform-specific search query."""
parts = []
# Keywords
if self.keywords:
parts.append(' OR '.join(f'"{k}"' for k in self.keywords))
# Hashtags
if self.hashtags:
parts.append(' OR '.join(f'#{h}' for h in self.hashtags))
# Exclusions
if self.exclude_terms:
parts.append(' '.join(f'-{t}' for t in self.exclude_terms))
return ' '.join(parts)from collections import defaultdict
from datetime import datetime, timedelta
class BreakingNewsDetector:
"""Detect sudden spikes in keyword mentions."""
def __init__(self, baseline_window_hours: int = 24):
self.baseline_window = timedelta(hours=baseline_window_hours)
self.mention_history = defaultdict(list)
def add_mention(self, keyword: str, timestamp: datetime):
"""Record a mention of a keyword."""
self.mention_history[keyword].append(timestamp)
# Prune old data
cutoff = datetime.now() - self.baseline_window * 2
self.mention_history[keyword] = [
t for t in self.mention_history[keyword] if t > cutoff
]
def is_spiking(self, keyword: str, threshold_multiplier: float = 3.0) -> bool:
"""Check if keyword is spiking above baseline."""
now = datetime.now()
recent = sum(1 for t in self.mention_history[keyword]
if t > now - timedelta(hours=1))
baseline_hourly = len([
t for t in self.mention_history[keyword]
if t > now - self.baseline_window
]) / self.baseline_window.total_seconds() * 3600
if baseline_hourly == 0:
return recent > 10 # Arbitrary threshold for new topics
return recent > baseline_hourly * threshold_multiplier
def get_trending(self, top_n: int = 10) -> List[tuple]:
"""Get keywords sorted by spike intensity."""
spikes = []
for keyword in self.mention_history:
if self.is_spiking(keyword):
recent = sum(1 for t in self.mention_history[keyword]
if t > datetime.now() - timedelta(hours=1))
spikes.append((keyword, recent))
return sorted(spikes, key=lambda x: x[1], reverse=True)[:top_n]from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
@dataclass
class AccountAnalysis:
username: str
platform: Platform
created_date: Optional[datetime] = None
follower_count: int = 0
following_count: int = 0
post_count: int = 0
# Authenticity signals
profile_photo_is_stock: Optional[bool] = None
bio_contains_keywords: List[str] = field(default_factory=list)
posts_primarily_reshares: Optional[bool] = None
posting_pattern_irregular: Optional[bool] = None
engagement_ratio_suspicious: Optional[bool] = None
def calculate_red_flags(self) -> dict:
"""Score account authenticity."""
flags = {}
# Account age
if self.created_date:
age_days = (datetime.now() - self.created_date).days
if age_days < 30:
flags['new_account'] = f"Created {age_days} days ago"
# Follower ratio
if self.following_count > 0:
ratio = self.follower_count / self.following_count
if ratio < 0.1:
flags['low_follower_ratio'] = f"Ratio: {ratio:.2f}"
# Posting frequency
if self.created_date and self.post_count > 0:
age_days = max(1, (datetime.now() - self.created_date).days)
posts_per_day = self.post_count / age_days
if posts_per_day > 50:
flags['excessive_posting'] = f"{posts_per_day:.0f} posts/day"
# Stock photo check
if self.profile_photo_is_stock:
flags['stock_profile_photo'] = "Profile appears to be stock image"
return flags
def authenticity_score(self) -> int:
"""0-100 score, higher = more likely authentic."""
score = 100
flags = self.calculate_red_flags()
penalty_per_flag = 20
score -= len(flags) * penalty_per_flag
return max(0, score)from collections import defaultdict
from typing import Set, Dict
class AccountNetwork:
"""Map relationships between accounts."""
def __init__(self):
self.interactions = defaultdict(lambda: defaultdict(int))
self.accounts = {}
def add_interaction(self, from_account: str, to_account: str,
interaction_type: str = "mention"):
"""Record an interaction between accounts."""
self.interactions[from_account][to_account] += 1
def find_clusters(self, min_interactions: int = 3) -> List[Set[str]]:
"""Find groups of accounts that frequently interact."""
# Build adjacency with minimum threshold
adjacency = defaultdict(set)
for from_acc, targets in self.interactions.items():
for to_acc, count in targets.items():
if count >= min_interactions:
adjacency[from_acc].add(to_acc)
adjacency[to_acc].add(from_acc)
# Find connected components
visited = set()
clusters = []
for account in adjacency:
if account in visited:
continue
cluster = set()
stack = [account]
while stack:
current = stack.pop()
if current in visited:
continue
visited.add(current)
cluster.add(current)
stack.extend(adjacency[current] - visited)
if len(cluster) > 1:
clusters.append(cluster)
return sorted(clusters, key=len, reverse=True)
def coordination_score(self, accounts: Set[str]) -> float:
"""Score how coordinated a group of accounts appears."""
if len(accounts) < 2:
return 0.0
total_possible = len(accounts) * (len(accounts) - 1)
actual_connections = 0
for acc in accounts:
for other in accounts:
if acc != other and self.interactions[acc][other] > 0:
actual_connections += 1
return actual_connections / total_possible if total_possible > 0 else 0from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Optional
@dataclass
class Claim:
text: str
first_seen: datetime
first_seen_url: str
variations: List[str] = field(default_factory=list)
appearances: List[Dict] = field(default_factory=list)
def add_appearance(self, url: str, platform: Platform,
timestamp: datetime, author: str):
"""Track where this claim has appeared."""
self.appearances.append({
'url': url,
'platform': platform.value,
'timestamp': timestamp,
'author': author
})
def spread_timeline(self) -> List[Dict]:
"""Get chronological spread of the claim."""
return sorted(self.appearances, key=lambda x: x['timestamp'])
def platforms_reached(self) -> Dict[str, int]:
"""Count appearances by platform."""
counts = defaultdict(int)
for app in self.appearances:
counts[app['platform']] += 1
return dict(counts)
def velocity(self, window_hours: int = 24) -> float:
"""Calculate spread rate in appearances per hour."""
if not self.appearances:
return 0.0
recent = [
a for a in self.appearances
if a['timestamp'] > datetime.now() - timedelta(hours=window_hours)
]
return len(recent) / window_hoursfrom collections import Counter
from datetime import datetime, timedelta
class HashtagAnalyzer:
"""Analyze hashtag usage patterns."""
def __init__(self):
self.hashtag_posts = defaultdict(list)
def add_post(self, hashtags: List[str], post: SocialPost):
"""Record a post's hashtags."""
for tag in hashtags:
self.hashtag_posts[tag.lower()].append(post)
def co_occurrence(self, hashtag: str, top_n: int = 10) -> List[tuple]:
"""Find hashtags that commonly appear with this one."""
co_tags = Counter()
for post in self.hashtag_posts.get(hashtag.lower(), []):
# Extract hashtags from post content
tags = [
word.lower() for word in post.content.split()
if word.startswith('#')
]
for tag in tags:
if tag != f'#{hashtag.lower()}':
co_tags[tag] += 1
return co_tags.most_common(top_n)
def posting_pattern(self, hashtag: str) -> Dict:
"""Analyze when posts with this hashtag appear."""
posts = self.hashtag_posts.get(hashtag.lower(), [])
hour_counts = Counter(p.timestamp.hour for p in posts)
day_counts = Counter(p.timestamp.strftime('%A') for p in posts)
return {
'by_hour': dict(hour_counts),
'by_day': dict(day_counts),
'total_posts': len(posts),
'unique_authors': len(set(p.author for p in posts))
}For full archiving workflows (rate limits, batch jobs, recovery from broken archive UIs), see the web-archiving skill.
import re
import requests
from datetime import datetime
from typing import Optional
from urllib.parse import quote, urljoin
class SocialArchiver:
"""Archive social content before deletion."""
def __init__(self):
self.archived = {}
def archive_to_wayback(self, url: str) -> Optional[str]:
"""Submit URL to Internet Archive.
Anonymous saves are rate-limited at roughly 15/minute and silently drop
some paywalled or heavily JS-rendered pages. For high-volume archiving,
register an Internet Archive S3 key and add an Authorization header.
"""
try:
save_url = f"https://web.archive.org/save/{quote(url, safe='')}"
response = requests.get(save_url, timeout=30)
if response.status_code == 200:
archived_url = response.url
self.archived[url] = {
'wayback': archived_url,
'archived_at': datetime.now().isoformat(),
}
return archived_url
except Exception as e:
print(f"Wayback archive failed: {e}")
return None
def archive_to_archive_today(self, url: str) -> Optional[str]:
"""Submit URL to archive.today.
Operational notes (2026): the FBI subpoenaed archive.today's registrar
in October 2025 to identify the operator; Wikipedia voted in February
2026 to stop accepting it as a citation source after the site shipped
DDoS-attack code in January 2026. The service is still useful for
capturing content the Wayback Machine can't render, but treat it as
secondary to the Internet Archive and document any reliance on it.
It also rate-limits aggressively and serves CAPTCHAs to scrapers.
"""
try:
response = requests.post(
'https://archive.today/submit/',
data={'url': url, 'anyway': '1'},
timeout=60,
allow_redirects=False,
headers={'User-Agent': 'Mozilla/5.0 (verification archive bot)'},
)
# archive.today returns the snapshot URL in one of two shapes:
# - 30x with Location: https://archive.today/<snapshot_id>
# - 200 with Refresh: 0;url=https://archive.today/<snapshot_id>
# Following redirects silently can land on /wip/ pages or hide the
# canonical snapshot URL, so handle both headers explicitly.
if response.status_code in (301, 302, 303, 307, 308):
location = response.headers.get('Location')
if location:
# Location MAY be relative per RFC 7231; resolve against request URL.
return urljoin(response.url, location)
if response.status_code == 200:
refresh = response.headers.get('Refresh', '')
# Refresh keyword is case-insensitive per HTML spec; values may
# contain ;-separated params. Match the url= directive itself.
m = re.search(r'\burl\s*=\s*(.+)', refresh, re.IGNORECASE)
if m:
target = m.group(1).strip().strip('\'"')
return urljoin(response.url, target)
except Exception as e:
print(f"archive.today failed: {e}")
return None
def full_archive(self, url: str) -> dict:
"""Archive to multiple services for redundancy."""
results = {
'original_url': url,
'archived_at': datetime.now().isoformat(),
'archives': {},
}
wayback = self.archive_to_wayback(url)
if wayback:
results['archives']['wayback'] = wayback
archive_today = self.archive_to_archive_today(url)
if archive_today:
results['archives']['archive_today'] = archive_today
return results## Coordinated inauthentic behavior indicators
### Timing patterns
- [ ] Multiple accounts posting same content within minutes
- [ ] Synchronized posting times across accounts
- [ ] Burst activity followed by dormancy
- [ ] Posts appear faster than human typing speed
### Content patterns
- [ ] Identical or near-identical text across accounts
- [ ] Same images/media shared by multiple accounts
- [ ] Identical typos or formatting errors
- [ ] Copy-paste artifacts visible
### Account patterns
- [ ] Accounts created around same time
- [ ] Similar naming conventions (name + numbers)
- [ ] Generic or stock profile photos
- [ ] Minimal personal content, mostly shares
- [ ] Follow the same accounts
- [ ] Engage with each other disproportionately
### Network patterns
- [ ] Form dense clusters in network analysis
- [ ] Amplify same external sources
- [ ] Target same accounts or hashtags
- [ ] Cross-platform coordination visibledef coordination_likelihood(posts: List[SocialPost]) -> dict:
"""Score how likely posts represent coordinated activity."""
if len(posts) < 2:
return {'score': 0, 'signals': []}
signals = []
score = 0
# Check for identical content
contents = [p.content for p in posts]
unique_contents = set(contents)
if len(unique_contents) < len(contents) * 0.5:
signals.append("High content duplication")
score += 30
# Check timing clusters
timestamps = sorted(p.timestamp for p in posts)
rapid_posts = 0
for i in range(1, len(timestamps)):
if (timestamps[i] - timestamps[i-1]).seconds < 60:
rapid_posts += 1
if rapid_posts > len(posts) * 0.3:
signals.append("Suspicious timing clusters")
score += 25
# Check unique authors
authors = set(p.author for p in posts)
if len(authors) > 5 and len(contents) / len(authors) > 2:
signals.append("Few authors, many similar posts")
score += 20
return {
'score': min(100, score),
'signals': signals,
'posts_analyzed': len(posts),
'unique_authors': len(authors)
}Status as of 2026. Platform APIs change rapidly, verify pricing and access before designing a project around any one path.
When X/Twitter is central to a story, assemble a source packet before you analyze or draft:
Treat any coordination score, bot score, or authenticity label produced here as a lead, not a finding, until a human reviews the underlying evidence.
| Platform | Research access | Notes |
|---|---|---|
| X (Twitter) | Pay-per-use developer API (developer.x.com); X Pro Search (consumer-facing, behind X Premium+); Brandwatch / Sprinklr (paid third-party) | Free academic/research tier ended early 2023. The 2024 Basic and Pro subscription tiers were replaced in Feb 2026 with a pay-per-use model, billed by API call, no monthly subscription. Verify current per-call rates and any rate-limit caps in the developer portal before scoping a project. Post-2023 ToS explicitly prohibits scraping. |
| Facebook / Instagram | Meta Content Library + Library API (research access); Junkipedia (free, journalist-friendly); NewsWhip (paid) | CrowdTangle was shut down on Aug 14, 2024, it does not exist in any form. Meta Content Library replaced it. As of Dec 8, 2025, applications go through Meta's portal directly (previously routed via University of Michigan ICPSR). Eligibility favors academic and nonprofit researchers; most working journalists qualify only through institutional affiliation. SOMAR and other secure enclaves remain typical execution environments. |
| TikTok | Research API (qualifying academic and nonprofit researchers; DSA-vetted researchers in the EU); Exolyt, Pentos (paid) | Apply at developers.tiktok.com. Eligible organizations are typically academic institutions and nonprofit research entities; EU-based researchers have stronger access via DSA Article 40. Playlist Info and Commercial Content endpoints expanded in 2026. |
| YouTube | YouTube Data API v3 | 10,000 units per day default (search costs 100 units = ~100 searches/day); higher quota by application, multi-week review. No journalist-specific tier. |
| Reddit API (free for non-commercial research); Arctic Shift (Pushshift successor, free dumps via Academic Torrents) | Pushshift restricted to verified moderators since 2023, it is no longer a journalist-research path. Arctic Shift is the active successor. | |
| Bluesky | Jetstream (filtered JSON over WebSocket, no auth required) or raw firehose | Public-by-default. Jetstream is the journalist-friendly entrypoint at ~850 MB/day filtered; raw firehose is 4-8 GB/hour and requires you to build archives yourself. |
| Threads (Meta) | Threads API (publishing/embedding); Meta Content Library (research) | Public profile discovery threshold lowered to 100 followers in March 2026. Bulk historical research routes through Meta Content Library, with the same academic-only restriction. |
| Mastodon / Fediverse | Public-timeline API (per-instance); cross-instance search at search.noc.social or fediverse.info | Many instances now set DISALLOW_UNAUTHENTICATED_API_ACCESS; admin-controlled. Cross-instance search is fragmented. |
| Telegram | Bot API + MTProto + public previews at t.me/s/<channel>; Bellingcat tools (Telegago, Telepathy, TelegramDB) | Public-channel scraping is legal in most jurisdictions; private groups are off-limits. |
The EU Digital Services Act gives EU-based researchers stronger access rights than US researchers on TikTok and the Meta platforms. US journalists may need EU institutional partners (a university, a vetted research nonprofit) to qualify for the Research API tiers on those platforms. This is a real path, not a workaround.
The custom Python heuristics above are starting points for monitoring and pattern surfacing. For production OSINT work, the canonical external tools are:
| Field | Value |
|---|---|
| version | 1.1.0 |
| created | 2025-12-26 |
| updated | 2026-06-18 |
| author | Joe Amditis |
| domain | journalism, osint |
| complexity | advanced |
© jamditis, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file in journalism-core/skills/social-media-intelligence of jamditis/claude-skills-journalism.
Open the folder on GitHubat commit e3e2172
Social Media Intelligence next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Social Media Intelligence this skilljamditis/claude-skills-journalism | 416 | — | ~7.1k | Automated safety check: Pass | MIT | |
| Metabigor OSINT Reconj3ssie/metabigor | 1.9k | — | ~2.4k | Automated safety check: Pass | MIT | |
| Ctf Osintljagiello/ctf-skills | 3.4k | 1 repos | ~2.3k | Automated safety check: Notes | MIT | |
| ShadowBroker Intelligence ClientBigBodyCobain/Shadowbroker | 11k | — | ~8.9k | Automated safety check: Warn | AGPL-3.0 | |
| Awesome Osint Operatorshoyann/RZK-The-Hunter | 141 | — | ~4.8k | Automated safety check: Pass | CC-BY-SA-4.0 | |
| Run Claude Osintelementalsouls/Claude-OSINT | 2.8k | — | ~1.2k | Automated safety check: Pass | MIT |
j3ssie/metabigor
Operates the metabigor CLI to map a target's network ranges, subdomains, ports, related domains, CDNs and archived URLs from free sources without API keys.
ljagiello/ctf-skills
Provides open source intelligence techniques for CTF challenges.
BigBodyCobain/Shadowbroker
Lets an agent query a ShadowBroker OSINT platform for tracked flights, ships, satellites and news, and place its findings on the map as intel pins.
shoyann/RZK-The-Hunter
Ethical, evidence-first OSINT planning, tool selection, verification, monitoring, reporting, and guarded official wanted/fugitive-person location intelligence using a structured catalog adapted from…
elementalsouls/Claude-OSINT
Build, validate, and run the claude-osint skills repo — check SKILL.md frontmatter, run the secretscan.py and h1reference.py helpers, run sync-skill-content.sh, run the smoke test.
smixs/osint-skill
Conduct deep OSINT research on individuals. An agent skill from smixs/osint-skill.
jamditis/claude-skills-journalism
A skill your agent uses when creating distinct website directions, a client review picker, asset catalog, previews, and Cloudflare-ready handoffs.
jamditis/claude-skills-journalism
Builds an Open Knowledge Format (OKF) knowledge base from existing docs, notes, or a repo.
jamditis/claude-skills-journalism
Local Gitleaks scans for staged changes, push ranges, and full history in private repos, with redacted reports.
jamditis/claude-skills-journalism
Acquire, clean, analyze, verify, visualize, and explain data for journalism.
jamditis/claude-skills-journalism
Creates print-ready HTML that exports to PDF. An agent skill from jamditis/claude-skills-journalism.
jamditis/claude-skills-journalism
Establishes how to find and use skills, requiring Skill tool invocation before any response.
Categories
Social media monitoring, narrative tracking, and OSINT. An agent skill from jamditis/claude-skills-journalism. Social Media Intelligence is an agent skill from jamditis/claude-skills-journalism. Social media monitoring, narrative tracking, and OSINT.
Social Media Intelligence fits situations like: tracking viral spread; coordinated campaigns; account vetting.
Run `npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a claude-code`. Or copy the skill folder (journalism-core/skills/social-media-intelligence in jamditis/claude-skills-journalism) into .claude/skills/social-media-intelligence in your project. Claude Code loads it when a task matches its description.
Run `npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a codex`. Or copy the skill folder (journalism-core/skills/social-media-intelligence in jamditis/claude-skills-journalism) into .agents/skills/social-media-intelligence in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add jamditis/claude-skills-journalism --skill social-media-intelligence -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/social-media-intelligence, .gemini/skills/social-media-intelligence, .github/skills/social-media-intelligence and .opencode/skills/social-media-intelligence in your project.
SKILL.md names no scripts, command-line tools or credentials: Social Media Intelligence is instructions for the agent only. Our summary lists: Python 3.
SKILL.md names 4 domains. In commands or code: archive.today and web.archive.org; the agent is likely to contact these when it follows the instructions. As links in the text: bellingcat.gitbook.io and gen-ai.witness.org. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Social Media Intelligence is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 7.1k tokens (SKILL.md is roughly 28k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Social Media Intelligence: Metabigor OSINT Recon (j3ssie/metabigor, 1.9k stars), Ctf Osint (ljagiello/ctf-skills, 3.4k stars), ShadowBroker Intelligence Client (BigBodyCobain/Shadowbroker, 11k stars) and Awesome Osint Operator (shoyann/RZK-The-Hunter, 141 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
jamditis (a GitHub user) maintains it in jamditis/claude-skills-journalism, which has 416 GitHub stars. The repository holds 53 skills in this directory. The repository was last updated on October 4, 2026.
Source: jamditis/claude-skills-journalism on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.