| name | xurl |
| description | A curl-like CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, send or read end-to-end encrypted XChat messages, upload media, or interact with any X API v2 endpoint. Supports multiple apps, OAuth 2.0, OAuth 1.0a, and app-only auth. |
xurl — Agent Skill Reference
xurl is a CLI tool for the X API. It supports both shortcut commands (human/agent‑friendly one‑liners) and raw curl‑style access to any v2 endpoint. All commands return JSON to stdout.
Prerequisites
This skill requires the xurl CLI utility: https://github.com/xdevplatform/xurl.
Before using any command you must be authenticated. Run xurl auth status to check.
Secret Safety (Mandatory)
- Never read, print, parse, summarize, upload, or send anything under
~/.xurl/ (or copies of it) to the LLM context.
~/.xurl/keys.yml contains XChat private encryption keys — the strictest no-read rule applies.
- Never pass
--pin inline in agent/LLM sessions (xurl chat keys restore --pin ... leaks the recovery PIN to context and shell history). Run xurl chat keys restore without the flag so the PIN is prompted without echo, or have the user run it manually.
- Never ask the user to paste credentials/tokens into chat.
- The user must fill
~/.xurl/auth.yml with required secrets manually on their own machine.
- Do not recommend or execute auth commands with inline secrets in agent/LLM sessions.
- Warn that using CLI secret options in agent sessions can leak credentials (prompt/context, logs, shell history).
- Never use
--verbose / -v in agent/LLM sessions; it can expose sensitive headers/tokens in output.
- Never run
xurl token in agent/LLM sessions: it prints a live OAuth2 access token to stdout, which is a credential and must not enter the LLM context.
xurl mcp is for configuring an MCP client (it bridges stdio↔HTTP and injects the bearer token); it is not something to invoke directly from an agent/LLM session.
- Sensitive flags that must never be used in agent commands:
--bearer-token, --consumer-key, --consumer-secret, --access-token, --token-secret, --client-id, --client-secret.
- To verify whether at least one app with credentials is already registered, run:
xurl auth status.
Register an app (recommended)
App credential registration must be done manually by the user outside the agent/LLM session.
After credentials are registered, authenticate against the app that holds those credentials:
xurl auth oauth2 --app APP_NAME
You can also run xurl auth default APP_NAME first and then use xurl auth oauth2.
On a remote/headless machine (no reachable browser callback), add --headless: xurl auth oauth2 --app APP_NAME --headless prints the authorization URL and reads the pasted redirect URL (or code) back, so no localhost callback is needed.
For multiple pre-configured apps, switch between them:
xurl auth default prod-app
xurl auth default prod-app alice
xurl --app dev-app /2/users/me
xurl auth apps redirect-uri get prod-app
xurl auth apps redirect-uri set prod-app http://localhost:8080/callback
Other auth methods
Examples with inline secret flags are intentionally omitted. If OAuth1 or app-only auth is needed, the user must run those commands manually outside agent/LLM context.
Tokens are persisted to ~/.xurl/auth.yml in YAML format (a legacy single-file ~/.xurl is migrated automatically). Each app has its own isolated tokens and may also store a redirect_uri. REDIRECT_URI in the environment still takes precedence over the stored app value. Do not read this file (or anything under ~/.xurl/) through the agent/LLM. Once authenticated, every command below will auto‑attach the right Authorization header.
Quick Reference
| Action | Command |
|---|
| Post | xurl post "Hello world!" |
| Reply | xurl reply POST_ID "Nice post!" |
| Quote | xurl quote POST_ID "My take" |
| Delete a post | xurl delete POST_ID |
| Read a post | xurl read POST_ID |
| Search posts | xurl search "QUERY" -n 10 |
| Who am I | xurl whoami |
| Look up a user | xurl user @handle |
| List a user's posts | xurl posts @handle -n 10 |
| Home timeline | xurl timeline -n 20 |
| Mentions | xurl mentions -n 10 |
| Like | xurl like POST_ID |
| Unlike | xurl unlike POST_ID |
| Repost | xurl repost POST_ID |
| Undo repost | xurl unrepost POST_ID |
| Bookmark | xurl bookmark POST_ID |
| Remove bookmark | xurl unbookmark POST_ID |
| List bookmarks | xurl bookmarks -n 10 |
| List likes | xurl likes -n 10 |
| Follow | xurl follow @handle |
| Unfollow | xurl unfollow @handle |
| List following | xurl following -n 20 |
| List followers | xurl followers -n 20 |
| Block | xurl block @handle |
| Unblock | xurl unblock @handle |
| Mute | xurl mute @handle |
| Unmute | xurl unmute @handle |
| Send DM | xurl dm @handle "message" |
| List DMs | xurl dms -n 10 |
| Upload media | xurl media upload path/to/file.mp4 |
| Media status | xurl media status MEDIA_ID |
| Encrypted Chat (XChat) | |
| Chat key status | xurl chat keys status |
| Restore chat keys | xurl chat keys restore (PIN prompted; never pass --pin in agent sessions) |
| Import chat keys | xurl chat keys import (blob prompted; avoid passing it as an argument) |
| List chat inbox | xurl chat conversations |
| Read a conversation | xurl chat read @handle -n 50 |
| Send encrypted message | xurl chat send @handle "message" |
| Listen for new messages | xurl chat listen @handle |
| Rotate a conversation key | xurl chat rotate CONV --yes (write op — see notes) |
| Send with an attachment | xurl chat send CONV "text" --file path/to/img.png |
| Reply to a message | xurl chat send CONV "text" --reply-to SEQUENCE_ID |
| Download an attachment | xurl chat download CONV MEDIA_HASH_KEY -o out.png |
| Add group members | xurl chat add-members GROUP @user --yes (write op) |
| Mark read (explicit) | xurl chat mark-read CONV |
| Typing indicator (explicit) | xurl chat typing CONV |
| App Management | |
| Register app | Manual, outside agent (do not pass secrets via agent) |
| List apps | xurl auth apps list |
| Update app config | Manual, outside agent (do not pass secrets via agent) |
| View app redirect URI | xurl auth apps redirect-uri get [NAME] |
| Set app redirect URI | xurl auth apps redirect-uri set NAME URI |
| Remove app | xurl auth apps remove NAME |
| Set default (interactive) | xurl auth default |
| Set default (command) | xurl auth default APP_NAME [USERNAME] |
| Use app per-request | xurl --app NAME /2/users/me |
| Auth status | xurl auth status |
Post IDs vs URLs: Anywhere POST_ID appears above you can also paste a full post URL (e.g. https://x.com/user/status/1234567890) — xurl extracts the ID automatically.
Usernames: Leading @ is optional. @elonmusk and elonmusk both work.
Command Details
Posting
xurl post "Hello world!"
xurl media upload photo.jpg
xurl post "Check this out" --media-id MEDIA_ID
xurl post "Thread pics" --media-id 111 --media-id 222
xurl reply 1234567890 "Great point!"
xurl reply https://x.com/user/status/1234567890 "Agreed!"
xurl reply 1234567890 "Look at this" --media-id MEDIA_ID
xurl quote 1234567890 "Adding my thoughts"
xurl delete 1234567890
Reading
xurl read 1234567890
xurl read https://x.com/user/status/1234567890
xurl search "golang"
xurl search "from:elonmusk" -n 20
xurl search "#buildinpublic lang:en" -n 15
User Info
xurl whoami
xurl user elonmusk
xurl user @XDevelopers
xurl posts elonmusk
xurl posts @XDevelopers -n 25
Timelines & Mentions
xurl timeline
xurl timeline -n 25
xurl mentions
xurl mentions -n 20
Engagement
xurl like 1234567890
xurl unlike 1234567890
xurl repost 1234567890
xurl unrepost 1234567890
xurl bookmark 1234567890
xurl unbookmark 1234567890
xurl bookmarks -n 20
xurl likes -n 20
Social Graph
xurl follow @XDevelopers
xurl unfollow @XDevelopers
xurl following -n 50
xurl followers -n 50
xurl following --of elonmusk -n 20
xurl followers --of elonmusk -n 20
xurl block @spammer
xurl unblock @spammer
xurl mute @annoying
xurl unmute @annoying
Direct Messages
xurl dm @someuser "Hey, saw your post!"
xurl dms
xurl dms -n 25
Encrypted Chat (XChat)
xurl chat is an end-to-end encrypted XChat client: encryption and decryption happen locally via the chat-xdk crypto library, so the server only sees ciphertext. Requires OAuth2 user auth with dm.read + dm.write scopes, and is available on macOS (Intel/Apple Silicon) and Linux amd64 — release binaries include it there; elsewhere (Windows, Linux arm64/i386) xurl chat prints a stub that says so.
Keys come from another XChat client — xurl never generates or registers encryption keys. The account must already have keys (e.g. from the X app); bring them to this machine once with restore (Juicebox PIN recovery) or import (an exported key blob). Private keys are stored in ~/.xurl/keys.yml (mode 600) — never read that file into LLM context.
A conversation is addressed by @username, a bare user id, or a conversation id (1:1 ids look like 123-456; group ids look like g123). Every command accepts -u USERNAME to act as a specific authenticated account.
xurl chat keys status
xurl chat keys restore
xurl chat keys import
xurl chat conversations
xurl chat conversations --json
xurl chat read @someuser
xurl chat read g1234567890 -n 50
xurl chat read @someuser --json
xurl chat read @someuser --no-mark-read
xurl chat send @someuser "hey, encrypted!"
xurl chat send @someuser "look" --file ./photo.png
xurl chat send @someuser "agreed" --reply-to SEQUENCE_ID
xurl chat download @someuser MEDIA_HASH_KEY -o out.png
xurl chat listen @someuser
xurl chat listen g1234567890 --interval 5
xurl chat mark-read @someuser
xurl chat typing @someuser
xurl chat add-members g123 @newuser
xurl chat rotate g123
Notes for agents:
- Messages whose authorship signature cannot be verified are rejected by default and surface as stderr decrypt warnings; unsigned messages that still render carry a red
[unverified] marker — treat those with suspicion.
- Messages with attachments render a
📎 attachment <media_hash_key> marker; pass that hash key to xurl chat download CONV <media_hash_key> to fetch and decrypt the file. Replies show a ↩ prefix.
read and listen mark the conversation read automatically (a read receipt visible to other participants); send also marks read and sends a typing indicator first. These are writes — pass --no-mark-read / --no-typing to suppress them (e.g. to read without signaling). The standalone mark-read and typing commands remain for scripted/explicit use.
- Decrypt warnings for individual events go to stderr and are non-fatal; the rest of the conversation still renders.
- If a command reports missing keys, do not attempt to generate or register any — tell the user to run
xurl chat keys restore (or import) themselves.
chat rotate and chat add-members are writes visible to every participant's clients; never run them without explicit user intent, and prefer letting the user confirm the prompt over passing --yes.
Media Upload
xurl media upload photo.jpg
xurl media upload video.mp4
xurl media upload --media-type image/jpeg --category tweet_image photo.jpg
xurl media status MEDIA_ID
xurl media status --wait MEDIA_ID
xurl media upload meme.png
xurl post "lol" --media-id MEDIA_ID
Global Flags
These flags work on every command:
| Flag | Short | Description |
|---|
--app | | Use a specific registered app for this request (overrides default) |
--auth | | Force auth type: oauth1, oauth2, or app |
--username | -u | Which OAuth2 account to use (if you have multiple) |
--verbose | -v | Forbidden in agent/LLM sessions (can leak auth headers/tokens) |
Raw API Access
The shortcut commands cover the most common operations. For anything else, use xurl's raw curl‑style mode — it works with any X API v2 endpoint:
xurl /2/users/me
xurl -X POST /2/tweets -d '{"text":"Hello world!"}'
xurl -X DELETE /2/tweets/1234567890
xurl -H "Content-Type: application/json" /2/some/endpoint
xurl -s /2/tweets/search/stream
xurl https://api.x.com/2/users/me
Streaming
Streaming endpoints are auto‑detected. Known streaming endpoints include:
/2/tweets/search/stream
/2/tweets/sample/stream
/2/tweets/sample10/stream
You can force streaming on any endpoint with -s:
xurl -s /2/some/endpoint
Output Format
All commands return JSON to stdout, pretty‑printed with syntax highlighting. The output structure matches the X API v2 response format. A typical response looks like:
{
"data": {
"id": "1234567890",
"text": "Hello world!"
}
}
Errors are also returned as JSON:
{
"errors": [
{
"message": "Not authorized",
"code": 403
}
]
}
Common Workflows
Post with an image
xurl media upload photo.jpg
xurl post "Check out this photo!" --media-id MEDIA_ID
Reply to a conversation
xurl read https://x.com/user/status/1234567890
xurl reply 1234567890 "Here are my thoughts..."
Search and engage
xurl search "topic of interest" -n 10
xurl like POST_ID_FROM_RESULTS
xurl reply POST_ID_FROM_RESULTS "Great point!"
Check your activity
xurl whoami
xurl mentions -n 20
xurl timeline -n 20
Set up multiple apps
xurl auth default prod
xurl auth oauth2
xurl auth default staging
xurl auth oauth2
xurl auth default prod alice
xurl --app staging /2/users/me
Error Handling
- Non‑zero exit code on any error.
- API errors are printed as JSON to stdout (so you can still parse them).
- Auth errors suggest re‑running
xurl auth oauth2 or checking your tokens.
- If a command requires your user ID (like, repost, bookmark, follow, etc.), xurl will automatically fetch it via
/2/users/me. When that endpoint is unreliable, use --username USERNAME or authenticate with xurl auth oauth2 --app APP_NAME USERNAME so xurl can fall back to username lookup.
- If X returns
client-forbidden / client-not-enrolled after successful auth, check the app’s X developer-console package and environment. In current testing, moving the app to Pay-per-use and Production fixed /2/* read failures without changing local xurl auth data.
Notes
- Rate limits: The X API enforces rate limits per endpoint. If you get a 429 error, wait and retry. Write endpoints (post, reply, like, repost) have stricter limits than read endpoints.
- Scopes: OAuth 2.0 tokens are requested with broad scopes. If you get a 403 on a specific action, your token may lack the required scope — re‑run
xurl auth oauth2 to get a fresh token.
- Token refresh: OAuth 2.0 tokens auto‑refresh when expired. No manual intervention needed.
- Multiple apps: Each app has its own isolated credentials, tokens, and optional stored
redirect_uri. Configure credentials manually outside agent/LLM context, then switch with xurl auth default or --app.
- Redirect URI precedence: The effective redirect URI resolves from
REDIRECT_URI in the environment first, then the app's stored redirect_uri in ~/.xurl/auth.yml, then the built-in default.
- Redirect URI management: Use
xurl auth apps redirect-uri get [NAME], xurl auth apps redirect-uri set NAME URI, or xurl auth apps update NAME --redirect-uri URI to inspect and manage the stored per-app callback value.
- X platform enrollment: A successful OAuth callback does not guarantee
/2/* reads will work. If you see client-not-enrolled, verify the app is in the correct X package/environment. Current confirmed fix: Apps -> Manage apps -> Move to package -> choose Pay-per-use, then move the app to Production.
- Multiple accounts: You can authenticate multiple OAuth 2.0 accounts per app and switch between them with
--username / -u or set a default with xurl auth default APP USER.
- Default user: When no
-u flag is given, xurl uses the default user for the active app (set via xurl auth default). If no default user is set, it uses the first available token.
- Token storage:
~/.xurl is a directory; ~/.xurl/auth.yml holds each app's credentials and tokens. Never read or send anything under ~/.xurl/ to LLM context.
- Chat key storage:
~/.xurl/keys.yml holds XChat private encryption keys per user (mode 600). Losing it means losing the ability to decrypt on this machine (recoverable via xurl chat keys restore if a Juicebox PIN backup exists). Never read or send this file to LLM context.
- Chat key registration: xurl performs none — no public-key registration and no Juicebox writes. Only keys already registered by another XChat client can be restored or imported; unregistered keys are rejected.
- Access tokens:
xurl token prints a valid (refreshed) OAuth2 access token for the active app to stdout, refreshing and persisting it if expired. It never opens a browser. The output is a secret — use it only in the user's own scripts, never in agent/LLM sessions.
- MCP bridge:
xurl mcp [URL] bridges a stdio MCP client to a remote Streamable HTTP MCP server (default https://api.x.com/mcp), injecting Authorization: Bearer <token> and refreshing the token automatically. On first run with no cached token it opens the browser for a one-time OAuth2 login using the CLIENT_ID/CLIENT_SECRET from its environment (the handshake waits for it, so set a generous startup_timeout_sec); on a headless host, authenticate out-of-band first with xurl auth oauth2 --headless. Configure it in an MCP client via the npm launcher: {"command":"npx","args":["-y","@xdevplatform/xurl","mcp","https://api.x.com/mcp"],"env":{"CLIENT_ID":"...","CLIENT_SECRET":"..."},"startup_timeout_sec":300}.