---
name: x-authentication-and-setup
description: Drive X (Twitter) inside the user's REAL logged-in Chrome profile (not a throwaway test browser) via chrome-devtools-axi over CDP. Use when asked to log into X, read/scroll the X feed, draft or send X posts, or automate any authenticated X action on the actual signed-in account. Covers the Chrome-136 default-profile debugging block, profile cloning, the axi stray-bridge trap, and a screenshot-verify-until-trusted loop that drops screenshots once an action proves repeatable.
---

# X authentication & setup (real Chrome, not a test browser)

Goal: control the user's **already-logged-in** X account in their real Chrome profile, using
`chrome-devtools-axi` over the Chrome DevTools Protocol. The hard parts are getting a debuggable
browser that keeps the login, and pointing axi at the *right* browser.

All commands live in `../tools/`. Run them from there.

**Related skill:** once the session is up, [../messaging/SKILL.md](../messaging/SKILL.md) drives
selective, human-paced engagement (curated reply queue, send-by-default replies, rate limits — no following).

## Dependencies

Everything below must be present. macOS-specific (the Chrome paths + `osascript` assume macOS).

| Dep | Why | Check | Install |
|-----|-----|-------|---------|
| **macOS** | Chrome paths, `osascript` quit, Keychain cookie decrypt | `uname` → Darwin | — |
| **Google Chrome 136+** | the browser being driven (tested 149) | `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version` | google.com/chrome |
| **Node.js + npx** | runs `chrome-devtools-axi` and its `chrome-devtools-mcp` bridge | `node -v && npx -v` | `brew install node` |
| **chrome-devtools-axi** | the CDP CLI (`axi()` wrapper). Auto-fetched by `npx -y`; pin for speed | `npx -y chrome-devtools-axi -v` | `npm i -g chrome-devtools-axi` (optional) |
| **python3** | JSON/state handling in every script | `python3 -V` | ships with macOS / `brew install python` |
| **sqlite3** | reads the Cookies DB to find the logged-in profile | `sqlite3 -version` | ships with macOS |
| **rsync** | clones the profile into the debug user-data-dir | `rsync --version` | ships with macOS |
| **curl** | CDP ground-truth target list (`localhost:9222/json`) | `curl --version` | ships with macOS |
| **bash** | the scripts. Stock macOS bash 3.2 works (no `mapfile`/`^^` used) | `bash --version` | — |

Optional speedup — skip the ~30s npx bootstrap of the MCP bridge each run:
```bash
npm install -g chrome-devtools-mcp
export CHROME_DEVTOOLS_AXI_MCP_PATH="$(npm prefix -g)/lib/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js"
```

One-shot preflight:
```bash
for c in node npx python3 sqlite3 rsync curl; do command -v "$c" >/dev/null && echo "ok: $c" || echo "MISSING: $c"; done
[ -x "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ] && echo "ok: Chrome" || echo "MISSING: Chrome"
```

## The one fact that blocks everyone

**Chrome 136+ refuses `--remote-debugging-port` when launched on the DEFAULT user-data-dir.**
So you cannot attach to a normally-running Chrome. You also cannot enable debugging on an
already-running instance. The fix: fully quit Chrome, **clone the logged-in profile** into a
separate user-data-dir, and debug the clone. Cookies still decrypt because it is the same
machine + Keychain ("Chrome Safe Storage").

## Quick start

```bash
cd ../tools
./which-profile.sh            # which profile holds the X login (auth_token/ct0/twid)
./launch-real-chrome.sh       # quits Chrome, clones that profile, launches debug Chrome on :9222
./attach.sh                   # stops stale bridge, attaches axi, confirms "LOGGED IN as @handle"
./verify-login.sh             # cheap re-check anytime
./scroll-feed.sh 5            # scroll home feed 5x
./post.sh "whats up gamers?"  # DRAFTS only — never sends without --send
```

Then drive anything directly:
```bash
./x open https://x.com/home
./x snapshot          # accessibility tree with @uid refs
./x eval "() => location.href"
```

## Setup workflow (do this once per session)

1. **Run a HEADED Chrome** so you can SEE what's happening while building/verifying. The launcher
   opens a visible window on purpose.
2. `./launch-real-chrome.sh` — clean clone + debug launch. On repeat runs use `--no-copy` to skip
   the ~1 GB rsync.
3. `./attach.sh` — **always run this before driving.** It does `chrome-devtools-axi stop` to kill
   any stale bridge, then verifies axi controls the logged-in tab.
4. Confirm `LOGGED IN as @handle`. If not, the clone lost its session — re-clone (drop `--no-copy`).

## Gotchas (learned the hard way — encoded in the scripts)

- **Stray-bridge trap.** If an axi bridge was ever started *without* `CHROME_DEVTOOLS_AXI_BROWSER_URL`,
  it spawns its OWN throwaway, logged-out browser and ignores the env on every later call. Symptom:
  `./x eval` says `logged-out / x.com/`, while raw CDP shows the logged-in `x.com/home`. Fix:
  `./attach.sh` (stops the bridge) and only ever call axi through `./x` (env always set). Ground-truth
  check: `curl -s localhost:9222/json` vs `./x pages` — they must agree.
- **`fill` eats the first character** of X's contenteditable composer (`"whats" -> "hats"`). Use
  click + `type` instead (`post.sh` does this). `type` in a single call is reliable.
- **Refs go stale** (`STALE_REF`). Re-`snapshot` to get a fresh `@uid` right before clicking/typing.
- **Which profile has the login?** Detect via the Cookies sqlite (`auth_token`,`ct0`,`twid` for
  `host_key like '%x.com'`), not by profile name. `which-profile.sh` does this.

## Safety: never auto-send

Posting, replying, following, DMing are outward, irreversible actions. `post.sh` **drafts only**
by default and requires an explicit `--send`. Do not wire `--send` on without the user saying so.

## The verify-then-go-fast loop (the point of this skill)

While an action is unproven, you must confirm its effect actually landed on the DOM — not just that
a command returned. Use `verify-dom.sh`, which keeps a confidence ledger:

```bash
./post.sh "hello world"
./verify-dom.sh --action post-draft \
  --assert "() => /hello world/.test((document.querySelector('[data-testid=\"tweetTextarea_0\"]')||{}).innerText||'')"
```

- **Untrusted action** → it takes a screenshot AND runs the assertion. Open the screenshot, confirm
  the change is really on screen. This is how you catch outputs that "succeeded" but landed nowhere.
- **Self-learn on FAIL** → the assertion failed or the screenshot is wrong: run `./x snapshot`, find
  the real selector/`@uid`, **edit the action script**, and re-verify from zero. The ledger resets to 0.
- **Trust** → after `TRUST_THRESHOLD` (5) consecutive passes, the action is marked trusted. From then
  on `verify-dom.sh` skips the screenshot and runs the cheap assertion only — fewer tokens, faster.
  Only drop screenshots once you've watched it work 5–6 times.
- **Regression** → any later FAIL revokes trust and you're back to screenshots until it re-earns trust.

Inspect/reset trust with `./confidence.sh` and `./confidence.sh reset [action]`.

## Files

```
x-authentication-and-setup/SKILL.md   (this file)
tools/
  lib.sh                shared config + axi() wrapper + profile/cookie helpers
  which-profile.sh      find the profile holding the X login
  launch-real-chrome.sh quit Chrome, clone profile, launch debug Chrome (:9222)
  attach.sh             kill stale bridge, attach axi, confirm logged in
  x                     pinned axi wrapper — run ALL CDP calls through this
  verify-login.sh       cheap logged-in assertion
  scroll-feed.sh        scroll the home feed N times
  post.sh               draft (default) / --send a post; uses type, not fill
  verify-dom.sh         screenshot+assert+confidence ledger (the trust loop)
  confidence.sh         view/reset the ledger
  state/                confidence.json + shots/  (runtime, git-ignore)
```
