Skip to content
alyn
oaustegard

Browsing Bluesky skill

Browse Bluesky content via API and firehose - search posts, fetch user activity, sample trending topics, read feeds and lists, analyze and categorize accounts. Supports authenticated access for personalized feeds. Use for Bluesky research, user monitoring, trend analysis, feed reading, firehose sampling, account categorization.

$npx skills add oaustegard/claude-skills --skill browsing-bluesky
View this skill on GitHuboaustegard/claude-skills/browsing-bluesky/SKILL.md
Pack stars
257
Pack installs
3.7k
Alyn Score
55/99
Updated
8 h ago
License
MIT
– views – installs via Alyn

oaustegard

Browsing Bluesky

Install Browsing Bluesky

Pick your agent. Every command is ready to paste.

$npx skills add oaustegard/claude-skills --skill browsing-bluesky

The open skills CLI detects your agents (Claude Code, Cursor, Codex and 70+ more) and installs to the right folder. Add -g to install globally.

01

Install it

Run the command above, or upload the folder in Claude.

02

Ask naturally

Describe the task. Your agent loads browsing-bluesky when the request matches its description.

03

Name it if needed

If it does not trigger, say “use the browsing-bluesky skill”. In Codex, type $browsing-bluesky.

The skill itself

SKILL.md

FolderGitHub

Browsing Bluesky

Access Bluesky content through public APIs and real-time firehose. Supports optional authentication for personalized feeds. Includes account analysis for categorization.

Implementation

Add skill directory to path and import:

import sys
sys.path.insert(0, '/path/to/skills/browsing-bluesky')  # or use .claude/skills symlink path
from browsing_bluesky import (
    # Core browsing
    search_posts, get_user_posts, get_profile, get_feed_posts, sample_firehose,
    get_thread, get_quotes, get_likes, get_reposts,
    get_followers, get_following, search_users,
    # Trending
    get_trending, get_trending_topics,
    # Account analysis
    get_all_following, get_all_followers, extract_post_text,
    extract_keywords, analyze_account, analyze_accounts,
    # Authentication utilities
    is_authenticated, get_authenticated_user, authenticated_identity,
    clear_session
)

Authentication (Optional)

Authentication enables personalized feeds (like Paper Skygest) that require knowing who's asking.

Setup

  1. Create an app password at Bluesky: Settings → Privacy and Security → App Passwords
  2. Set environment variables:
    export BSKY_HANDLE="yourhandle.bsky.social"
    export BSKY_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx"
    

Which account reads

Two credential pairs are recognised, and the prefix is the only difference:

Pair Identity
MUNINN_BSKY_HANDLE / MUNINN_BSKY_APP_PASSWORD muninn — preferred
BSKY_HANDLE / BSKY_APP_PASSWORD owner — used when no Muninn pair is set

BSKY_IDENTITY=muninn|owner picks one outright, and a pair named that way is never substituted for: asking for muninn with no Muninn pair set reads as public rather than quietly reading as the owner. An unrecognised value raises.

This matters because a booted container holds both pairs. Until 2026-08-28 the unprefixed one won, so every authenticated read — the following timeline, and the mutes, blocks and labelers that shape all of them — came back as the account owner with nothing in the output saying so, from a skill documented as read-only that was nonetheless holding an app password carrying write scope on his account. On a machine with only the documented BSKY_* pair, nothing changes.

Behavior

  • Transparent: All functions work identically with or without credentials
  • Automatic: Auth headers are added opportunistically when credentials exist
  • Graceful: Failed auth silently falls back to public access. A misspelled BSKY_IDENTITY is configuration, not auth failure, and raises instead
  • Secure: Tokens cached in memory only, never logged or persisted

Check Auth Status

if is_authenticated():
    print(f"Logged in as: {get_authenticated_user()}")
else:
    print("Using public access")

# Which pair answered — a handle alone does not say whether it was chosen
# or simply the only one set.
authenticated_identity()   # {'identity': 'muninn', 'handle': ..., 'did': ...}

# Clear session if needed (e.g., switching accounts or identities)
clear_session()

Research Workflows

Investigate a Topic

Use search_posts() with query syntax matching bsky.app advanced search:

  • Basic terms: event sourcing
  • Exact phrases: "event sourcing"
  • User filter: from:acairns.co.uk or use author= param
  • Date filter: since:2025-01-01 or use since= param
  • Hashtags, mentions, domain links: #python mentions:user domain:github.com

Combine query syntax with function params for complex searches.

Monitor a User

  1. Fetch profile with get_profile(handle) for context (bio, follower count, post count)
  2. Get recent posts with get_user_posts(handle, limit=N)
  3. For topic-specific user content, use search_posts(query, author=handle)

Recommended workflow — trending API first, firehose for deep dives:

topics = get_trending_topics(limit=10)
# Returns: {topics: [{topic, display_name, description, link}, ...],
#           suggested: [...]}
2. Rich trends with post counts and actors
trends = get_trending(limit=10)
for t in trends:
    print(f"{t['display_name']} — {t['post_count']} posts ({t['status']})")
# Each trend includes: topic, display_name, link, started_at,
#   post_count, status, category, actors
posts = search_posts(trend["topic"], limit=25)
4. Optional: Firehose for velocity monitoring or long-tail discovery

Prerequisites: Install Node.js dependencies once per session:

cd /home/claude && npm install ws https-proxy-agent 2>/dev/null
data = sample_firehose(duration=30)  # Full firehose sample
data = sample_firehose(duration=20, filter="python")  # Filtered sample

Returns dict with keys:

  • window: {startTime, endTime, durationSeconds} — sampling time range
  • stats: {totalReceived, totalPosts, postsPerSecond, filter, languages} — volume metrics and language breakdown
  • topWords: [[word, count], ...] — top 50 words (count >= 3)
  • topPhrases: [[bigram, count], ...] — top 30 bigrams (count >= 2)
  • topTrigrams: [[trigram, count], ...] — top 20 trigrams (count >= 2)
  • entities: [[entity, count], ...] — top 25 handles/hashtags (count >= 2)
  • samplePosts: [{text, altTexts, hasImages}, ...] — first 50 matching posts

Read Feeds and Lists

get_feed_posts() accepts:

  • List URLs: https://bsky.app/profile/austegard.com/lists/3lankcdrlip2f
  • Feed URLs: https://bsky.app/profile/did:plc:xxx/feed/feedname
  • AT-URIs: at://did:plc:xxx/app.bsky.graph.list/xyz

The function extracts the AT-URI from URLs automatically.

Explore a Thread

Fetch full thread context for a post with parents and replies:

thread = get_thread("https://bsky.app/profile/user/post/xyz", depth=10)
# Returns: {post: {...}, parent: {...}, replies: [...]}

Find Quote Posts

Discover posts that quote a specific post:

quotes = get_quotes("https://bsky.app/profile/user/post/xyz")
for q in quotes:
    print(f"@{q['author_handle']}: {q['text'][:80]}")

Analyze Engagement

Get users who engaged with a post:

likes = get_likes(post_url)
reposts = get_reposts(post_url)

# Accepts both URLs and AT-URIs
likes = get_likes("at://did:plc:.../app.bsky.feed.post/...")

Read Embed Images

Every parsed post carries an images field — a list of {alt, url, transcription} dicts, one per embed image. The legacy image_alts: list[str] field is preserved (non-empty alts only).

When alt text is missing and the image content matters, opt in to model transcription via the transcribe parameter on any post-fetch function (get_user_posts, search_posts, get_feed_posts, get_thread, get_quotes):

# Routine/bulk work (zeitgeist, inbox review, news scans) —
# gemini-2.5-flash-lite is the recommended default. Cheapest production
# model anywhere ($0.10/$0.40 per 1M tokens), ~95% accuracy on dense
# screenshots in May 2026 benchmarks:
posts = get_user_posts("ayourtch.bsky.social", limit=40, transcribe="gemini-lite")

# Token-perfect transcription, still cheap:
posts = get_user_posts(..., transcribe="gemini-flash")

# Frontier model with thinking_level=minimal — for cases where the image
# content needs reasoning, not just transcription:
posts = get_user_posts(..., transcribe="gemini-3.5-flash")

# Anthropic single-vendor option (note: empirically weaker prompt-following
# than Gemini on dense transcription — Haiku tends to summarize rather
# than transcribe):
posts = get_user_posts(..., transcribe="haiku")

# Interactive sessions where image is part of the active task and you want
# conversation context to inform interpretation (only available on Anthropic):
thread = get_thread(post_url, transcribe="opus")

# Default (no transcription) — current behavior preserved:
posts = get_user_posts("ayourtch.bsky.social", limit=40)

Policy is invariant across all callers: images with non-empty alt text are never transcribed (the author already described the image; trust it). Only images with missing or empty alt are sent to the model. Network or API failures leave transcription as None; callers degrade silently.

Cost/quality empirics (May 2026, n=3 dense terminal screenshots, single run each — sample size is small, treat as directional):

Alias Latency $/image Chord-token recall
gemini-lite ~8s ~$0.001 95%
gemini-flash ~10s ~$0.003 100%
gemini-3.5-flash ~10s ~$0.014 100%
haiku ~7s ~$0.008 18% (summarizes)
opus ~20s ~$0.12 91%

The haiku and opus rows were measured on Haiku 4.5 and Opus 4.7. The aliases now resolve to claude-haiku-5-5 and claude-opus-5-5; those rows have not been re-measured.

Requires either ANTHROPIC_API_KEY (or API_KEY in /mnt/project/claude.env) for the haiku / opus aliases, or CF AI Gateway credentials in /mnt/project/proxy.env for the gemini-* aliases. Transcription only fires when the parameter is set, so callers without the relevant credentials can simply pick a different alias or leave the feature off.

Explore Social Graph

Navigate follower/following relationships:

followers = get_followers("handle.bsky.social")
following = get_following("handle.bsky.social")

# Returns list of actor dicts with handle, display_name, did, description, etc.

Find Users

Search for users by name, handle, or bio:

users = search_users("machine learning researcher")
for u in users:
    print(f"{u['display_name']} (@{u['handle']}): {u['description'][:100]}")

API Endpoint Notes

  • Public AppView: https://api.bsky.app/xrpc/ for unauthenticated reads
  • PDS: https://bsky.social/xrpc/ for authenticated requests
  • Trending: app.bsky.unspecced.getTrends (rich) and app.bsky.unspecced.getTrendingTopics (lightweight)
  • Firehose: wss://jetstream1.us-east.bsky.network/subscribe
  • Endpoint routing is automatic - authenticated requests go to PDS, public requests go to AppView
  • Rate limits exist but are generous for read operations

Return Format

All API functions return structured dicts with:

  • uri: AT protocol identifier
  • text: Post content
  • created_at: ISO timestamp
  • author_handle: User handle
  • author_name: Display name
  • likes, reposts, replies: Engagement counts
  • links: Full URLs extracted from post facets (post text truncates URLs with "...")
  • image_alts: Alt text from embedded images
  • url: Direct link to post on bsky.app

Profile function returns: handle, display_name, description, followers, following, posts, did

Account Analysis

Analyze accounts for categorization by topic. Fetches profile and posts, extracts keywords, and returns structured data for Claude to categorize.

Analyze a User's Network

# Analyze accounts you follow
results = analyze_accounts(following="yourhandle.bsky.social", limit=50)

# Analyze your followers
results = analyze_accounts(followers="yourhandle.bsky.social", limit=50)

# Analyze specific handles
results = analyze_accounts(handles=["user1.bsky.social", "user2.bsky.social"])

Single Account Analysis

analysis = analyze_account("user.bsky.social")
# Returns: {handle, display_name, description, keywords, post_count, followers, following}

Keyword Extraction Options

Stopwords parameter filters domain-specific noise:

  • "en": English (general purpose, default)
  • "ai": AI/ML domain (filters tech boilerplate)
  • "ls": Life Sciences (filters research methodology)
results = analyze_accounts(following="handle", stopwords="ai")

Requires: extracting-keywords skill with YAKE venv for keyword extraction.

Filtering Accounts

results = analyze_accounts(
    following="handle",
    exclude_patterns=["bot", "spam", "promo"]  # Skip accounts matching these
)

Paginated Following/Followers

For large account lists beyond the 100 limit of get_following/get_followers:

all_following = get_all_following("handle", limit=500)  # Handles pagination
all_followers = get_all_followers("handle", limit=500)

Account Analysis Output

Each analyzed account returns:

{
    "handle": "user.bsky.social",
    "display_name": "User Name",
    "description": "Bio text here",
    "keywords": ["keyword1", "keyword2", "keyword3"],
    "post_count": 20,
    "followers": 1234,
    "following": 567
}

Claude uses bio + keywords to categorize accounts by topic without hardcoded rules

Tips for Browsing Bluesky

  • • Start a fresh session after installing so your agent picks up the new skill.
  • • This skill ships scripts. Read them before the first run, and run it in a project without secrets first.
  • • It is part of a pack with 30 skills. Install only the ones you need with --skill to keep triggers precise.
  • • If two skills overlap, mention the one you want by name.

Questions about Browsing Bluesky

What does the Browsing Bluesky skill do?

Browse Bluesky content via API and firehose - search posts, fetch user activity, sample trending topics, read feeds and lists, analyze and categorize accounts. Supports authenticated access for personalized feeds. Use for Bluesky research, user monitoring, trend analysis, feed reading, firehose sampling, account categorization.

How do I install Browsing Bluesky?

Run "npx skills add oaustegard/claude-skills --skill browsing-bluesky" in your project. The skills CLI installs it for Claude Code, Cursor, Codex and 70+ other agents. In Claude on the web, upload the skill folder as a ZIP under Customize > Skills.

Does Browsing Bluesky work in Cursor and Codex?

Yes. Browsing Bluesky uses the open SKILL.md format, so it works in Claude Code, Claude, Cursor, OpenAI Codex, Gemini CLI, GitHub Copilot and other agents that support agent skills.

Is Browsing Bluesky free?

Browsing Bluesky is open source on GitHub under the MIT license. It is free to install; check the license before commercial use.

Who made Browsing Bluesky?

Browsing Bluesky is published by oaustegard in the oaustegard/claude-skills repository, together with 29 other skills.

Similar skills. From other creators.