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.
React to a LinkedIn post (by its urn:li:activity or permalink url) from the logged-in account, with the given reaction type (default: Celebrate).
{"urn":"string (urn:li:activity:... of the post, or its full permalink url)","reaction":"string (friendly: celebrate/like/support/love/insightful/funny, or the API code: PRAISE/LIKE/APPRECIATION/EMPATHY/INTEREST/ENTERTAINMENT. Default celebrate)","threadUrn":"string optional (force the thread and skip resolution)"}{"reacted":"bool","status":"int (HTTP of the create, 200 = ok)","threadUrn":"string (thread that was reacted on)","resourceKey":"string (urn of the fsd_reaction created)","reactionType":"string (API code applied)"}Reacts to a post. It resolves the post's threadUrn by fetching the permalink same-origin, so it runs from any LinkedIn page with no navigate first. Resolves in a single bridge call. The extractor lives as text in the site's localStorage (key linkedin-react) 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.
WRITE. Every successful call performs a real action on the logged-in account. It is reversible (it can be removed), but it is public the moment it lands.
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 (linkedin-react and linkedin-react:ver), overwritten each store, versions never accumulate.
await (() => {
const KEY = "linkedin-react", VER = "1";
const s = localStorage.getItem(KEY);
if (!s || localStorage.getItem(KEY + ":ver") !== VER) return 'NEEDS_STORE';
const fn = eval('(' + s + ')');
return fn(document, { urn: "urn:li:activity:7280000000000000000", reaction: "celebrate" });
})()
Returns reacted: true and the reaction that was applied. 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; a pre-eval await trips LinkedIn's CSP and the call fails.
NEEDS_STORE; persists until localStorage is cleared)While logged in at https://www.linkedin.com/feed/ (the JSESSIONID cookie must be present), 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 = "linkedin-react", 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.
Idempotent: it best-effort DELETEs any previous reaction before creating the new one, so calling it again just changes the reaction.
The two GraphQL queryId hashes are pinned in evaluate.js and rotate on every LinkedIn deploy. When they do, the call returns HTTP 400/404 → recapture both hashes from the reaction button and bump version/VER.
NEEDS_STORE → extractor missing or version-mismatched in this browser; run step 2 once, then retry step 1.not-applicable: unknown reaction type → the reaction value is not one of the accepted names/codes.queryId hashes rotated; recapture (see Notes).re-auth / no JSESSIONID → no LinkedIn session in this browser; log in once.tool-broken → LinkedIn changed the API shape; recapture the extractor.{ "type": "json", "jsonPath": "reacted" }