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

timeline.js:169 performs the same ct0 CSRF-cookie check as thread.js before issuing authenticated timeline GraphQL requests. It reads cookies for https://x.com via page.getCookies and throws AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)') when none is found, because HomeTimeline/UserTweets endpoints require a logged-in session's CSRF token.

Source

Thrown at clis/twitter/timeline.js:169

            name: 'type',
            default: 'for-you',
            choices: ['for-you', 'following'],
            help: 'Which home-timeline feed to read. Default for-you (algorithmic). Use following for the chronological feed of accounts you follow.',
        },
        { name: 'limit', type: 'int', default: 20, help: 'Maximum number of tweets to return (default 20).' },
        { name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the timeline by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps X\'s native ordering.' },
    ],
    columns: ['id', 'author', 'bio', 'text', 'likes', 'retweets', 'replies', 'views', 'created_at', 'url', 'has_media', 'media_urls', 'media_posters', 'card', 'quoted_tweet'],
    func: async (page, kwargs) => {
        const limit = kwargs.limit || 20;
        const timelineType = kwargs.type === 'following' ? 'following' : 'for-you';
        const { endpoint, method, fallbackQueryId } = TIMELINE_ENDPOINTS[timelineType];
        // Cookie context auto-established by framework pre-nav (Strategy.COOKIE + domain).
        // Read CSRF token directly from the cookie store via CDP — zero page.evaluate round-trip.
        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)');
        // Dynamically resolve queryId for the selected endpoint
        const queryId = await resolveTwitterQueryId(page, endpoint, fallbackQueryId);
        // Build auth headers
        const headers = JSON.stringify({
            Authorization: `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
            'X-Csrf-Token': ct0,
            'X-Twitter-Auth-Type': 'OAuth2Session',
            'X-Twitter-Active-User': 'yes',
        });
        // Paginate — fetch in browser, parse in TypeScript
        const allTweets = [];
        const seen = new Set();
        let cursor = null;
        // Runaway guard only; --limit and cursor exhaustion control normal pagination.
        for (let i = 0; i < MAX_PAGINATION_PAGES && allTweets.length < limit; i++) {
            const fetchCount = Math.min(40, limit - allTweets.length + 5); // over-fetch slightly for promoted filtering
            const variables = buildTimelineVariables(timelineType, fetchCount, cursor);
            const apiUrl = buildHomeTimelineUrl(queryId, endpoint, variables);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log into x.com in the browser profile the CLI uses, then rerun the command
  2. Check that a ct0 cookie exists for https://x.com (DevTools > Application > Cookies) — its presence is exactly what this guard tests
  3. If you inject cookies programmatically, make sure you include ct0 (and auth_token) for the x.com domain before the request
  4. Re-authenticate after any x.com logout event; a single expired session invalidates the CSRF pairing
Defensive patterns

Strategy: validation

Validate before calling

const cookies = await page.getCookies({ url: 'https://x.com' });
const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
if (!ct0) throw new Error('x.com session missing (no ct0 cookie): log in first');

Type guard

function isAuthenticated(cookies) {
  return cookies?.some?.((c) => c.name === 'ct0' && !!c.value) ?? false;
}

Try / catch

import { AuthRequiredError } from '@jackwener/opencli/errors';
try {
  const timeline = await fetchTimeline(type, opts);
} catch (e) {
  if (e instanceof AuthRequiredError) {
    console.error('Open a visible browser session, log into x.com, then retry.');
    process.exitCode = 1;
  } else throw e;
}

Prevention

When it happens

Trigger: Running `opencli twitter timeline` or `opencli twitter tweets` while the driven browser profile has no ct0 cookie for x.com — logged-out profile, expired session, cookies cleared, or cookie context targeting the wrong domain.

Common situations: New environment/CI without a logged-in profile; x.com revoked the session (password change, suspicious-activity logout); user wiped browser data; profile switcheroo between twitter.com and x.com cookie domains; long-lived sessions that x.com rotates periodically.

Related errors


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