Search Threads. Filter precisely. Keep the evidence.
English · 简体中文 · MIT license
An unofficial Threads CLI for turning fuzzy social search into a useful local collection. Search posts, inspect replies and profiles, apply strict filters, and export source-linked evidence as JSON. Account notifications and explicit text publishing are also available through the browser bridge.
Current setup: authenticated commands require an existing Dia browser connection in Codex Desktop and its trusted Node runtime. Installing the CLI alone does not provide that connection. Offline commands and a limited anonymous public-page mode work from a terminal. Other browser environments are unverified.
Threads search is fuzzy. A query containing two words does not guarantee both
appear in every result. threads-cli adds a deterministic second pass:
- Precise filters: required terms, exclusions, dates, and reply-count limits.
- Traceable results: original text, author, URL, timestamps, and collection status.
- Reusable local data: SQLite checkpoints, JSON export, and offline inspection.
- Bounded collection: explicit page limits, partial-result reporting, and shared pacing.
- Explicit publishing: local previews, account checks, and a persistent outbox that prevents blindly repeating an uncertain send.
A built-in CS2 teammate preset demonstrates multi-query collection, source-backed assessment, and one candidate per person. General search works with any topic.
Requires Python 3.12+ and uv. Browser-backed commands also need the connected browser's trusted Node runtime.
uv tool install 'git+https://github.com/hhh2210/threads-cli.git'
threads doctor --json
threads --helpThe distribution is named larry-threads-cli; the command is threads.
doctor reports local setup and helper paths; it does not prove a live login.
For a source checkout:
git clone https://github.com/hhh2210/threads-cli.git
cd threads-cli
uv sync --locked
uv tool install --editable .- Log in to Threads normally in Dia and connect the existing browser to Codex Desktop.
- In the trusted Node REPL, use the connection's documented API to obtain a task-owned Threads tab. Read the runtime's browser, confirmation, and CDP documentation. The helper navigates this tab, so use one without an active draft.
- Import
helper_pathfromthreads doctor --json, then run:
// tab: a task-owned tab from the existing Dia connection.
const { runBrowserCli } = await import("<helper_path from threads doctor>");
const result = await runBrowserCli(tab, [
"search", "open source", "--sort", "recent", "--pages", "2", "--limit", "50",
"--all", "open", "--all", "source", "--exclude", "giveaway", "--since", "14d"
]);
nodeRepl.write(result);The helper defaults to ~/.local/bin/threads. If uv installed it elsewhere,
pass { executable: "/absolute/path/to/threads" } as the third argument.
See the browser workflow for runtime discovery.
The runtime is supplied by the host; this repository does not ship a standalone
browser driver or create a browser, extension, background service, or public port.
browser_connection_required means the command needs this driver. It is not a
request for a token to paste into your terminal. Authentication stays in the
browser: no cookie export, keychain access, password collection, or credential replay.
| Option | Meaning |
|---|---|
--all TEXT (repeatable) |
Require every term locally, with script normalization |
--exclude TEXT (repeatable) |
Reject results containing any excluded term |
--since 14d / --since YYYY-MM-DD |
Require a known timestamp after the cutoff |
--max-replies N |
Require a known reply count at or below the limit |
--sort top|recent |
Select the upstream search surface |
--pages N / --limit N |
Bound collection pages / returned unique results |
A whole page may be cached before the output limit is applied. These filters refine observed results; they cannot recover posts the platform did not return.
await runBrowserCli(tab, ["read", "https://www.threads.com/@user/post/shortcode"]);
await runBrowserCli(tab, ["user", "username"]);Local commands need no browser:
threads runs --json
threads candidates --include-review --json
threads export evidence.json
threads import evidence.jsonData lives in ~/.local/share/threads-cli. Use --data-dir PATH or
THREADS_CLI_HOME for a separate dataset. Imports accept normalized schema-v1
post evidence, not arbitrary HTML. Raw bootstrap pages, request headers, and
raw GraphQL bodies are not saved to the evidence database.
For a limited anonymous public window:
threads --auth public search 'open source' --pages 1 --jsonAnonymous results can omit posts available when logged in. An empty response is not proof that no matching posts exist.
Bulk hidden words
Import a whole word list into Threads' Custom filters in Hidden Words. This changes your account's actual filter, not just the CLI's local search exclusions. The current English web interface is supported; the older mobile-only word-list screen and other web languages are unverified.
Put one word or phrase per line in words.txt. English/Chinese commas also work;
spaces inside phrases are preserved. Preview without connecting:
threads hidden-words add --filter "My filter" --file words.txt --jsonInspect your filters, check additions against the live list, then apply through the existing browser helper (using the expected Threads account):
const options = { viewer: "your_handle" };
await runBrowserCli(tab, ["hidden-words", "list"], options);
await runBrowserCli(tab, ["hidden-words", "add", "--filter", "My filter",
"--file", "/absolute/path/words.txt", "--check"], options);
await runBrowserCli(tab, ["hidden-words", "add", "--filter", "My filter",
"--file", "/absolute/path/words.txt", "--apply"], options);Add --create only to allow a missing filter to be created. New filters are
active for anyone's posts until turned off. Existing filters retain their
enabled/disabled state, audience scope, duration, description, and old words.
Duplicate input and already-present words are skipped. Writes use batches of 50
in the editor, a single Save, then a full reload/read-back check. Repeating an
already-completed import returns unchanged.
See the detailed reference for input limits, recovery, and the live verification record.
Set your expected public handle in ~/.local/share/threads-cli/config.toml:
viewer = "your_handle"This is an account check and tracking preference, not authentication. You can
also pass { viewer: "your_handle" } as the helper's third argument.
await runBrowserCli(tab, ["notifications", "--kind", "all", "--limit", "50"]);
await runBrowserCli(tab, ["inbox", "--limit", "50"]); // incoming public repliesPreview a text post or reply locally:
threads --viewer your_handle post --text-file /absolute/path/post.txt
threads --viewer your_handle reply https://www.threads.com/@author/post/code \
--text-file /absolute/path/reply.txtPublish only when intended, through the connected browser:
await runBrowserCli(tab, ["reply", "https://www.threads.com/@author/post/code",
"--text-file", "/absolute/path/reply.txt", "--send"]);
await runBrowserCli(tab, ["post", "--text-file", "/absolute/path/post.txt", "--send"]);The outbox tracks sender, target, and exact text. An uncertain send requires
read-only reconciliation before another attempt. Never automatically retry it.
inbox reports incoming public replies; private messages use the separate dm
commands for existing one-to-one conversations. See command reference and
verification limits before using experimental writing features.
Live checks were performed on 2026-09-08/09 in the author's Dia/Codex setup. They describe that tested environment, not a compatibility guarantee.
| Capability | Status |
|---|---|
| Account status, paginated search, post replies, profiles | Live verified; collapsed replies may be missing |
| Hidden Words batch import | Live verified: 54 additions, existing-word preservation, repeat import, and filter creation |
| Notifications and incoming public replies | Live verified; bounded notification window |
| Public text reply | Published and read back with author, exact text, and permalink |
| Standalone text post | Composer checked; final publication not yet live verified |
| DM send to an existing conversation | One authorized test sent and read back |
| CLI DM unsend | Experimental; not end-to-end verified |
| New DM conversations, attachments, likes, follows | Not implemented |
complete means the returned web connection ended, not that all of Threads
was searched. Page limits, partial results, login failures, rate limits, and
schema changes remain distinct outcomes.
uv sync --locked
uv run pytest
uv run ruff check .
node --test tests/*.test.mjsThe latest local validation passed 56 Python tests, 23 Node tests, and Ruff. Bug reports are most useful with the command, structured error code, environment, and a minimal redacted example. Browser compatibility, collection coverage, and reproducible publishing checks are useful areas for contributions.
MIT. Unofficial; not affiliated with Meta.
Built with Click, Rich, curl_cffi, Beautiful Soup, and OpenCC Python. The anonymous public-page approach was informed by tamnd/threads-cli.
