| name | xurl |
| description | X/Twitter via xurl CLI: post, search, DM, media, v2 API. |
| version | 1.1.1 |
| author | xdevplatform + hermes + Hermes Agent |
| license | MIT |
| platforms | ["linux","macos"] |
| prerequisites | {"commands":["xurl"]} |
| metadata | {"hermes":{"tags":["twitter","x","social-media","xurl","official-api"],"homepage":"https://github.com/xdevplatform/xurl","upstream_skill":"https://github.com/hermes/hermes/blob/main/skills/xurl/SKILL.md"}} |
xurl — X (Twitter) API via the Official CLI
xurl is the X developer platform's official CLI for the X API. It supports shortcut commands for common actions AND raw curl-style access to any v2 endpoint. All commands return JSON to stdout.
Use this skill for:
- posting, replying, quoting, deleting posts
- searching posts and reading timelines/mentions
- liking, reposting, bookmarking
- following, unfollowing, blocking, muting
- direct messages
- media uploads (images and video)
- raw access to any X API v2 endpoint
- multi-app / multi-account workflows
This skill replaces the older xitter skill (which wrapped a third-party Python CLI). xurl is maintained by the X developer platform team, supports OAuth 2.0 PKCE with auto-refresh, and covers a substantially larger API surface.
Secret Safety (MANDATORY)
Critical rules when operating inside an agent/LLM session:
- Never read, print, parse, summarize, upload, or send
~/.xurl to LLM context.
- Never ask the user to paste credentials/tokens into chat.
- The user must fill
~/.xurl with secrets manually on their own machine. In Docker, this must be the ~ seen by Hermes tool subprocesses; see the Docker note below.
- Never recommend or execute auth commands with inline secrets in agent sessions.
- Never use
--verbose / -v in agent sessions — it can expose auth headers/tokens.
- To verify credentials exist, only use:
xurl auth status.
Forbidden flags in agent commands (they accept inline secrets):
--bearer-token, --consumer-key, --consumer-secret, --access-token, --token-secret, --client-id, --client-secret
App credential registration (xurl auth apps add with --client-id / --client-secret) must be done by the user manually, outside the agent session — those flags accept inline secrets.
OAuth2 token completion is agent work when the app is already registered. On Igor's Mac fleet (~/.hermes/SOUL.md No constraints + use-existing-browser-sessions), the agent MUST complete xurl auth oauth2 via terminal + browser/computer_use — never hand terminal homework to the phone user.
Fleet operator override (Igor Mac — SOUL precedence)
When xurl auth status shows a named app with client_id but oauth2: (none):
-
Run OAuth on the Mac (api_server host), not on the phone:
xurl auth oauth2 --app APP_NAME USERNAME
This opens a local browser on the Mac. If the callback listener times out, use headless + browser tools:
xurl auth oauth2 --headless --app APP_NAME USERNAME
Then browser_navigate to the printed authorize URL (Igor's signed-in Chrome / CDP 127.0.0.1:9222), click Authorize, capture the http://localhost:8080/callback?...&code=... redirect, and paste the full redirect URL (or code) back into the waiting xurl terminal prompt.
-
Never tell the user to "run this in your local terminal" — especially from Hermes Mobile chat. Phone users cannot run Mac terminal commands. Report progress as agent actions ("Completing X OAuth on your MacBook…") or an honest blocker after browser+terminal attempts fail.
-
Forbidden wrong flags: --skip-browser does not exist; use --headless when the default browser callback fails.
-
After OAuth succeeds: xurl auth default APP_NAME USERNAME, then xurl auth status and xurl whoami before any write/DM action.
-
Only escalate to human when: (a) app not registered yet (needs client-id/secret paste), or (b) X developer portal enrollment/billing blocks the app.
Tokens persist to ~/.xurl in YAML. Each app has isolated tokens. OAuth 2.0 tokens auto-refresh.
Installation
Pick ONE method. On Linux, the shell script or go install are the easiest.
curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | bash
brew install --cask xdevplatform/tap/xurl
npm install -g @xdevplatform/xurl
go install github.com/xdevplatform/xurl@latest
Verify:
xurl --help
xurl auth status
If xurl is installed but auth status shows no apps at all, the user must register the app once (client-id/secret) — see the next section. If the app exists but oauth2: (none), the agent completes OAuth via Fleet operator override above.
One-Time User Setup (app registration only — secrets paste)
Steps 1–4 must be performed by the user directly when no app is registered, because they involve pasting client-id/secret. Do not execute xurl auth apps add with inline secrets in agent sessions.
- Create or open an app at https://developer.x.com/en/portal/dashboard
- Set the redirect URI to
http://localhost:8080/callback
- Copy the app's Client ID and Client Secret
- Register the app locally (user runs this):
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
- OAuth (agent completes on Igor Mac fleet) — see Fleet operator override. User-only fallback when not on a Mac agent with browser tools:
xurl auth oauth2 --app my-app YOUR_USERNAME
- Set the app as default so all commands use it:
xurl auth default my-app
- Verify:
xurl auth status
xurl whoami
After this, the agent can use any command below without further setup. OAuth 2.0 tokens auto-refresh.
Common pitfall: If you omit --app my-app from xurl auth oauth2, the OAuth token is saved to the built-in default app profile — which has no client-id or client-secret. Commands will fail with auth errors even though the OAuth flow appeared to succeed. If you hit this, re-run xurl auth oauth2 --app my-app and xurl auth default my-app.
Docker HOME pitfall: In the official Hermes Docker layout, /opt/data is HERMES_HOME, but Hermes tool subprocesses use /opt/data/home as HOME. That means ~/.xurl resolves to /opt/data/home/.xurl for Hermes-run xurl commands, not /opt/data/.xurl. Run the user setup with the same HOME:
HOME=/opt/data/home xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
HOME=/opt/data/home xurl auth oauth2 --app my-app YOUR_USERNAME
HOME=/opt/data/home xurl auth default my-app YOUR_USERNAME
HOME=/opt/data/home xurl auth status
If HOME=/opt/data xurl auth status succeeds but HOME=/opt/data/home xurl auth status shows no apps or tokens, Hermes tool calls will not see the credentials.
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 |
| Home timeline | xurl timeline -n 20 |
| Mentions | xurl mentions -n 10 |
| Like / Unlike | xurl like POST_ID / xurl unlike POST_ID |
| Repost / Undo | xurl repost POST_ID / xurl unrepost POST_ID |
| Bookmark / Remove | xurl bookmark POST_ID / xurl unbookmark POST_ID |
| List bookmarks / likes | xurl bookmarks -n 10 / xurl likes -n 10 |
| Follow / Unfollow | xurl follow @handle / xurl unfollow @handle |
| Following / Followers | xurl following -n 20 / xurl followers -n 20 |
| Block / Unblock | xurl block @handle / xurl unblock @handle |
| Mute / Unmute | xurl mute @handle / 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 |
| List apps | xurl auth apps list |
| Remove app | xurl auth apps remove NAME |
| Set default app | xurl auth default APP_NAME [USERNAME] |
| Per-request app | xurl --app NAME /2/users/me |
| Auth status | xurl auth status |
Notes:
POST_ID accepts full URLs too (e.g. https://x.com/user/status/1234567890) — xurl extracts the ID.
- Usernames work with or without a leading
@.
Command Details
Posting
xurl post "Hello world!"
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 & Search
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
For X Articles, use raw API mode instead of the read shortcut. xurl read
expects a post ID or post URL; do not put read before a /2/tweets/...
endpoint. Request the article tweet field and ingest data.article.plain_text
from the JSON response:
xurl --app APP_NAME '/2/tweets/2057909493250539891?expansions=author_id,attachments.media_keys,referenced_tweets.id&tweet.fields=created_at,lang,public_metrics,context_annotations,entities,possibly_sensitive,conversation_id,in_reply_to_user_id,referenced_tweets,article'
Users, Timeline, Mentions
xurl whoami
xurl user elonmusk
xurl user @XDevelopers
xurl timeline -n 25
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 -n 25
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
Raw API Access
The shortcuts cover common operations. For anything else, use raw curl-style mode against 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
Global Flags
| Flag | Short | Description |
|---|
--app | | Use a specific registered app (overrides default) |
--auth | | Force auth type: oauth1, oauth2, or app |
--username | -u | Which OAuth2 account to use (if multiple exist) |
--verbose | -v | Forbidden in agent sessions — leaks auth headers |
--trace | -t | Add X-B3-Flags: 1 trace header |
Streaming
Streaming endpoints are auto-detected. Known ones include:
/2/tweets/search/stream
/2/tweets/sample/stream
/2/tweets/sample10/stream
Force streaming on any endpoint with -s.
Output Format
All commands return JSON to stdout. Structure mirrors X API v2:
{ "data": { "id": "1234567890", "text": "Hello world!" } }
Errors are also 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
Multiple apps (credentials pre-configured manually)
xurl auth default prod alice
xurl --app staging /2/users/me
Error Handling
- Non-zero exit code on any error.
- API errors are still printed as JSON to stdout, so you can parse them.
- Auth errors → agent re-runs OAuth via Fleet operator override (terminal + browser); user handoff only if Mac browser tools exhausted.
- Commands that need the caller's user ID (like, repost, bookmark, follow, etc.) will auto-fetch it via
/2/users/me. An auth failure there surfaces as an auth error.
Agent Workflow
- Verify prerequisites:
xurl --help and xurl auth status.
- Check default app has credentials. Parse the
auth status output. The default app is marked with ▸. If the default app shows oauth2: (none) but another app has a valid oauth2 user, tell the user to run xurl auth default <that-app> to fix it. This is the most common setup mistake — the user added an app with a custom name but never set it as default, so xurl keeps trying the empty default profile.
- If no app registered (
xurl auth apps list empty), stop and direct user to app registration (client-id/secret paste only). If app exists but oauth2: (none), execute Fleet operator override — do NOT tell phone user to run terminal commands.
- Start with a cheap read (
xurl whoami, xurl user @handle, xurl search ... -n 3) to confirm reachability.
- Confirm the target post/user and the user's intent before any write action (post, reply, like, repost, DM, follow, block, delete).
- Use JSON output directly — every response is already structured.
- Never paste
~/.xurl contents back into the conversation.
Troubleshooting
| Symptom | Cause | Fix |
|---|
| Auth errors after successful OAuth flow | Token saved to default app (no client-id/secret) instead of your named app | xurl auth oauth2 --app my-app then xurl auth default my-app |
unauthorized_client during OAuth | App type set to "Native App" in X dashboard | Change to "Web app, automated app or bot" in User Authentication Settings |
UsernameNotFound or 403 on /2/users/me right after OAuth | X not returning username reliably from /2/users/me | Re-run xurl auth oauth2 --app my-app YOUR_USERNAME (xurl v1.1.0+) to pass the handle explicitly |
| 401 on every request | Token expired or wrong default app | Check xurl auth status — verify ▸ points to an app with oauth2 tokens |
client-forbidden / client-not-enrolled | X platform enrollment issue | Dashboard → Apps → Manage → Move to "Pay-per-use" package → Production environment |
CreditsDepleted | $0 balance on X API | Buy credits (min $5) in Developer Console → Billing |
media processing failed on image upload | Default category is amplify_video | Add --category tweet_image --media-type image/png |
| Two "Client Secret" values in X dashboard | UI bug — first is actually Client ID | Confirm on the "Keys and tokens" page; ID ends in MTpjaQ |
Notes
- Rate limits: X enforces per-endpoint rate limits. A 429 means wait and retry. Write endpoints (post, reply, like, repost) have tighter limits than reads.
- Scopes: OAuth 2.0 tokens use broad scopes. A 403 on a specific action usually means the token is missing a scope — have the user re-run
xurl auth oauth2.
- Token refresh: OAuth 2.0 tokens auto-refresh. Nothing to do.
- Multiple apps: Each app has isolated credentials/tokens. Switch with
xurl auth default or --app.
- Multiple accounts per app: Select with
-u / --username, or set a default with xurl auth default APP USER.
- Token storage:
~/.xurl is YAML. In Docker, use the Hermes subprocess HOME (/opt/data/home in the official image) so tokens land under /opt/data/home/.xurl. Never read or send this file to LLM context.
- Cost: X API access is typically paid for meaningful usage. Many failures are plan/permission problems, not code problems.
Attribution