# Xurl

> Interact with X (Twitter) via xurl, the official X API CLI: post, reply, quote, delete, search, read timelines/mentions, like, repost, bookmark, follow, DM, upload media, raw v2 endpoints. Triggers on "post to X", "tweet this", "reply to [X post]", "search X for", any x.com/status URL, or "xurl". Do NOT trigger on bare "X" meaning the letter, a variable, or an unknown.

- Skill: `chrisliu298/xurl` (Agent Skill)
- Install (CLI): `npx skillmds@latest add chrisliu298/xurl`
- Raw SKILL.md: https://api.skillmd.com/api/skills/chrisliu298/xurl/raw
- Safety review: WARNING
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: chrisliu298 (https://skillmd.com/u/chrisliu298)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/chrisliu298/xurl

---


# xurl — X (Twitter) API via the Official CLI

`xurl` is the X developer platform's official CLI for the X API. It has 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

---

## 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 fills `~/.xurl` with secrets manually on their own machine.
- **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 and rotation must be done by the user manually, outside the agent session. Tokens persist to `~/.xurl` in YAML. Each app has isolated tokens. OAuth 2.0 tokens auto-refresh.

---

## Installation

Installed via Homebrew on this machine:

```bash
brew install --cask xdevplatform/tap/xurl
```

Other options: `curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | bash`, `npm install -g @xdevplatform/xurl`, or `go install github.com/xdevplatform/xurl@latest`.

Verify:

```bash
xurl --help
xurl auth status
```

---

## One-Time User Setup (user runs these outside the agent)

These steps involve pasting secrets and must be done by the user directly. Direct the user to this block; do not execute it for them.

1. Open an app at https://developer.x.com/en/portal/dashboard (redirect URI `http://localhost:8080/callback` if using OAuth 2.0).
2. Register the app locally (user runs this):
   ```bash
   xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
   ```
3. Authenticate with one of:
   - `xurl auth oauth2` — browser PKCE flow (recommended; auto-refreshing tokens)
   - `xurl auth app --bearer-token ...` — app-only reads
   - `xurl auth oauth1 --consumer-key ... --consumer-secret ... --access-token ... --token-secret ...` — user-context v1.1
4. Verify: `xurl auth status` and `xurl whoami`.

---

## 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

```bash
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

```bash
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
```

**Reading X long-form articles.** When a post contains a long-form X article (the body is just a `t.co` link to `x.com/i/article/...`), `xurl read` returns only the title and a 500-status link — the article body isn't in the default response. Use raw mode and request the `article` tweet field:

```bash
xurl "/2/tweets/POST_ID?tweet.fields=article"
```

The response includes `data.article` with:
- `title` — article headline
- `preview_text` — short snippet
- `plain_text` — full article body (this is what `read` is missing)
- `cover_media`, `media_entities` — embedded image media keys; resolve via `?expansions=attachments.media_keys&media.fields=url`

> **Not gated — don't mistake the thin response for a permissions block.** The `status: 500` on the t.co link and the title-only `xurl read` output look like the article is gated/blocked, but they are not. The body is simply absent from the default tweet fields; requesting `tweet.fields=article` returns `plain_text` in full with the same auth (no extra scope, no paid tier). If `xurl read` gives you only a title for an `x.com/i/article/...` link, always retry with the raw `?tweet.fields=article` call before concluding it can't be read.

### Users, Timeline, Mentions

```bash
xurl whoami
xurl user elonmusk
xurl user @XDevelopers

xurl timeline -n 25
xurl mentions -n 20
```

### Engagement

```bash
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

```bash
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

```bash
xurl dm @someuser "Hey, saw your post!"
xurl dms -n 25
```

### Media Upload

```bash
# Auto-detect type
xurl media upload photo.jpg
xurl media upload video.mp4

# Explicit type/category
xurl media upload --media-type image/jpeg --category tweet_image photo.jpg

# Videos need server-side processing — check status (or poll)
xurl media status MEDIA_ID
xurl media status --wait MEDIA_ID

# Full workflow
xurl media upload meme.png                  # returns media id
xurl post "lol" --media-id MEDIA_ID
```

---

## Raw API Access

For anything not covered by shortcuts, use raw curl-style mode against any X API v2 endpoint:

```bash
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:

```json
{ "data": { "id": "1234567890", "text": "Hello world!" } }
```

Errors are also JSON:

```json
{ "errors": [ { "message": "Not authorized", "code": 403 } ] }
```

---

## 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 → have the user re-run `xurl auth oauth2` (or the relevant auth subcommand) outside the agent session.
- 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

1. Verify prerequisites: `xurl --help` and `xurl auth status`.
2. If auth is missing, stop and direct the user to the "One-Time User Setup" section — do NOT attempt to register apps or pass secrets yourself.
3. Start with a cheap read (`xurl whoami`, `xurl user @handle`, `xurl search ... -n 3`) to confirm reachability.
4. Confirm the target post/user and the user's intent before any write action (post, reply, like, repost, DM, follow, block, delete).
5. Use JSON output directly — every response is already structured.
6. Never paste `~/.xurl` contents back into the conversation.

---

## Notes

- **Rate limits:** X enforces per-endpoint rate limits. A 429 means wait and retry. Write endpoints 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.
- **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. 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

- Upstream CLI: https://github.com/xdevplatform/xurl (X developer platform)
- Upstream agent skill: https://github.com/NousResearch/hermes-agent/blob/main/skills/social-media/xurl/SKILL.md (Hermes Agent / openclaw)
- Adapted for this dotfiles repo: frontmatter swapped to local conventions; safety guardrails preserved.

