A CLI tool for publishing to Threads and fetching Threads data as JSON.
- OAuth authentication with Meta's Threads API
- Publish text, image, video, and carousel (2–20 item) posts from markdown drafts
- Publish multi-post threads from a single file
- Attach links, GIFs, and topic tags to posts
- Fetch posts with engagement metrics (views, likes, replies, reposts, quotes)
- View profile and follower insights
- Draft management with YAML frontmatter support
- Auto-archive published drafts
bun install- Go to developers.facebook.com/apps
- Create a new app and select Threads API as the use case
- Under Customize use case, add these permissions:
threads_basicthreads_content_publishthreads_manage_insights
- Set the Redirect Callback URL to:
https://localhost:3000/callback - Add yourself as a Tester under App Roles and accept the invitation in Threads settings
Important: This CLI authenticates against the Threads API, which uses a separate app ID/secret from the top-level Meta app. You'll need the Threads App ID and Threads App secret, found at App Dashboard → App settings → Basic (scroll down — they're distinct from the Meta App ID/Secret shown at the top of that page). Using the Meta App ID will fail with:
Authorization Failed: No app ID was sent with the request(error 4476002).
Meta requires HTTPS for OAuth callbacks. Use mkcert to create local certificates:
# Install mkcert
brew install mkcert
# Install local CA
mkcert -install
# Generate certificates
mkcert localhost
# Move to config directory
mkdir -p ~/.threads-cli
mv localhost.pem localhost-key.pem ~/.threads-cli/bun run src/index.ts auth loginThis will prompt for your Threads App ID and Threads App secret (see the note above — these are different from the Meta App ID/Secret), then open a browser for authorization.
# Login (opens browser for OAuth)
threads auth login
# Check auth status
threads auth status
# Logout
threads auth logout# View your profile
threads profile
# Include follower insights
threads profile --insights# List recent posts with metrics
threads posts list
threads posts list --limit 50
# Fetch all posts since a date, paging as needed
# (--since returns every matching post; --limit is ignored here)
threads posts list --since 2024-01-01
# Get a specific post by ID or URL
threads posts get <post-id>
threads posts get "https://threads.net/@user/post/abc123"
# Get replies to a post
threads posts replies <post-id>
# Delete a post by ID or URL (prompts for confirmation; --yes to skip)
threads posts delete <post-id>
threads posts delete <post-id> --yes# Create a new draft
threads draft new "My Post Title"
# List all drafts
threads draft list
# Delete a draft
threads draft delete <filename># Publish a draft (with confirmation)
threads publish my-draft.md
# Skip confirmation
threads publish my-draft.md --yes
# Preview only (dry run)
threads publish my-draft.md --dry-run# Publish a thread of connected posts from one file
threads thread my-thread.md
# Skip confirmation / preview only
threads thread my-thread.md --yes
threads thread my-thread.md --dry-run# Show the config file, drafts, and archive paths
threads config path
# Set a configuration value
threads config set drafts_path ~/threads/drafts
threads config set archive_path ~/threads/archive
threads config set default_limit 50
threads config set archive_after_publish falseSettable keys: drafts_path, archive_path, default_limit,
archive_after_publish.
Drafts are markdown files with YAML frontmatter:
---
title: My Post Title
image: https://example.com/image.jpg
alt: Image description
created: 2024-01-15T10:30:00Z
---
Your post content goes here. This will be published to Threads.Frontmatter fields:
title- Optional title for organizing draftsimage- Single image URL to attachvideo- Single video URL to attachimages- Carousel of 2–20 items; each item is{ url, alt?, type? }wheretypeisIMAGEorVIDEO(inferred from the URL when omitted)alt- Alt text for a singleimage/videolink- URL to attach as a link preview (text-only posts only)gif- GIF attachment id (text-only posts only)topic- Topic tag, 1–50 characters, no periods or ampersandscreated- Auto-generated timestamp
Media posts may be caption-less — a draft whose body is empty still publishes
if it carries an image, video, or images carousel. A carousel example:
---
images:
- url: https://example.com/one.jpg
alt: First slide
- url: https://example.com/clip.mp4
type: VIDEO
---
Optional caption for the carousel.A thread is a markdown file with posts separated by --- on its own line. Each
post is published in order and chained as a reply to the previous one. A file
needs at least two posts (use publish for single posts).
First post in the thread.
---
Second post.
---
 Third post with a leading image.A post may start with one or more  image lines; stacked image lines
become a carousel (up to 20 items), and trailing text on the same line as the
last image becomes that post's caption.
A thread file may also open with YAML frontmatter (e.g. title:), which is
stripped before the posts are split. Frontmatter fields are ignored by thread
— per-post media comes from  lines, not frontmatter. A leading ---
block is only treated as frontmatter when it parses as a YAML mapping whose keys
look like frontmatter fields, so a file that opens with a bare separator (or a
first post containing a colon) still keeps every post.
Config is stored at ~/.threads-cli/config.json:
{
"auth": {
"app_id": "your-app-id",
"app_secret": "your-app-secret",
"access_token": "...",
"user_id": "...",
"expires_at": "2024-03-15T00:00:00.000Z"
},
"paths": {
"drafts": "~/.threads-cli/drafts",
"archive": "~/.threads-cli/archive"
},
"settings": {
"archive_after_publish": true,
"default_limit": 25
}
}Security note:
app_secretandaccess_tokenare stored in plaintext in this file. The file is created with0o600permissions (owner read/write only), but anyone with access to your user account can read these credentials. Treat~/.threads-cli/config.jsonlike any other secret, and runthreads auth logoutto clear the stored token when needed.
THREADS_CLI_CONFIG_DIR- Override the config directory (useful for testing)
# Run CLI
bun run src/index.ts <command>
# Run tests
bun test
# Type check
bun run typecheck- Initial OAuth returns a short-lived token (1 hour)
- The CLI automatically exchanges it for a long-lived token (60 days)
- Tokens are auto-refreshed when expired
MIT