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

listRemoveUser navigates to x.com and reads the browser cookies looking for the ct0 cookie, which X.com sets on login and uses as the CSRF token for authenticated GraphQL calls. If no ct0 cookie exists it throws AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)') at list-remove-core.js:66, because list mutations are impossible without an authenticated session.

Source

Thrown at clis/twitter/list-remove-core.js:66

    }
    return { ok: false, error: `HTTP ${status}` };
}

export async function listRemoveUser(page, kwargs) {
        const listId = String(kwargs.listId || '').trim();
        const username = String(kwargs.username || '').replace(/^@/, '').trim();
        if (!listId || !/^\d+$/.test(listId)) {
            throw new ArgumentError(`Invalid listId: ${JSON.stringify(kwargs.listId)}. Expected numeric ID.`);
        }
        if (!username) throw new ArgumentError('twitter list-remove username is required');

        // Strategy.UI does not get a domain URL pre-nav from the framework.
        // This page context is load-bearing for pre-target GraphQL calls below.
        await page.goto('https://x.com');
        await page.wait(3);
        const cookies = await page.getCookies({ url: 'https://x.com' });
        const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
        if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');

        const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
        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 userLookupUrl = buildUserByScreenNameQueryUrl(userByScreenNameQueryId, username);
        const userId = unwrapBrowserResult(await page.evaluate(`async () => {
            const resp = await fetch(${JSON.stringify(userLookupUrl)}, { headers: ${headers}, credentials: 'include' });
            if (!resp.ok) return null;
            const d = await resp.json();
            return d.data?.user?.result?.rest_id || null;
        }`));
        if (!userId) throw new CommandExecutionError(`Could not resolve user @${username}`);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log into x.com in the same browser profile the CLI uses, then re-run the command
  2. Point the CLI at the browser profile where you are already logged in (set the user-data-dir/profile option for the launch)
  3. Re-authenticate if your session was invalidated (password change, logout-everywhere, security challenge)
  4. Verify with a read-only command (e.g. listing your lists) that the profile's session works before write operations
  5. Ensure cookies aren't blocked for x.com in the automation browser (check page.getCookies({ url: 'https://x.com' }) output)

Example fix

// before (fresh profile, not logged in)
await cliRun('twitter list-remove 1734567890123456789 alice');
// after (launch with the logged-in profile, or pre-auth first)
await launchBrowser({ userDataDir: '/path/to/logged-in-profile' });
await cliRun('twitter list-remove 1734567890123456789 alice');
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check the session before a write command:
const cookies = await page.getCookies({ url: 'https://x.com' });
if (!cookies.some(c => c.name === 'ct0')) {
  throw new Error('x.com session missing — log into x.com in the automation browser profile first');
}

Type guard

function hasCt0Cookie(cookies) {
  return Array.isArray(cookies) && cookies.some(c => c.name === 'ct0' && !!c.value);
}

Try / catch

import { AuthRequiredError } from '@jackwener/opencli/errors';
try {
  await run(`opencli twitter list-remove ${listId} ${username}`);
} catch (e) {
  if (e instanceof AuthRequiredError || /no ct0 cookie/.test(e.message)) {
    // pause and prompt the user to log into x.com in the CLI's browser profile, then retry
    await promptXComLogin();
    return run(`opencli twitter list-remove ${listId} ${username}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Running the browser command without ever logging into x.com in the automation browser's profile; the session expired or was logged out so cookies were cleared; using a fresh/incognito browser context or a different user-data-dir than the one where login happened; cookies filtered by URL such that the x.com cookie jar is empty (wrong domain, cookie policy blocking cookies); a proxy or region redirect putting the session on a different domain's cookies.

Common situations: CI or headless runs with a brand-new browser profile; logging into x.com in your normal Chrome but the CLI launching its own isolated profile; X logged the profile out after a password change or security challenge; system clock skew invalidating the session; cookie blocking extensions or hardened browser settings.

Related errors


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