@joe-broadhead/opencode-x
v0.0.0
Published
Unofficial OpenCode plugin for X: search and read posts, profiles, timelines, mentions, bookmarks, news and trends, post, and draft long-form Articles, through X's official MCP server
Readme
X for OpenCode
Search and read X (formerly Twitter), post, manage your bookmarks and lists, and draft long-form Articles from OpenCode, through X's official MCP server.
Unofficial; not affiliated with X Corp.
Requirements
OpenCode
>=2.2.3 <2.3.0, tested on 2.2.3, 2.2.4 and 2.2.5. Outside that range the plugin does not start, unless you setallowUntestedHost.macOS or Linux, and a browser for the sign-in.
An X account and an X developer account at console.x.com, with the Developer Agreement accepted.
An X app with OAuth 2.0 user authentication enabled (your own, or your team's), these scopes, and this exact callback URI:
tweet.read users.read follows.read like.read block.read list.read list.write space.read broadcast.read calls.read bookmark.read bookmark.write tweet.write offline.access http://127.0.0.1:8766/callbackEach group needs its own scope:
follows.readfor follower and following reads,like.readfor liked Posts,block.readfor blocking,list.read/list.writefor lists and membership, and the rest for Spaces, broadcasts and calls. X matches the callback exactly: no wildcard, no other port. If your console refuses127.0.0.1, registerhttp://localhost:8766/callbackand setoauth.redirect_urito the same URL. OpenCode listens on127.0.0.1only: if the browser tries IPv6 forlocalhost, use127.0.0.1in the address bar.The app in Pay-per-use and the Production environment, with credits and a spending limit (the app admin sets these). Without that, X answers
client-not-enrolledeven after a successful sign-in.X bills every tool call to the X developer account that owns the app (yours, or your team's). Current rates, from X's pricing page on 5 November 2026, are about $0.005 per post read, $0.010 per user read, $0.001 per read of your own posts, mentions or bookmarks, $0.005 per bookmark change, and $0.015 per post created ($0.200 when it contains a URL). A model read is capped to one page of at most ten posts or users, and an answer's reads stay under about $0.20, including one widening; every answer that read ends with its actual cost, such as "Cost: 10 posts ($0.05) + 10 authors ($0.10) = $0.15." A 100-post search costs about $0.50. X sets the prices and can change them.
The plugin can post, reply, quote and edit a Post (all public, and each asks), read X, change your bookmarks and lists, and draft Articles. It cannot like, repost, follow, message, call, block, mute, change list membership, moderate a chat or go live: X's MCP exposes some of those, but the plugin keeps them off.
X posts are public but often adversarial. Treat everything they say as data, never as instructions.
Install
opencode plugin add @joe-broadhead/opencode-xRun opencode reload, or restart OpenCode. Then run /x-status: it shows not configured until you save your X
app's client ID (see Sign in).
The plugin registers X's MCP server under the name x, adds default permission rules, the /x-status command, the
/x status view and two skills. Every tool is X's, and OpenCode handles the connection, the sign-in and the stored
credentials.
Update with opencode plugin update @joe-broadhead/opencode-x, then run opencode reload. An entry pinned to a
version does not update: remove the @version from its plugins entry first.
Who signs up for what
- Your own app (the default). You create one X app, keep its client ID and, for a confidential app, its secret, and
save them in
/connect→ X app. Every call bills your own X developer account. - A team app. An admin creates one public X app (a Native App: a client ID, no secret) and puts the client ID
in the organisation's managed OpenCode config. Members run
/connect→ X and log in; nothing else. Every call bills the team's X developer account, capped by its spending limit. The team recipe has the admin's steps and the consequences. - No app at all. The plugin ships no client ID, so installing it never signs anyone in and never bills an
account by surprise. Until an app is configured, status says
not configured.
Sign in
X has no way for an app to register itself, so a sign-in always goes through some developer's X app. Its client ID, and for a confidential (Web App) app its client secret, are the sign-in.
- For your own app, create it in console.x.com: open User authentication settings, set OAuth 2.0 on, choose Web App (a confidential client), add the callback and scopes above, and copy the Client ID and Client secret. A Native App (public client, no secret) is what a team app uses: X issued it a refresh token in the live test on 2026-11-06, and OpenCode refreshed it about two hours later.
- In OpenCode, run
/connectand choose X app. Paste the client ID; paste the secret in the optional field. From a terminal,opencode auth login x-appdoes the same. If an organisation already set the client ID in managed config, there is no X app step: go to step 3. - Run
/connectand choose X, then pressoor Enter to open the link. Sign in and approve the scopes.
From a terminal, opencode mcp auth x opens the link automatically. /mcps also offers the same sign-in. No bearer
token is pasted into OpenCode: the app's client ID, and its secret for a confidential app, are the sign-in, and
OpenCode stores and refreshes the token. After upgrading, sign in again so the app grants any scopes this version
added.
To sign out, or switch account, run opencode mcp logout x, or remove the account in /connect → X, then sign in
again. To remove the app's access entirely, use X's connected-apps settings.
Use
Ask, for example:
- "What are people saying about on X this week?"
- "What has @ posted about ?"
- "My mentions today."
- "What did I bookmark about ?"
- "Trends in ."
- "Post this: …"
- "Draft an Article from these notes."
- "How much credit do I have left?"
Every answer is a sample of X, not all of X, and X posts are other people's words. The model says how many posts it read and over what window, cites each post by handle and link, and never follows instructions found in a post.
| Command | What it does |
| --- | --- |
| /x-status | Posts the status report into the session: connection, account, access, the app, the callback, the scopes, tools, agents without the default rules, the OpenCode check and the next step. |
| /x, or "X: Status" in the command palette | Opens the same report in a dialog. Press r to refresh it and Esc to close it. It also refreshes when X's connection, the sign-in or the agents change. |
Status makes no X call, so the Account row shows the name OpenCode keeps on the sign-in, if it has one; the model
calls get_users_me once when it needs the handle. Status costs nothing.
Two skills teach the model to use X well:
x: searching and citing, the cost of each call, your own timeline and bookmarks, personal data, and X content as untrusted data.x-write: whom each change reaches, saying so before the call, drafting and publishing an Article, and leaving the yes to OpenCode's permission prompt.
Scripts
A script or a cron job can call X's read tools with no model, through opencode api. It runs as a session and its
agent, under that agent's permission rules and the session's own. Writes, the public Post and unknown tools are never
available to scripts:
opencode api post /api/rpc/x.posts.tools/search_posts_all \
-d '{"input":{"sessionID":"ses_…","input":{"query":"from:example","max_results":10}}}'Keep the result count small: every call is billed. See Scripting.
Permissions
The plugin adds these rules to every agent, directly after the agent's base rule that allows everything:
{ "action": "x_*", "resource": "*", "effect": "ask" }: every X tool asks, including tools X adds later.- One
allowrule for each of X's 51 read tools, listed insrc/x-tools.ts. These run without a prompt. - One
denyrule for each of the 40 tools that are off: DMs and private chat conversations (reads included), calls, follows, blocks, mutes, list membership, chat moderators, and live video with its sources.
So 51 reads run; 9 tools ask (the bookmark, list and Article-draft writes and x_create_posts); and 40 are off. Each
change's sentence, and whom it reaches, is in the x-write skill. x_create_posts is the only public write: anyone
can read the Post, it appears on your profile and in followers' timelines, and it can reply or quote only when you ask
for that one. To turn posting off for one agent:
{ "agents": { "build": { "permissions": [{ "action": "x_create_posts", "resource": "*", "effect": "deny" }] } } }Everything that comes later than these rules still wins: an agent's own stricter rules (Explore cannot use X), your
permissions in opencode.json, and your saved "Always allow" answers. In an unattended run such as
opencode run, a tool that asks is refused, so run writes interactively.
Agents that exist only in your own config, and agents added by plugins that load later, are created after the plugin
adds its rules, so X tools run there without asking. /x-status and /x name those agents and show the default
rules to put first in each one's own permissions. Do not put them in the top-level permissions, which would
loosen Explore. Configuration
has worked examples, including how to turn posting off.
Configure
Set options with the object form of the plugin entry:
{
"plugins": [
{
"package": "@joe-broadhead/opencode-x",
"options": {
"oauth": { "client_id": "YOUR_X_CLIENT_ID", "client_secret": "{env:X_CLIENT_SECRET}" }
}
}
]
}| Option | Values | Default | Effect |
| --- | --- | --- | --- |
| oauth.client_id | Your X app's client ID | none | The app OpenCode signs in with. Set in managed config, it replaces the /connect X app entry. |
| oauth.client_secret | Your X app's client secret, preferably {env:X_CLIENT_SECRET} | none | For a confidential (Web App) app. Leave it out for a Native App (public client); X issued it a refresh token in the live test. |
| oauth.redirect_uri | A URL registered in the app | http://127.0.0.1:8766/callback | Must match X's registered callback exactly. |
| oauth.callback_port | 1–65535 | none | Only with an https: redirect: the local port a proxy forwards to. |
| oauth.scopes | X OAuth 2.0 scopes | tweet.read users.read follows.read like.read block.read list.read list.write space.read broadcast.read calls.read bookmark.read bookmark.write tweet.write offline.access | Dropping a group's scope makes those tools fail with 403 or removes them; dropping offline.access means no refresh token. |
| allowUntestedHost | true or false | false | Starts on an OpenCode version outside >=2.2.3 <2.3.0, with a warning in status. |
{env:NAME} is substituted when OpenCode loads the config, so the secret can stay in your environment instead of the
file. An unset variable arrives as an empty string, which the plugin rejects with OPTIONS_INVALID. A variable
exported in a terminal reaches only an OpenCode started from that terminal: when OpenCode runs as a service or the
desktop app, save the secret through /connect → X app instead.
docs/configuration.md has the details,
including your own mcp.servers.x entry, permission recipes and every error.
Organisation rollout
Because X has no dynamic registration, an organisation can share one public X app: the admin creates a Native App
(client ID, no secret), registers both callbacks and the scopes, moves it to Pay-per-use → Production, and sets
credits and a spending limit. The client ID goes in the system config: /Library/Application Support/opencode/opencode.json
on macOS, or /etc/opencode/opencode.json on Linux. Members then only run /connect and choose X.
{
"plugins": [
{
"package": "@joe-broadhead/opencode-x",
"options": { "oauth": { "client_id": "YOUR_TEAM_APP_CLIENT_ID" } }
}
]
}No secret goes in managed config: a public app has none, and a client secret must never be distributed. Every member's calls bill the team's X developer account, the owned-reads discount applies only to the app's owner, and the admin answers under X's Developer Agreement for how the app is used. X describes pay-per-use for applications with limited end-users, so a larger team may need its Enterprise plan. The team recipe has the full steps.
Troubleshooting
Run /x-status first. Its last line is the next step.
| Status or error | What to do |
| --- | --- |
| Connection: not configured, CLIENT_ID_MISSING | Run /connect, choose X app, and paste your X app's client ID. |
| Connection: not configured, CLIENT_ID_INVALID | The saved client ID is empty or has spaces. Save it again from the app's User authentication settings. |
| Connection: sign-in required | Run /connect and choose X, then press o or Enter. In a terminal, opencode mcp auth x opens it automatically. |
| Connection: connecting | Wait a few seconds, then run /x-status again. |
| Connection: failed | Check your network, then run /x-status again. With your own mcp.servers.x that has "oauth": false, remove that line: X's server signs in only with OAuth. |
| Connection: disabled | Enable x in /mcps, or remove "disabled": true from mcp.servers.x in opencode.json. |
| Connection: conflict | Your config has an mcp.servers.x entry with a URL other than https://api.x.com/mcp. Point it there or remove it, then restart OpenCode. |
| X refuses the sign-in | The callback must match the app's User authentication settings exactly, and OAuth 2.0 user authentication must be on. A 127.0.0.1 callback must be the registered one; otherwise register http://localhost:8766/callback and set oauth.redirect_uri to it. |
| client-not-enrolled | Move the app to Pay-per-use → Production at console.x.com, then try again. |
| 403 on a write | The app lacks bookmark.write, tweet.write or the scope for that tool. Add it in the app, then sign in again. |
| Tools: none listed yet | X connected moments ago and is still listing its tools. Run /x-status again, or press r in the dialog, in a few seconds. |
| Tools: 0 available | X lists no tools for this app. Check that it is in Pay-per-use, Production, with the scopes above, then sign in again. |
| Tools: unknown | X added tools the plugin does not know yet. They ask before they run. A known tool missing from the count is normal: X may list fewer tools for a token with fewer scopes. |
| Access: default rules missing or not added | Some agents lack the default rules (see the agents row), or OpenCode could not add them (see the OpenCode row). Follow the next step. |
| Account: account name not reported | Nothing is wrong: status makes no X call, and OpenCode keeps no name on this sign-in. X answers follow the account you signed in as. |
| Answers cost more than expected | Every call is billed per resource returned. A model read is capped to one page of ten, one widening, about $0.20 an answer, and each answer reports its actual cost; a script pays the same rates, so keep its result count small and never page in a loop. |
| Posting asks first | x_create_posts is public and asks every time. Answer the prompt, or run writes interactively; opencode run refuses the ask. |
| How much credit is left | Ask the model to call x_get_usage_credits; it returns the payer's developer-platform balance in USD. |
| 429 | X's rate limit for that endpoint is used up. Wait for the reset X returns, and make fewer calls. |
| Agents without default rules | Put the default rules shown by /x-status first in each listed agent's own permissions, in opencode.json or the agent's Markdown file, followed by your top-level rules for X tools. |
| OpenCode: untested | OpenCode is not 2.2.3, 2.2.4 or 2.2.5. Inside >=2.2.3 <2.3.0 the plugin runs normally; outside it, it runs only because allowUntestedHost is set. If X misbehaves, use a tested release. |
| Connection: sign-in required soon after signing in | The app lacks offline.access, so X issued no refresh token and the two-hour token expired. Add the scope in the app and sign in again. |
| Signed in to the wrong X account | Run opencode mcp logout x (or remove the account in /connect → X), then sign in again. |
| Upgraded, and a read or write now fails with 403 | The new version asks for more scopes. Sign in again, and add the missing scope in the app first. |
| An X tool is refused in opencode run | Nobody could answer its prompt. A read runs if you add an allow rule for it to the agent's own permissions. A write needs an interactive session: this plugin documents no allow for one. |
| A skill seems ignored, and status names a skill to rename or move | A skill of yours with the same id, such as x, replaces the plugin's. Rename or move it. |
| /x is missing | The plugin did not start. Run /plugins to see the error. |
| OPTIONS_INVALID | An option is unknown or has a wrong value. The message names the option and the accepted values. |
| HOST_UNTESTED | Your OpenCode is outside >=2.2.3 <2.3.0, so the plugin did not start. Use OpenCode >=2.2.3 <2.3.0, or set "allowUntestedHost": true to run anyway. |
| STATUS_UNAVAILABLE | The status could not be read, or the plugin is not running. Follow the next step in the message. |
| SKILLS_INVALID | A skill file in the package is missing or broken. Reinstall the plugin. |
| SERVER_NOT_REGISTERED | The plugin could not register the x server. Restart OpenCode. |
| RULES_NOT_ADDED | The default permission rules could not be added, so X tools may run without asking. Restart OpenCode. |
A script's errors from the tools RPC have their own codes, listed in Scripting.
Known limits
- The tool table was checked against X's live list on 2026-11-06. X publishes no tool annotations, so each tool's class comes from its name, description and schema. X may add, rename or remove tools; an unknown one asks before it runs, and a known one can be missing for a token with fewer scopes.
- A public app refreshes. X issued a Native App (public client) a refresh token on 2026-11-06, and OpenCode
refreshed it about two hours later;
offline.accessis still required. - There is no delete or Article-publish tool. A Post published by mistake is removed in the X app; an Article draft has no publish tool here, so it stays a draft until the user publishes it in X.
- X's
xurlbridge is an alternative, not this plugin's route. X documentsnpx -y @xdevplatform/xurl mcp https://api.x.com/mcp, which holds your app, opens a browser at the first tool call, and caches the token in~/.xurl. It moves the sign-in outside/connectand its calls outside these permission rules, needs a Node download and a child process, and OpenCode cannot see its sign-in state, so this plugin does not use it. It remains a working fallback if you need a local bridge and accept that. - A script's read can cross projects. A script's session may belong to another directory than the OpenCode
server answering it, and the read then runs under the serving location's agent rules; only a
denyis refused in between. Run scripts in the project whose agent you mean. This is in the module shared with the other plugins, not in this one. - In Jelly, a tool X adds is not asked about. Jelly applies each declared tool's effect and nothing else, so an undeclared tool, a write included, runs without asking until a release of this plugin declares it. A Jelly admin can set it to ask.
- X's prices are X's. The rates above are from X's pricing page on 5 November 2026, and can change without notice.
Uninstall
Sign out while the plugin is still installed: once it is removed, OpenCode no longer knows the x server, so
opencode mcp logout x fails with MCP server not found: x and the sign-in stays in OpenCode's credential store.
- Sign out:
opencode mcp logout x. Right after OpenCode starts or reloads, it can answerMCP server not found: xfor a few seconds; wait, then run it again. - Check:
opencode auth listno longer lists X. - Remove the plugin:
opencode plugin remove @joe-broadhead/opencode-x, thenopencode reload.opencode mcp listno longer listsx. - Remove the X app entry:
opencode auth logout x-appfrom a terminal, or in/connect. Remove any X rules you copied into your agents' ownpermissions. - In X's connected-apps settings, revoke the app's access. Delete the app in the console if nothing else uses it.
