birdclaw is a local-first Twitter workspace: archive import, cached live reads, focused triage, and reply flows in one local web app + CLI. Built by @steipete.
Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and rough edges while the core settles.
~/.birdclawxurl or bird~/.birdclawHome timelineMentions queueLikes and Bookmarks review lanesDMs workspace with two-column layoutInbox for mixed mention + DM triageBlocks for local blocklist maintenancexurl-compatible JSONxurl when availableIf you need polished product-grade sync parity today, this is not there yet.
Home: read and reply without fighting the main Twitter timelineMentions: work the reply queue with clean filtersLikes / Bookmarks: revisit saved posts from archive or live syncDMs: triage by sender context, follower count, and influenceInbox: let heuristics / OpenAI float likely-important itemsBlocks: maintain a local-first account-scoped blocklistDefault root:
~/.birdclaw
Important paths:
~/.birdclaw/birdclaw.sqlite~/.birdclaw/media~/.birdclaw/media/thumbs/avatars.playwright-homeOverride the root:
export BIRDCLAW_HOME=/path/to/custom/root
25.8.1pnpmxurl optional for live reads / writesbird optional for cookie-backed likes, bookmarks, mentions, DMs, and write fallbackHomebrew:
brew install steipete/tap/birdclaw
From source:
fnm use
pnpm install
pnpm dev
Open:
http://localhost:3000
Initialize local state:
birdclaw init
birdclaw auth status --json
birdclaw db stats --json
Find and import an archive:
birdclaw archive find --json
birdclaw import archive --json
birdclaw import archive ~/Downloads/twitter-archive-2025.zip --json
birdclaw import hydrate-profiles --json
Back up the local SQLite store as canonical JSONL text:
birdclaw backup sync --repo ~/Projects/backup-birdclaw --remote https://github.com/steipete/backup-birdclaw.git --json
Merge the backup into the current BIRDCLAW_HOME:
birdclaw backup import ~/Projects/backup-birdclaw --json
Start the app:
birdclaw serve
First moderation pass:
pnpm cli mentions export --mode xurl --refresh --all --max-pages 9 --limit 100
pnpm cli profiles replies @borderline_handle --limit 12 --json
pnpm cli blocks import ~/triage/blocklist.txt --account acct_primary --json
pnpm cli search tweets "local-first" --json
pnpm cli search tweets "sync engine" --limit 20 --json
pnpm cli search tweets --since 2020-01-01 --until 2021-01-01 --originals-only --hide-low-quality --limit 500 --json
pnpm cli search tweets --liked --limit 20 --json
pnpm cli search tweets --bookmarked --limit 20 --json
auto tries xurl first, then falls back to bird. Use bird directly when the API path is unavailable for the account/token you have locally.
pnpm cli sync likes --mode auto --limit 100 --refresh --json
pnpm cli sync bookmarks --mode auto --limit 100 --refresh --json
pnpm cli sync bookmarks --mode bird --all --max-pages 5 --limit 100 --refresh --json
pnpm cli sync timeline --limit 100 --refresh --json
pnpm cli sync mention-threads --limit 30 --delay-ms 1500 --timeout-ms 15000 --json
Default birdclaw mode returns normalized items with text, plainText, markdown, author metadata, and canonical URLs:
pnpm cli mentions export "agent" --unreplied --limit 10
Cached live modes return xurl-compatible data/includes/meta, but stay in the local SQLite cache so repeat reads do not keep spending live calls:
pnpm cli mentions export --mode bird --limit 20
pnpm cli mentions export --mode bird --refresh --limit 20
pnpm cli mentions export --mode xurl --limit 5
pnpm cli mentions export --mode xurl --refresh --limit 5
pnpm cli mentions export --mode xurl --refresh --all --max-pages 9 --limit 100
pnpm cli mentions export "courtesy" --mode xurl --limit 5
Home config lives in ~/.birdclaw/config.json. Example:
{
"actions": {
"transport": "auto"
},
"mentions": {
"dataSource": "bird",
"birdCommand": "/Users/steipete/Projects/bird/bird"
}
}
Notes:
--refresh forces a live fetch--cache-ttl <seconds> tunes freshness--all walks every retrievable mentions page; --max-pages caps that scanxurl mode, --limit is the per-page sizementions.dataSource controls live mention reads onlyactions.transport controls live block/mute writes onlyactions.transport accepts auto, bird, or xurlbird mode uses your local bird CLI and caches its mentions output into birdclaw’s canonical storexurl mode; filtered payloads are rebuilt from the local canonical store after syncsync likes, sync bookmarks, sync timeline, and sync mention-threads store live results in the same local timeline table, so search tweets, search tweets --liked, and search tweets --bookmarked work across archive and live databirdclaw research turns bookmarked tweets into a markdown brief with local thread expansion, live ancestor lookup when needed, and extracted links/handles:
birdclaw research "codex" --limit 20 --thread-depth 10 --json
birdclaw research --account acct_primary --out ~/research/codex.md
pnpm cli search dms "prototype" --json
pnpm cli search dms "layout" --min-followers 1000 --min-influence-score 120 --sort influence --json
pnpm cli dms sync --limit 50 --refresh --json
pnpm cli dms list --refresh --limit 10 --json
pnpm cli dms list --unreplied --min-followers 500 --min-influence-score 90 --sort influence --json
pnpm cli inbox --json
pnpm cli inbox --kind dms --limit 10 --json
pnpm cli inbox --score --hide-low-signal --limit 8 --json
pnpm cli blocks list --account acct_primary --json
pnpm cli blocks sync --account acct_primary --json
pnpm cli blocks import ~/triage/blocklist.txt --account acct_primary --json
pnpm cli blocks add @amelia --account acct_primary --json
pnpm cli blocks record @amelia --account acct_primary --json
pnpm cli blocks remove @amelia --account acct_primary --json
pnpm cli ban @amelia --account acct_primary --transport auto --json
pnpm cli unban @amelia --account acct_primary --transport bird --json
Notes:
ban / unban accept --transport auto|bird|xurlauto tries bird first, then falls back to xurl, then x-web cookie-backed block/unblock when both failxurl writes still verify through bird status before sqlite changesauto is the safe default for block/unblockblocks import accepts newline-delimited blocklists with comments and markdown bulletsblocks sync is for slow/manual remote reconciliation; not for a hot cron loopblocks record stores a known-good remote block locally without issuing another live writeExample blocklist file:
# crypto / AI slop
@jpctan
@SystemDaddyAi
- @Pepe202579 memecoin bait
https://x.com/someone/status/2030857479001960633?s=20
pnpm cli profiles replies @jpctan --limit 12 --json
Notes:
Typical tell:
pnpm cli mutes list --account acct_primary --json
pnpm cli mute @amelia --account acct_primary --transport xurl --json
pnpm cli mutes record @amelia --account acct_primary --json
pnpm cli unmute @amelia --account acct_primary --transport auto --json
Notes:
mute / unmute accept --transport auto|bird|xurlbird user --json before any xurl /2/users lookupauto tries bird first, then falls back to xurl when bird failsxurl writes still verify through bird status before sqlite changesmutes record stores a known-good remote mute locally without issuing another live write--localstorage-file from NODE_OPTIONS before starting Vitepnpm cli compose post "Ship local software."
pnpm cli compose reply tweet_004 "On it."
pnpm cli compose dm dm_003 "Send it over."
birdclaw backup export writes deterministic JSONL shards that can rebuild the local SQLite index without committing SQLite WAL/SHM files, FTS shadow tables, or transient live caches.
Layout:
manifest.json
data/accounts.jsonl
data/profiles.jsonl
data/tweets/YYYY.jsonl
data/tweets/unknown.jsonl
data/collections/likes.jsonl
data/collections/bookmarks.jsonl
data/dms/conversations.jsonl
data/dms/YYYY.jsonl
data/moderation/blocks.jsonl
data/moderation/mutes.jsonl
Tweets are sharded by year for human browsing and yearly analysis. Collection-only tweets whose real creation date is unknown go into data/tweets/unknown.jsonl instead of pretending they belong to 1970. DMs are sharded by year with conversation_id in each row; this keeps Git fast while preserving conversation membership. Likes and bookmarks are stored as collection edges in data/collections and mirrored into the timeline rows for current query compatibility.
Use backup sync when the target is a private Git repo. It pulls first, merge-imports the remote backup into local SQLite, exports the local union back into text shards, commits, and pushes.
pnpm cli backup sync --repo ~/Projects/backup-birdclaw --remote https://github.com/steipete/backup-birdclaw.git --json
pnpm cli backup validate ~/Projects/backup-birdclaw --json
Configure stale-aware backup reads in ~/.birdclaw/config.json:
{
"backup": {
"repoPath": "/Users/steipete/Projects/backup-birdclaw",
"remote": "https://github.com/steipete/backup-birdclaw.git",
"autoSync": true,
"staleAfterSeconds": 900
}
}
Read paths such as CLI search, inbox, API status/query, and web startup pull + merge from Git only when the last backup check is stale. Data-changing commands run a full backup sync afterward when this config is enabled. Set BIRDCLAW_BACKUP_AUTO_SYNC=0 to disable that behavior for one process.
birdclaw jobs sync-bookmarks refreshes live bookmarks and appends one JSONL audit entry per run. Each entry includes host, timestamps, duration, before/after bookmark counts, source transport, fetched count, backup sync result, and any error.
birdclaw --json jobs sync-bookmarks --mode auto --limit 100 --max-pages 5 --refresh
tail -n 5 ~/.birdclaw/audit/bookmarks-sync.jsonl | jq .
After a successful bookmark refresh, the job runs the normal backup auto-sync path. If ~/.birdclaw/config.json has backup.autoSync enabled, the changed local data is merged into the configured Git backup repo, committed, and pushed. The audit entry records that backup result so scheduled runs are inspectable later.
On macOS, install the 3-hour LaunchAgent after choosing the Birdclaw executable path for that machine:
birdclaw --json jobs install-bookmarks-launchd --program /opt/homebrew/bin/birdclaw
If the machine uses bird with browser cookies that are not available to launchd, write an export-only env file with mode 0600 and install with --env-file ~/.config/bird/env.sh. Birdclaw sources that file inside the scheduled process without storing the secrets in the plist.
The LaunchAgent writes ~/Library/LaunchAgents/com.steipete.birdclaw.bookmarks-sync.plist, runs at load, then every 10,800 seconds. It writes the audit log to ~/.birdclaw/audit/bookmarks-sync.jsonl and stdout/stderr to ~/.birdclaw/logs/bookmarks-sync.*.log. A lock file prevents overlapping runs and records an already-running skip when needed. The default job fetches up to 5 pages every 3 hours; pass --all if you want every retrievable page each run.
Useful checks:
launchctl print gui/$(id -u)/com.steipete.birdclaw.bookmarks-sync
launchctl kickstart -k gui/$(id -u)/com.steipete.birdclaw.bookmarks-sync
tail -n 1 ~/.birdclaw/audit/bookmarks-sync.jsonl | jq .
Home for readingMentions for reply triageprofiles repliesblocks importDMs for high-context conversation workInbox when you want AI help cutting noiseCurrent preference:
xurl firstbird fallback for surfaces where cookie-backed reads work betterWithout xurl or bird, birdclaw still works in local/archive mode.
Check transport:
pnpm cli auth status --json
fnm exec --using 25.8.1 pnpm check
fnm exec --using 25.8.1 pnpm test
fnm exec --using 25.8.1 pnpm coverage
fnm exec --using 25.8.1 pnpm build
fnm exec --using 25.8.1 pnpm e2e
Current bar:
80%GitHub Actions runs:
pnpm checkpnpm coveragepnpm buildpnpm e2eWorkflow: ci.yml