Tweet without going to twitter.com
I love creating content on Twitter but twitter.com leads to doomscrolling. This is my way of fighting that.
Simple CLI for posting to Twitter using their API v2.
Install the latest version with the following command. If you prefer, review the script first.
curl -fsSL https://raw.githubusercontent.com/StanleyMasinde/twitter/main/install.sh | shThis will automatically
- Detect your operating system and architecture
- Download the correct binary
- Verify the SHA256 checksum for security
- Install to /usr/local/bin
- Clean up temporary files
Review the installer before running:
curl -fsSL https://raw.githubusercontent.com/StanleyMasinde/twitter/main/install.shcurl -fsSL https://raw.githubusercontent.com/StanleyMasinde/twitter/main/install.sh | sh -s v1.5.0curl -fsSL https://raw.githubusercontent.com/StanleyMasinde/twitter/main/install.sh | TWITTER_INSTALL=~/.local/bin shsudo twitter updatetwitter update downloads the latest release, writes a temporary binary in the install directory, then atomically replaces the current executable.
If twitter is installed in /usr/local/bin (default install location), that directory is root-owned, so updating requires elevated privileges.
If you installed to a user-writable location (for example with TWITTER_INSTALL=~/.local/bin), sudo is not required.
ArchLinux users can install the community maintained AUR binary package using yay or any other AUR helper:
yay -S twitter-cliWarning
The AUR package is managed by the community. Please use the install script for other Unix based systems.
After installation the executable is available as twitter
You can also download the appropriate binary for your machine from releases:
Administrator privileges are only needed when writing to a protected directory. Works in both Windows PowerShell 5.1 (
powershell) and PowerShell 7+ (pwsh).
Install latest version:
& ([scriptblock]::Create((irm "https://raw.githubusercontent.com/StanleyMasinde/twitter/main/install.ps1")))Install a specific version:
& ([scriptblock]::Create((irm "https://raw.githubusercontent.com/StanleyMasinde/twitter/main/install.ps1"))) v1.5.0Custom install location:
$env:TWITTER_INSTALL = "$env:USERPROFILE\bin"
& ([scriptblock]::Create((irm "https://raw.githubusercontent.com/StanleyMasinde/twitter/main/install.ps1")))twitter updatetwitter update schedules replacement of the current twitter.exe after the running process exits. Run an elevated terminal only if the executable is in a protected directory (for example C:\Program Files).
Note
For the CLI to run on Windows, ensure you have installed the latest C++ redistributable runtime for your architecture. After that installation, open the Windows Terminal and use Twitter CLI by typing twitter.
Use PowerShell:
git clone https://github.com/StanleyMasinde/twitter.git
cd twitter
# Install Rust if needed
winget install --id Rustlang.Rustup -e
# Install and bootstrap vcpkg
# If vcpkg is already installed, skip clone/bootstrap and set VCPKG_ROOT accordingly
git clone https://github.com/microsoft/vcpkg
cd vcpkg
.\bootstrap-vcpkg.bat
# Install native dependencies
$env:VCPKG_ROOT = (Get-Location).Path
[Environment]::SetEnvironmentVariable("VCPKG_ROOT", $env:VCPKG_ROOT, "User")
$oldPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($oldPath -notlike "*$env:VCPKG_ROOT*") {
[Environment]::SetEnvironmentVariable("Path", "$oldPath;$env:VCPKG_ROOT", "User")
}
$env:PATH = "$env:VCPKG_ROOT;$env:PATH"
vcpkg install sqlite3 curl
cd ..
# Build
cargo build --release
# If linker errors persist after installing dependencies
cargo clean
cargo build --release
# Install binary to a user PATH location
$dest = "$env:USERPROFILE\bin"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Copy-Item -Force ".\target\release\twitter.exe" "$dest\twitter.exe"
# Add that directory to PATH (persistent, per-user)
$old = [Environment]::GetEnvironmentVariable("Path","User")
if ($old -notlike "*$dest*") {
[Environment]::SetEnvironmentVariable("Path","$old;$dest","User")
}
# Make it available in this session
$env:Path += ";$dest"- Create a Twitter developer account at developer.twitter.com
- Create a new app and get your API credentials
Warning
This will override your existing config. Only run it on setup.
twitter config --inittwitter config --editOpens your default editor ($EDITOR) with the config file. Creates ~/.config/twitter_cli/config.toml if it doesn't exist.
Expected format:
# The account that will be used to tweet
# Please note, current account uses 0 based index.
# This means the first account is 0
current_account = 0
# Account 1
[[accounts]]
consumer_key = "your_consumer_key"
consumer_secret = "your_consumer_secret"
access_token = "your_access_token"
access_secret = "your_access_secret"
bearer_token = "your_bearer_token"
client_id = "your_oauth2_client_id"
client_secret = "your_oauth2_client_secret"
# Account 2
[[accounts]]
consumer_key = "your_consumer_key"
consumer_secret = "your_consumer_secret"
access_token = "your_access_token"
access_secret = "your_access_secret"
bearer_token = "your_bearer_token"
client_id = "your_oauth2_client_id"
client_secret = "your_oauth2_client_secret"On main (not yet released), client_id and client_secret are required config fields for each account. Keep them populated even if you are not using OAuth 2.0-backed commands yet.
Create config file at ~/.config/twitter_cli/config.toml with the format above. Keep this file private since it contains API secrets.
twitter config --show # Visual preview
twitter config --validate # Check for issuesIf you face a 403 error when tweeting:
- In the Twitter Developer Portal, go to your App → User authentication settings
- Set App permissions to Read and write
- Save changes, then regenerate your Access Token & Secret
- Update your config with the new values
NB: Regenerate tokens after updating permissions, otherwise old tokens remain read-only.
If media upload works but POST /2/tweets fails with:
{"title":"Service Unavailable","detail":"Service Unavailable","type":"about:blank","status":503}check the following:
- In the X Developer Portal (
developer.x.com/console.x.com), confirm your app has active billing and available credits. - Ensure your app permissions are still Read and write, then regenerate Access Token and Secret after permission changes.
- Retry a minimal text-only tweet first:
twitter tweet --body "test" - If text-only works, retry with media:
twitter tweet --body "test with image" --image ~/path/to/image.png
- Check your X API usage dashboard and logs to confirm write calls are not blocked by billing, limits, or temporary platform incidents.
Note: Some 503 responses are transient. If configuration and billing are correct, wait a few minutes and retry.
twitter tweet --body "Building something cool today"echo "I love CLIs" | twitter tweetcat drafts.txt | twitter tweetOmit --body and it will launch your default terminal editor. Like Vim or Nano.
twitter tweetThe parameter is image because I do not see the point of uploading video from the terminal.
twitter tweet --image ~/Downloads/image.pngThreads are created whenever input contains --- separators, regardless of input mode.
twitter tweet --editorThis will launch your default terminal editor. Separate your threads with --- like this.
This is a very long thread. This being the intro.
---
The three dashes indicate the boundary between tweets so this will be tweet 2.
---
This will be tweet 3Pipe thread content with --- separators.
cat thread.txt | twitter tweetUse either --on, --at or --in to specify the time. Please note that these are aliases.
twitter schedule new --body "Ship update at 5:06pm" --at "17:06"
twitter schedule new --body "Ship update on Tuesday" --on "Tuesday"
twitter schedule new --body "Ship update in 30 minutes" --in "30 minutes"twitter schedule list
twitter schedule list --filter failed
twitter schedule list --filter sentIf no rows match your filter, the CLI prints:
No scheduled tweets were found.
twitter schedule runIf none are due, the CLI prints:
No pending scheduled tweets to run.
schedule run is intended to be executed regularly by your OS scheduler.
Run scheduler jobs as the same user who ran twitter config --init.
First get your installed binary path:
command -v twitterLinux (cron):
crontab -e* * * * * /usr/local/bin/twitter schedule run >> /tmp/twitter-schedule.log 2>&1If your binary is not in /usr/local/bin/twitter, replace that path with the output of command -v twitter.
macOS (launchd):
<!-- ~/Library/LaunchAgents/com.twitter.schedule.plist -->
<?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.twitter.schedule</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/twitter</string>
<string>schedule</string>
<string>run</string>
</array>
<key>StartInterval</key><integer>60</integer>
<key>StandardOutPath</key><string>/tmp/twitter-schedule.log</string>
<key>StandardErrorPath</key><string>/tmp/twitter-schedule.log</string>
</dict>
</plist>If your binary is not in /usr/local/bin/twitter, replace that path with the output of command -v twitter.
Load it (current user):
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.twitter.schedule.plist
launchctl kickstart -k gui/$(id -u)/com.twitter.scheduleWindows (Task Scheduler):
Use where twitter in PowerShell to find the full path to twitter.exe.
Create a task that runs every minute with:
schtasks /Create /SC MINUTE /MO 1 /TN "TwitterScheduleRunner" /TR "\"C:\Users\<you>\bin\twitter.exe\" schedule run" /FIf your binary is not in C:\Users\<you>\bin\twitter.exe, replace that path with the output of where twitter.
twitter schedule clearThe CLI prints how many records were removed, for example:
Cleared 3 scheduled tweets.
twitter lists by-id --list-id 1234567890twitter lists create --name "CLI builders"
twitter lists create --name "CLI builders" --description "People building CLI tools"
twitter lists create --name "Private list" --private trueUser-scoped list commands use the current authenticated user automatically.
twitter lists owned
twitter lists owned --max-results 25twitter lists delete --list-id 1234567890twitter lists update --list-id 1234567890 --name "Updated name"
twitter lists update --list-id 1234567890 --description "Updated description"
twitter lists update --list-id 1234567890 --private truetwitter lists members --list-id 1234567890
twitter lists members --list-id 1234567890 --max-results 25User-scoped list commands use the current authenticated user automatically.
twitter lists add-member --list-id 1234567890 --user-id 987654321twitter lists tweets --list-id 1234567890
twitter lists tweets --list-id 1234567890 --max-results 25User-scoped list commands use the current authenticated user automatically.
twitter lists remove-member --list-id 1234567890The CLI resolves the current authenticated user automatically for user-scoped list endpoints.
twitter lists memberships
twitter lists memberships --max-results 25twitter retweets by --tweet-id 1234567890
twitter retweets by --tweet-id 1234567890 --max-results 25User-scoped retweet commands use the current authenticated user automatically.
twitter retweets create --tweet-id 1234567890twitter retweets delete --tweet-id 1234567890twitter dms conversation-events --conversation-id 1234567890-987654321
twitter dms conversation-events --conversation-id 1234567890-987654321 --max-results 25twitter dms events
twitter dms events --max-results 25twitter dms with --participant-id 123456
twitter dms with --participant-id 123456 --max-results 25twitter dms create --participant-ids 123456,987654 --text "hello from the CLI"twitter dms send-with --participant-id 123456 --text "hello from the CLI"twitter dms send --conversation-id 1234567890-987654321 --text "hello from the CLI"User-scoped mute commands use the current authenticated user automatically.
twitter mutes create --target-user-id 1234567890User-scoped mute commands use the current authenticated user automatically.
twitter mutes list
twitter mutes list --max-results 25User-scoped mute commands use the current authenticated user automatically.
twitter mutes delete --target-user-id 1234567890User-scoped block commands use the current authenticated user automatically.
twitter blocks create --target-user-id 1234567890User-scoped block commands use the current authenticated user automatically.
twitter blocks list
twitter blocks list --max-results 25twitter blocks list uses OAuth 2.0 (Authorization Code + PKCE) and reuses cached tokens after first authorization.
User-scoped block commands use the current authenticated user automatically.
twitter blocks delete --target-user-id 1234567890Fetch your reverse-chronological home timeline.
twitter timeline reverseFetch a specific tweet by id.
twitter tweets by-id 2006409743426818416Fetch tweets from a user by id.
twitter tweets user --id 2244994945Search recent tweets by query.
twitter tweets recent --query "rustlang"Search all tweets by query.
twitter tweets all --query "rustlang"Fetch tweets that mention the currently authenticated user.
twitter mentionsShow who liked a tweet.
twitter likes by --tweet-id 1234567890
twitter likes by --tweet-id 1234567890 --max-results 25Like a tweet for the currently authenticated user.
twitter likes create --tweet-id 1234567890Remove a like for the currently authenticated user.
twitter likes delete --tweet-id 1234567890Fetch tweets liked by the currently authenticated user.
twitter likes tweetsFetch a single user by id or username.
twitter users by-id --id 2244994945
twitter users by-username --username jackFetch multiple users by ids or usernames.
twitter users by-ids --ids 2244994945,6253282
twitter users by-usernames --usernames jack,openaiFetch follow relationships.
twitter users following --id 2244994945
twitter users followers --id 2244994945Manage follows for the currently authenticated user.
twitter users follow --target-user-id 6253282
twitter users unfollow --target-user-id 6253282Fetch bookmarks and manage them for the current authenticated user.
twitter bookmarks list
twitter bookmarks create --tweet-id 1234567890
twitter bookmarks delete --tweet-id 1234567890OAuth behavior:
twitter bookmarks listuses OAuth 2.0 (Authorization Code + PKCE) and stores access/refresh tokens in the local cache database.twitter blocks listuses OAuth 2.0 (Authorization Code + PKCE) and shares the same local token cache.twitter bookmarks create,twitter bookmarks delete,twitter bookmarks folders, andtwitter bookmarks foldercurrently use OAuth 1.0a user tokens from your config.
First OAuth 2.0 key exchange for bookmarks list or blocks list (on main, unreleased):
- Add
client_idandclient_secretto your account in~/.config/twitter_cli/config.toml. - In the X/Twitter Developer Dashboard (App settings), set the callback/redirect URL to
http://127.0.0.1:3000. - Run
twitter bookmarks listortwitter blocks list. - Open the printed authorization URL in your browser.
- After consent, the browser redirect may show an error (for example, server not found). That is expected because no local dev server is required (works for VPS/embedded environments too).
- Copy the full callback URL from the browser address bar, paste it in the CLI prompt, then press Enter.
- The CLI exchanges the code for tokens and caches them; later runs auto-refresh with the refresh token.
List bookmark folders and fetch the tweets in a folder.
twitter bookmarks folders
twitter bookmarks folder --folder-id 1234567890List and manage filtered stream rules.
twitter streams rules list
twitter streams rules add --value "rustlang has:links" --tag "rust"
twitter streams rules delete --ids 1234567890Connect to the filtered stream.
twitter streams connect
twitter streams connect --backfill-minutes 5Fetch details for the currently authenticated user.
twitter meExample output:
User Id: 123456789
Name: Jane Doe
Username: @janedoe
API Response:
Tweet Id: 2006409743426818416
Tweet body: Hello, world
You can show the API usage via the usage subcommand.
twitter usageThe above command will show the API usage like the one below.
Daily project usage: 0/100
- Rust
- Twitter API v2