{"record":{"id":"2af5f68821b29eec","repo":"jackwener/OpenCLI","slug":"not-logged-into-x-com-no-ct0-cookie-2af5f6","errorCode":null,"errorMessage":"Not logged into x.com (no ct0 cookie)","messagePattern":"Not logged into x\\.com \\(no ct0 cookie\\)","errorType":"exception","errorClass":"AuthRequiredError","httpStatus":null,"severity":"error","filePath":"clis/twitter/search.js","lineNumber":289,"sourceCode":"        { name: 'product', type: 'string', choices: PRODUCT_CHOICES, help: 'Which X search tab to read: top (default), live (Latest), photos, videos. Maps to the f= URL param.' },\n        { name: 'from', type: 'string', help: 'Restrict to tweets authored by <user>. Leading @ is stripped. Equivalent to appending `from:<user>` to the query.' },\n        { name: 'has', type: 'string', choices: HAS_CHOICES, help: 'Restrict to tweets that have media|images|videos|links|replies. Maps to X\\'s `filter:<has>` operator.' },\n        { name: 'exclude', type: 'string', choices: EXCLUDE_CHOICES, help: 'Exclude tweets matching <type>: replies|retweets|media|links. Maps to X\\'s `-filter:<x>` operator (retweets → -filter:nativeretweets).' },\n        { name: 'limit', type: 'int', default: 15, help: 'Maximum number of tweets to return (default 15). Result count after server-side filtering.' },\n        { name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the results 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.' },\n    ],\n    columns: ['id', 'author', 'bio', 'text', 'created_at', 'likes', 'views', 'url', 'has_media', 'media_urls', 'media_posters', 'card', 'quoted_tweet'],\n    func: async (page, kwargs) => {\n        const finalQuery = buildSearchQuery(kwargs.query, kwargs);\n        if (!finalQuery) {\n            throw new ArgumentError('twitter search query is empty', 'Provide a non-empty <query>, or use at least one of --from / --has / --exclude.');\n        }\n        if (!Number.isInteger(Number(kwargs.limit)) || Number(kwargs.limit) <= 0) {\n            throw new ArgumentError('twitter search --limit must be a positive integer', 'Example: opencli twitter search opencli --limit 15');\n        }\n        const cookies = await page.getCookies({ url: 'https://x.com' });\n        const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;\n        if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');\n        await page.goto('https://x.com/home', { waitUntil: 'load', settleMs: 1000 });\n        const operation = await resolveTwitterOperationMetadata(page, 'SearchTimeline', SEARCH_TIMELINE_OPERATION);\n        const headers = JSON.stringify({\n            'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,\n            'X-Csrf-Token': ct0,\n            'X-Twitter-Auth-Type': 'OAuth2Session',\n            'X-Twitter-Active-User': 'yes',\n            'Content-Type': 'application/json',\n        });\n        const product = resolveSearchProduct(kwargs);\n        const results = [];\n        const seen = new Set();\n        let cursor = null;\n        // Runaway guard only; --limit and cursor exhaustion control normal pagination.\n        for (let i = 0; i < MAX_PAGINATION_PAGES && results.length < kwargs.limit; i++) {\n            const fetchCount = Number(kwargs.limit) - results.length + 10;\n            const [requestUrl, requestPayload] = buildSearchTimelineRequest(operation, finalQuery, product, fetchCount, cursor);\n            const requestBody = JSON.stringify(requestPayload);","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/search.js#L271-L307","documentation":"AuthRequiredError thrown before the search runs when the browser profile has no ct0 cookie for https://x.com. The ct0 cookie is X's CSRF token required to sign authenticated GraphQL requests (paired with the bearer token and X-Csrf-Token header), so its absence means the session is not logged in.","triggerScenarios":"Running `opencli twitter search ...` with a fresh/incognito browser profile, after x.com logout, after cookies were cleared or expired, or when page.getCookies returns no x.com cookies.","commonSituations":"First run without ever logging into x.com in the automation browser, cookie purge by browser settings, logging out manually, or pointing opencli at the wrong profile directory.","solutions":["Log into x.com in the browser session opencli uses, then re-run the command","Refresh/restore the session cookies (ct0 and auth_token)","Point opencli at the correct browser profile that holds the x.com session","Re-run `opencli login`/auth flow if the CLI provides one"],"exampleFix":"// before\nopencli twitter search opencli  // AuthRequiredError: no ct0 cookie\n// after\nopencli login   # or manually log into x.com in the controlled browser\nopencli twitter search opencli","handlingStrategy":"validation","validationCode":"const cookies = await page.getCookies({ url: 'https://x.com' });\nif (!cookies.find((c) => c.name === 'ct0')) {\n  throw new Error('Not logged into x.com — run the login flow first');\n}","typeGuard":"function hasXSession(cookies) { return Array.isArray(cookies) && cookies.some((c) => c.name === 'ct0' && c.value); }","tryCatchPattern":"try {\n  await opencli.twitter.search(q);\n} catch (e) {\n  if (e.name === 'AuthRequiredError') {\n    await runXLoginFlow();   // open x.com, log in, persist cookies\n    await opencli.twitter.search(q);\n  } else throw e;\n}","preventionTips":["Log into x.com in the automation browser profile before scripted runs","Persist and reuse the profile that holds the session cookies","Re-check ct0 freshness at the start of long jobs","Avoid clearing cookies between runs"],"tags":["auth","twitter","cookies","csrf","login"],"backgroundTag":"missing-auth-session","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}