{"record":{"id":"d929f9d63a9c9212","repo":"jackwener/OpenCLI","slug":"not-logged-into-x-com-no-ct0-cookie-d929f9","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/thread.js","lineNumber":128,"sourceCode":"    strategy: Strategy.COOKIE,\n    browser: true,\n    args: [\n        { name: 'tweet-id', positional: true, type: 'string', required: true, help: 'Tweet numeric ID (e.g. 1234567890) or full status URL' },\n        { name: 'limit', type: 'int', default: 50 },\n        { name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the thread by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps the conversation\\'s structural ordering.' },\n    ],\n    columns: ['id', 'author', 'bio', 'text', 'likes', 'retweets', 'url', 'has_media', 'media_urls', 'media_posters', 'card', 'quoted_tweet'],\n    func: async (page, kwargs) => {\n        let tweetId = kwargs['tweet-id'];\n        const urlMatch = tweetId.match(/\\/status\\/(\\d+)/);\n        if (urlMatch)\n            tweetId = urlMatch[1];\n        // Cookie context auto-established by framework pre-nav (Strategy.COOKIE + domain).\n        // Read CSRF token directly from the cookie store via CDP — zero page.evaluate round-trip.\n        const cookies = await page.getCookies({ url: 'https://x.com' });\n        const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;\n        if (!ct0)\n            throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');\n        // Build auth headers in TypeScript\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        });\n        // Paginate — fetch in browser, parse in TypeScript\n        const allTweets = [];\n        const seen = new Set();\n        let cursor = null;\n        for (let i = 0; i < 5; i++) {\n            const apiUrl = buildTweetDetailUrl(tweetId, cursor);\n            // Browser-side: fetch + JSON parse with HTML-as-JSON sniffer so a\n            // login wall / WAF page surfaces as a structured LoginWallError\n            // instead of `SyntaxError: Unexpected token '<'`.\n            const data = throwIfLoginWall(await page.evaluate(`async () => {\n        ${BROWSER_JSON_SNIFF_FN}","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/thread.js#L110-L146","documentation":"thread.js:128 requires an authenticated x.com session before calling the TweetDetail GraphQL API. It reads cookies for https://x.com via CDP and looks for the ct0 cookie, which x.com sets as the CSRF token for logged-in sessions. If no ct0 cookie exists, AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)') is thrown because the authenticated GraphQL request would otherwise fail with a 403/404.","triggerScenarios":"Running `opencli twitter thread <url>` (or any command reaching thread.js:128) while the browser profile used by the CLI is logged out of x.com, the session expired and cookies were rotated/cleared, cookies exist for twitter.com but not x.com, or the framework pre-nav to x.com was blocked.","commonSituations":"Fresh CI/machine without a logged-in browser profile; x.com logged the session out (password change, security sweep); user cleared cookies; using the wrong browser profile directory; x.com domain migration leaving stale twitter.com-only cookies.","solutions":["Log into x.com in the browser profile the CLI drives (run the CLI once with a visible browser and complete the login), then retry the command","Verify the ct0 cookie exists: in DevTools on x.com, check Application > Cookies > https://x.com for a ct0 entry","Ensure the CLI's cookie/auth strategy targets the x.com domain, not twitter.com, and that cookies are being loaded into that context","Re-export or refresh the saved session/cookies if you import them from another tool, since ct0 rotates with sessions"],"exampleFix":"// before (logged-out profile)\nopencli twitter thread https://x.com/jack/status/123\n// after: login first in the driven browser profile\nawait page.goto('https://x.com/login'); // complete manual login, then rerun\nopencli twitter thread https://x.com/jack/status/123","handlingStrategy":"try-catch","validationCode":"const cookies = await page.getCookies({ url: 'https://x.com' });\nconst hasCt0 = cookies.some((c) => c.name === 'ct0' && c.value);\nif (!hasCt0) {\n  console.error('Not logged into x.com — run the CLI with a visible browser and log in first.');\n  process.exit(1);\n}","typeGuard":"function hasCt0Cookie(cookies) {\n  return Array.isArray(cookies)\n    && cookies.some((c) => c.name === 'ct0' && typeof c.value === 'string' && c.value.length > 0);\n}","tryCatchPattern":"import { AuthRequiredError } from '@jackwener/opencli/errors';\ntry {\n  await fetchThread(tweetUrl);\n} catch (e) {\n  if (e instanceof AuthRequiredError) {\n    console.error(`${e.domain} login required: ${e.message}. Re-authenticate and retry.`);\n  } else throw e;\n}","preventionTips":["Keep a dedicated, logged-in browser profile for the CLI and re-login when sessions expire","Check for the ct0 cookie as a pre-flight step in scripts before long batch runs","Avoid clearing browser data on the profile the CLI uses","Handle x.com logout events (password changes, security reviews) by re-authenticating promptly"],"tags":["authentication","cookies","csrf","twitter"],"backgroundTag":"missing-auth-cookie","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}