jackwener/OpenCLI · error · AuthRequiredError

Not logged into x.com (no ct0 cookie)

Error message

Not logged into x.com (no ct0 cookie)

What it means

resolveUserTimelineContext drives a logged-in x.com browser session and needs the 'ct0' CSRF cookie that x.com sets for authenticated users. After loading x.com it calls page.getCookies() and throws AuthRequiredError when no ct0 cookie is present, because GraphQL calls (UserTweets, UserByScreenName) require the X-Csrf-Token header derived from it. Without ct0 the requests would be rejected as unauthenticated, so the tool fails fast.

Source

Thrown at clis/twitter/user-timeline.js:190

    if (!username) {
        await page.goto('https://x.com/home');
        await page.wait({ selector: '[data-testid="primaryColumn"]' });
        const href = unwrapBrowserResult(await page.evaluate(`() => {
            const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
            return link ? link.getAttribute('href') : null;
        }`));
        if (!href || typeof href !== 'string') {
            throw new AuthRequiredError('x.com', 'Could not detect logged-in user. Are you logged in?');
        }
        username = normalizeTwitterScreenName(href);
        if (!username) {
            throw new AuthRequiredError('x.com', 'Could not detect logged-in user. Are you logged in?');
        }
    }

    const cookies = await page.getCookies({ url: 'https://x.com' });
    const ct0 = cookies.find((cookie) => cookie.name === 'ct0')?.value || null;
    if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');

    const userTweetsOperation = await resolveTwitterOperationMetadata(page, 'UserTweets', USER_TWEETS_OPERATION);
    const userByScreenNameOperation = await resolveTwitterOperationMetadata(page, 'UserByScreenName', USER_BY_SCREEN_NAME_OPERATION);
    const headers = JSON.stringify({
        Authorization: `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
        'X-Csrf-Token': ct0,
        'X-Twitter-Auth-Type': 'OAuth2Session',
        'X-Twitter-Active-User': 'yes',
    });
    const userByScreenNameUrl = buildUserByScreenNameUrl(userByScreenNameOperation, username);
    const userId = unwrapBrowserResult(await page.evaluate(`async () => {
        const resp = await fetch(${JSON.stringify(userByScreenNameUrl)}, { headers: ${headers}, credentials: 'include' });
        if (!resp.ok) return null;
        const data = await resp.json();
        return data?.data?.user?.result?.rest_id || null;
    }`));
    if (!userId) throw new CommandExecutionError(`Could not resolve @${username}`);
    return { username, userId, headers, userTweetsOperation };

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log into x.com in the browser session/profile the tool uses (complete the login flow manually in that browser, then retry).
  2. Persist the browser profile (user-data-dir / storageState) so cookies survive between runs.
  3. Verify the page actually reaches https://x.com before getCookies is called; if x.com redirects to a login page, re-authenticate.
  4. Re-run after clearing stale state if x.com invalidated the session; if cookie names changed upstream, update the cookie lookup.

Example fix

// before: reusing a throwaway context
const page = await browser.newPage();
await runTimeline(page, 'someuser');
// after: reuse a persistent, logged-in profile
const ctx = await browser.createBrowserContext({
  userDataDir: '/home/me/.opencli/x-profile' // already logged into x.com
});
const page = await ctx.newPage();
await runTimeline(page, 'someuser');
Defensive patterns

Strategy: validation

Validate before calling

const cookies = await page.getCookies({ url: 'https://x.com' });
if (!cookies.some((c) => c.name === 'ct0')) {
  throw new Error('Run the login flow for x.com before fetching the timeline.');
}

Try / catch

try {
  await fetchUserTimeline(page, username);
} catch (err) {
  if (err instanceof AuthRequiredError) {
    await runXLoginFlow(page); // open x.com/login, wait for user
    return fetchUserTimeline(page, username);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling the user-timeline command/`context` flow when the Puppeteer/Playwright page used by resolveUserTimelineContext is not logged into x.com: fresh browser profile with no session, expired session cookies, logged-out/incognito context, or x.com failing to set the ct0 cookie on the loaded page.

Common situations: Running the CLI for the first time without ever logging into x.com in the automation profile; x.com logged the session out server-side (password change, security event); using a data dir that was wiped; x.com A/B changes altering cookie names; running against a page that landed on a login or error page instead of x.com.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/87b7bd0a3de73001. Report an issue: GitHub.