One .zip per skill. Inside: SKILL.md with the intent and the frontmatter, and the code that runs in the page.
Settings → Personalize → Skills. This is Cowork's own skill library, shared across every chat.
Click Add and choose the file you just downloaded. Cowork installs it once; no CLAUDE.md, no per-project setup.
The skill stays available in every session. Cowork runs it in your own Chrome, riding the session you are already signed into.
Return the handle and recent tweets of the LOGGED-IN account on X, without asking for the handle.
{"count":"int (optional, default 20)"}{"handle":"string","userId":"string","count":"int","tweets":"array of { id, text, url, time, likes, retweets, replies, isRetweet }"}Reads your own timeline of posts, resolves the account from the session, so no handle is needed. Resolves in a single bridge call against X's internal GraphQL API, no page is opened.
The extractor lives as text in the site's localStorage (key x-my-tweets) and is rebuilt on each call with synchronous eval. It is stored once and then persists across navigation and browser restarts, so on every run except the first it is already there.
Default action: just call (step 1). The call itself reports NEEDS_STORE when the extractor is missing or out of date, only then do you store it (step 2) and call again.
Versioning. The stored extractor is gated by VER (must match the version in the frontmatter above). Both snippets carry VER = "1", keep them in sync with the frontmatter. When evaluate.js changes, bump version and VER in both snippets: the next call sees the mismatch, returns NEEDS_STORE, and re-stores automatically. Storage stays at two keys (x-my-tweets and x-my-tweets:ver), overwritten each store, versions never accumulate.
await (() => {
const KEY = "x-my-tweets", VER = "1";
const s = localStorage.getItem(KEY);
if (!s || localStorage.getItem(KEY + ":ver") !== VER) return 'NEEDS_STORE';
const fn = eval('(' + s + ')');
return fn(document, { count: 20 });
})()
Returns the logged-in account's recent tweets. If it returns the string 'NEEDS_STORE', the extractor is missing or stale in this browser → do step 2 once, then run this exact call again.
Run this snippet verbatim. Never add an await before the eval, only the outer await is allowed.
Any logged-in x.com page works: the extractor reads the session cookies and the page's own script tags, then calls the API itself. It does not read the page's content, so it does not matter which x.com page is open.
NEEDS_STORE; persists until localStorage is cleared)From a logged-in x.com page, evaluate the following with the bridge, replacing <FN> with the contents of evaluate.js (the ( async (root, params) => { ... } ) function). Keep VER equal to the call snippet's:
(() => {
const KEY = "x-my-tweets", VER = "1";
const fn = (<FN>);
localStorage.setItem(KEY, String(fn));
localStorage.setItem(KEY + ":ver", VER);
return 'stored';
})()
Returns "stored". The extractor passes through the model only here, once. Then go back to step 1.
UserTweets allows only 50 requests per 15 minutes (verified: x-rate-limit-limit: 50), the tightest of the read skills. The response is also large (~200 KB for 20 tweets), so ask for what you need and no more.
NEEDS_STORE → extractor missing or version-mismatched in this browser; run step 2 once, then retry step 1.could not read twid/userId → not logged in.no twid cookie / empty ct0 → not logged in to X in this browser; log in once and retry.{ "type": "json", "jsonPath": "count" }