Auto-Sync Twitter Bookmarks to Obsidian with Bird CLI
Auto-Sync Twitter Bookmarks to Obsidian with Bird CLI
I bookmark a lot on Twitter/X. AI tools, Claude Code plugins, research papers, workflow tips. The good stuff that shows up in my feed but I don't have time to process immediately.
The problem? Twitter bookmarks are a black hole. You save them, they pile up, and you never see them again.
I wanted these bookmarks flowing into my Obsidian vault automatically. Here's how I built it using Bird CLI and a simple Python script.
The Problem
Twitter bookmarks are where good ideas go to die. I was bookmarking 10-20 tweets a day about AI tools, agent patterns, and dev workflows, but they just sat there. No search, no integration with my knowledge base, no way to process them systematically.
I needed bookmarks flowing into Obsidian where I could:
- Search across them semantically
- Link them to projects and notes
- Process them with AI for categorization
- Actually reference them later
The Solution
Bird CLI + Python + LaunchAgent = automated bookmark sync every 2 hours.
Stack
- Bird CLI: Fast Twitter/X client for terminal access by @steipete
- Python script: Polls bookmarks, deduplicates, formats to Obsidian markdown
- LaunchAgent: Runs every 2 hours on macOS
Setup
1. Install Bird CLI
Bird CLI is a fast Twitter/X client built by @steipete.
brew install steipete/tap/bird
2. Authenticate Bird
Login to X/Twitter in Safari (Bird reads cookies from your browser):
bird whoami
3. Create the processor script
`(local path)
#!/usr/bin/env python3
"""
Twitter Bookmark Processor
Polls Bird CLI for new bookmarks and processes them into Obsidian format.
"""
import json
import subprocess
import sys
from datetime import datetime
from pathlib import Path
BOOKMARKS_FILE = Path.home() / "vaults/bioinfo/Knowledge/Bookmarks.md"
STATE_FILE = Path.home() / ".config" / "bird" / "last_processed.json"
def get_bookmarks(count=50):
"""Fetch bookmarks from Bird CLI."""
try:
result = subprocess.run(
["bird", "bookmarks", "-n", str(count), "--json"],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
return []
bookmarks = json.loads(result.stdout)
return bookmarks if isinstance(bookmarks, list) else []
except Exception as e:
print(f"Failed to fetch bookmarks: {e}", file=sys.stderr)
return []
def load_state():
"""Load the last processed bookmark ID."""
if not STATE_FILE.exists():
return None
try:
with open(STATE_FILE) as f:
return json.load(f).get("last_id")
except Exception:
return None
def save_state(bookmark_id):
"""Save the last processed bookmark ID."""
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump({"last_id": bookmark_id, "timestamp": datetime.now().isoformat()}, f)
def format_bookmark(bookmark):
"""Format a bookmark into Obsidian markdown."""
tweet_id = bookmark.get("id", "")
author = bookmark.get("author", {})
username = author.get("username", "")
name = author.get("name", "")
text = bookmark.get("text", "").replace("&", "&")
quoted = bookmark.get("quotedTweet")
url = f"https://x.com/{username}/status/{tweet_id}"
output = [f"## @{username} - {name}"]
if quoted:
quoted_author = quoted.get("author", {})
quoted_text = quoted.get("text", "")
output.append(f"> *Quoting @{quoted_author.get('username', '')}:* {quoted_text}")
output.append(">")
for line in text.split("\n"):
output.append(f"> {line}")
output.append("")
output.append(f"- **Tweet:** {url}")
output.append("- **What:** [AI analysis placeholder]")
output.append("")
return "\n".join(output)
def main():
bookmarks = get_bookmarks(count=50)
if not bookmarks:
return
last_id = load_state()
# Filter to new bookmarks only
if last_id:
new_bookmarks = []
for bm in bookmarks:
if bm.get("id") == last_id:
break
new_bookmarks.append(bm)
bookmarks = new_bookmarks
if bookmarks:
# Process and append to markdown file
# ... formatting logic ...
save_state(bookmarks[0].get("id"))
if __name__ == "__main__":
main()
Make it executable:
chmod +x (local path)
4. Create LaunchAgent for automation
`(local path)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.bioinfo.twitter-bookmarks</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/python3</string>
<string>/Users/YOUR_USERNAME/scripts/twitter-bookmark-processor.py</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>StartInterval</key>
<integer>7200</integer>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
5. Load the agent
launchctl load (local path)
How It Works
Polling Strategy
Every 2 hours, the script:
- Fetches your 50 most recent bookmarks via Bird CLI
- Checks the last processed bookmark ID from state file
- Filters to only new bookmarks since last run
- Formats them as Obsidian markdown
- Prepends to your bookmarks file
- Saves the newest bookmark ID
Deduplication
The script tracks state in `(local path)
{
"last_id": "2010330642714894391",
"timestamp": "2026-01-12T09:40:33.069702"
}
On each run, it only processes bookmarks newer than last_id, preventing duplicates.
Format
Bookmarks get formatted as:
# Sunday, January 11, 2026
## @username - Display Name
> Tweet text here
> Multiple lines preserved
- **Tweet:** https://x.com/username/status/123456789
- **What:** [AI analysis placeholder]
Chronological Sorting
The script groups bookmarks by date and sorts chronologically (newest first), so your most recent bookmarks always appear at the top of the file.
Why This Approach Works
Lightweight
No API keys, no authentication flows, no rate limits. Bird CLI reads cookies from your browser. The Python script is 200 lines with zero dependencies beyond stdlib.
Resilient
If Bird fails to fetch bookmarks, the script logs the error and exits cleanly. LaunchAgent retries in 2 hours. State file ensures no bookmarks are lost or duplicated.
Extensible
The "What:" placeholder is perfect for AI analysis. You can pipe bookmarks through Claude to categorize them:
def analyze_bookmark(text):
# Call Claude API to categorize
# "AI tool", "Research paper", "Workflow tip", etc.
pass
Or link extraction to expand t.co URLs:
def expand_urls(text):
# Resolve shortened URLs
# Extract GitHub repos, papers, blog posts
pass
Real-World Usage
I've been running this for a few weeks. Here's what I've learned:
Search is Key
Once bookmarks are in Obsidian, you can search across them. With semantic search via vector embeddings, you can find tweets about "agent orchestration patterns" even if they never used those exact words.
Processing Workflow
I have a weekly review where I:
- Open
Bookmarks.md - Scan new entries (everything since last review)
- Extract actionable items to TODO lists
- Link relevant tweets to project notes
- Archive or delete noise
Integration Points
Bookmarks feed into:
- Project notes: Link relevant tweets to active projects
- Research notes: Pull in papers and technical threads
- Tool tracking: Monitor new AI tools and frameworks
- Learning queue: Tutorials and guides to try later
Variations
Different Intervals
For less frequent syncing:
<key>StartInterval</key>
<integer>21600</integer> <!-- 6 hours -->
For real-time (every minute):
<key>StartInterval</key>
<integer>60</integer>
Different Formats
Want a flat list instead of grouped by date?
def format_flat(bookmarks):
output = []
for bm in bookmarks:
output.append(format_bookmark(bm))
return "\n".join(output)
Multiple Collections
Bird supports bookmark folders (collections). Sync different folders to different files:
bird bookmarks --folder-id abc123 -n 50 --json
Troubleshooting
"No Twitter cookies found in Safari"
Bird needs you logged into X/Twitter in Safari. If you use Chrome or Firefox:
bird --chrome-profile Default bookmarks -n 5
LaunchAgent not running
Check if it's loaded:
launchctl list | grep twitter-bookmarks
View logs:
tail -f (local path)
Bookmarks out of order
The script sorts chronologically. If existing entries are misordered, run a one-time reorganization:
# Parse all date headers
# Sort by actual datetime
# Rewrite file in proper order
Alternatives
Official Twitter API
Requires developer account, app creation, OAuth flow. Rate limits are strict (75 requests per 15 minutes for bookmark endpoints).
Bird CLI bypasses all of this by reading browser cookies.
Browser Extensions
Extensions like Obsidian Web Clipper can save individual tweets, but require manual clicking.
This approach is fully automated and processes all bookmarks in batch.
Readwise + Obsidian
Readwise has a Twitter integration, but it's designed for highlights and threads, not bookmarks. Also requires a paid subscription.
Next Steps
This system handles the sync, but the real value comes from processing. I'm experimenting with:
- AI categorization: Claude analyzes each bookmark and tags it (tool, paper, workflow, etc.)
- Link expansion: Resolve t.co URLs and fetch page titles
- Thread extraction: When a bookmark is part of a thread, fetch the entire thread
- Duplicate detection: Identify when multiple people share the same link
- Priority scoring: Rank bookmarks by relevance to active projects
The syncing is solved. Now it's about turning raw bookmarks into actionable knowledge.
Related Articles
- Building an AI Research Night Shift: Automated Learning While You SleepshippedPractical ApplicationsNov 6, 2025My AI Research Assistant Works the Night Shift (A Claude Code Skill Story)How I built a Claude Code skill that researches AI developments overnight using intelligent automation that adapts, prevents duplicates, and provides instant answers.
- Automating Claude Code CLI Tool UpdatesshippedPractical ApplicationsJan 12, 2026Auto-Updating CLI Tools for Claude Code SkillsBuild an automated system that discovers CLI dependencies from Claude Code skill metadata and keeps them updated weekly via LaunchAgent.
- Syncing Claude Code Configurations Across Multiple Machines: A Practical GuideshippedPractical ApplicationsOct 20, 2025Syncing Claude Code Configurations Across Multiple Machines: A Practical GuideLearn how to intelligently sync Claude Code configurations across Mac, Pi, and DGX boxes while preserving machine-specific settings like model endpoints and API keys
About the Author: Justin Johnson builds AI systems and writes about practical AI development.
justinhjohnson.com | Twitter | LinkedIn | Run Data Run | Subscribe
Follow the lab
Get the next experiment
Enjoyed the breakdown on Auto-Sync Twitter Bookmarks to Obsidian with Bird CLI? New entries land roughly weekly. No digest, no roundup. Just the next build log, when it ships.
Related experiments
Apparatus
922 words · 12 min read
- automation
- obsidian
- bird-cli
- knowledge-management
- python