- Fix plugin.json hooks field from invalid array to empty object - Add Claude Code plugin install above ClawHub badge - Version bump to v2.9.5 with new features block (Bluesky, comparative mode, ScrapeCreators X, per-project env, expanded tests) - Add Bluesky to all source list references - Document BSKY_HANDLE/BSKY_APP_PASSWORD env vars in install + optional sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.7 KiB
title, type, status, date
| title | type | status | date |
|---|---|---|---|
| fix: Make Bluesky opt-in with auth (searchPosts now requires authentication) | fix | completed | 2026-03-09 |
fix: Make Bluesky opt-in with auth (searchPosts now requires authentication)
Problem
Bluesky's app.bsky.feed.searchPosts endpoint now returns 403 Forbidden for unauthenticated requests. This broke today (2026-03-09), likely related to the CEO transition. The searchActors endpoint still works without auth, but searchPosts does not.
We built Bluesky as an always-on source (like HN and Polymarket) because the AT Protocol public API was documented as free. That's no longer true for post search.
Current behavior: Bluesky silently returns 0 results (403 caught, empty list returned). No stats line appears, no error shown. Users don't know it exists.
Desired behavior: Bluesky is opt-in via env vars. When not configured, completely invisible (no error, no stats line, no mention). When configured with auth, it works as before.
Approach
Follow the same pattern as X/Twitter auth: env vars for credentials, availability check gates dispatch.
AT Protocol auth flow:
- User creates an App Password at
bsky.app/settings/app-passwords - User sets
BSKY_HANDLEandBSKY_APP_PASSWORDenv vars bluesky.pycallscom.atproto.server.createSessionto get a bearer token- Subsequent
searchPostscalls includeAuthorization: Bearer {token}
Files to Change
| File | Change | Lines (est.) |
|---|---|---|
scripts/lib/bluesky.py |
Add _create_session() auth, pass bearer token to search |
~25 |
scripts/lib/env.py |
Update is_bluesky_available() to check env vars; add env var loading |
~10 |
scripts/last30days.py |
Gate do_bluesky on is_bluesky_available(config); update diag dicts |
~8 |
tests/test_bluesky.py |
Add auth session tests, update existing tests | ~15 |
SKILL.md |
Document BSKY_HANDLE / BSKY_APP_PASSWORD env vars in setup section |
~5 |
Implementation Steps
1. scripts/lib/env.py
- Add
BSKY_HANDLEandBSKY_APP_PASSWORDto the env var loading inget_config() - Update
is_bluesky_available():def is_bluesky_available(config: Dict[str, Any]) -> bool: return bool(config.get('BSKY_HANDLE') and config.get('BSKY_APP_PASSWORD'))
2. scripts/lib/bluesky.py
- Add
_create_session(handle, app_password)that POSTs tohttps://bsky.social/xrpc/com.atproto.server.createSession - Cache the access token for the lifetime of the search (module-level or passed through)
- Update
search_bluesky()to accept config dict, extract creds, create session, addAuthorization: Bearer {token}header - On auth failure, return
{"posts": [], "error": "Bluesky auth failed"}(don't raise)
3. scripts/last30days.py
- Change
do_blueskydefault fromTrueto checkingis_bluesky_available(config):has_bluesky = env.is_bluesky_available(config) - Gate the
bluesky_futuresubmit onhas_bluesky(already gated ondo_bluesky, just wire it) - Update both diagnostic dicts:
"bluesky": True->"bluesky": has_bluesky - Pass
configto_search_bluesky()so it can extract auth creds
4. tests/test_bluesky.py
- Test
_create_sessionwith mocked HTTP response - Test
search_blueskywith auth header injection - Existing parse/normalize tests don't change (they don't hit the network)
5. SKILL.md
- Add Bluesky to the optional env vars section (near AUTH_TOKEN/CT0):
BSKY_HANDLE=your-handle.bsky.social BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx - Keep Bluesky in source lists but note "(requires app password)" in setup
What NOT to Change
render.py- already handles empty bluesky gracefully (hides stats line when 0 items)schema.py,normalize.py,score.py,dedupe.py- no changes needed, they work on items already- No error display when unconfigured - completely silent, same as how TikTok/Instagram behave when SCRAPECREATORS_API_KEY is missing
Acceptance Criteria
is_bluesky_available()returns False when env vars not set- No Bluesky stats line, no error, no mention when unconfigured
- With valid
BSKY_HANDLE+BSKY_APP_PASSWORD, posts are returned - Auth failure returns empty results gracefully (no crash)
- SKILL.md documents the env vars
- All existing Bluesky tests still pass
- New auth tests added
bash scripts/sync.shdeploys successfully
Sources
- AT Protocol auth:
POST https://bsky.social/xrpc/com.atproto.server.createSessionwith{identifier, password} - App passwords:
bsky.app/settings/app-passwords - Existing pattern: X auth with AUTH_TOKEN/CT0 in
env.py:256-257 - Existing pattern: TikTok/Instagram opt-in via
is_tiktok_available()inenv.py:499