Social and communities

x.com

X (Twitter): 2 skills for AI agents

Notes an AI agent reads before it works on x.com: what to open, what to call, what breaks. Written by browser-harness contributors after real runs, and kept on GitHub under the MIT license.

Give it to your agent

Any coding agent

Start the task with this line. The Markdown version is the same notes without the page around them.

Read https://browser-use.com/skills/x.md, then <your task>

Browser Harness

Save the files into your workspace and turn domain skills on. The code below calls its helpers, such as http_get and js. What Browser Harness is.

d="${BH_AGENT_WORKSPACE:-$HOME/.config/browser-harness/agent-workspace}/domain-skills/x"
mkdir -p "$d" && cd "$d"
curl -sLO https://raw.githubusercontent.com/browser-use/browser-harness/main/agent-workspace/domain-skills/x/article-source-recovery.md
curl -sLO https://raw.githubusercontent.com/browser-use/browser-harness/main/agent-workspace/domain-skills/x/posting.md
export BH_DOMAIN_SKILLS=1

Recover canonical sources from public X article cards

Use this when a public x.com/<handle>/status/<id> post contains an X Article card, but opening the card itself redirects an anonymous browser to login.

Public status extraction

A public status page can render the post and article card without authentication even when the article route is gated. Inspect links in the post before clicking:

Array.from(document.querySelectorAll('article a')).map(a => ({
  text: (a.innerText || '').trim(),
  href: a.href,
})).filter(x => x.href)

Article cards commonly point to https://x.com/i/article/<id>. An anonymous visit to that route may redirect to x.com/i/jf/onboarding/web?...mode=login; do not interpret this as evidence that the status itself is private or deleted.

Cross-post fallback

Authors sometimes cross-post the same article on LinkedIn or another public profile and link the canonical external page there. A LinkedIn sign-in modal may cover the page visually while the public post text and anchors remain in the DOM.

Inspect the public DOM rather than entering credentials:

({
  description: document.querySelector('meta[name="description"]')?.content || null,
  body: (document.body.innerText || '').slice(0, 12000),
  links: Array.from(document.querySelectorAll('a')).map(a => ({
    text: (a.innerText || '').trim(),
    href: a.href,
  })),
})

LinkedIn wraps external links as https://www.linkedin.com/redir/redirect?url=<encoded-url>&.... Decode the url query parameter, open that canonical URL directly, and validate the article title and content there.

Guardrails

  • Treat the X status and the author's canonical page as primary sources; comments and search snippets are secondary context.
  • If no author-controlled canonical page is recoverable, cite the public status and mark the X Article body as inaccessible rather than reconstructing it from commentary.
  • Never type credentials from screenshots or attempt to bypass a login wall.

article-source-recovery.md · updated Jul 13, 2026 · 267 words

X (Twitter) - Posting & Auth

Login

Navigate to https://x.com/i/flow/login (not https://x.com/) to get the full sign-in modal. The homepage may render a truncated version of the auth buttons.

Google sign-in (FedCM) - cannot be automated

The "Sign in as <user>" Google button uses Chrome's Federated Credential Management (FedCM) API. CDP mouse events (Input.dispatchMouseEvent) do not count as a trusted user gesture for FedCM, and the button lives in a sandboxed cross-origin iframe (accounts.google.com). Clicking it via automation has no effect.

If the user wants Google sign-in, ask them to click the button themselves, then wait for confirmation before proceeding.

Username / password flow

Use the standard form on https://x.com/i/flow/login. Ask the user for credentials - do not guess or read them from screenshots.

Composing and posting a tweet

Once logged in, the home feed is at https://x.com/home.

import json
 
# Find the compose box and type
result = js(r'''
  var el = document.querySelector("[data-testid=\"tweetTextarea_0\"]");
  if (!el) return null;
  var r = el.getBoundingClientRect();
  return JSON.stringify({x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2)});
''')
if result is None:
    raise RuntimeError("compose textarea not found — are you logged in and on x.com/home?")
pos = json.loads(result)
click_at_xy(pos["x"], pos["y"])
type_text("hello world!")
 
# Find and click Post
btn = js(r'''
  var b = document.querySelector("[data-testid=\"tweetButtonInline\"]")
       || document.querySelector("[data-testid=\"tweetButton\"]");
  if (!b) return null;
  var r = b.getBoundingClientRect();
  return JSON.stringify({x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2)});
''')
if btn is None:
    raise RuntimeError("post button not found — did the compose surface fail to open?")
pos = json.loads(btn)
click_at_xy(pos["x"], pos["y"])

Stable selectors

ElementSelector
Compose textbox[data-testid="tweetTextarea_0"]
Post button (home feed inline)[data-testid="tweetButtonInline"]
Post button (modal)[data-testid="tweetButton"]

Confirmation

A toast "Your post was sent." appears at the bottom of the page after a successful post. Verify with a screenshot.

Gotchas

  • The home feed compose area and the sidebar "Post" button both open a compose surface, but tweetButtonInline is specific to the inline feed composer. If you open a modal (e.g. via the sidebar "Post" button), use tweetButton instead.
  • If the Grok side panel opens unexpectedly, click a neutral area (e.g. left sidebar) to dismiss it before interacting with the compose box.
  • twitter.com redirects to x.com - all canonical URLs use x.com.

posting.md · updated May 4, 2026 · 387 words

Sites change. These notes describe what worked when they were last updated, not a guarantee they still do. If something broke, the fix is a pull request to browser-harness.