Deletes cookies for a single IDP domain from Chrome, Edge, Safari (macOS), and Firefox profiles. The domain must be an exact host string (no wildcards). Use ONLY after a cloud IDP password reset has been confirmed. Dry-run returns the count of cookies that would be cleared per profile without deleting anything.
/** * mcp/skills/clearBrowserSsoCookies.ts — clear_browser_sso_cookies * * Targeted cookie deletion for a single IDP domain across Chrome, Edge, * Safari, and Firefox. Scope is strictly limited to cookies whose host * matches or is a sub-domain of the supplied domain — all other site * logins are preserved. * * Platform strategy * ----------------- * Chromium (Chrome/Edge): * Cookies live in a SQLite DB. We issue: * DELETE FROM cookies WHERE host_key = '.<domain>' OR host_key = '<domain>' * OR host_key LIKE '%.<domain>' OR host_key LIKE '%.<domain>.' * No decryption required — the host_key column is plain-text. * Must be called while the browser process is closed; otherwise the * in-memory cookie jar will rewrite the SQLite file on shutdown and * lose our delete. We do NOT attempt to close the browser — the dry- * run surface reports counts so the consent gate shows the user which * browsers they need to quit first. * * Safari (macOS only): * Cookies.binarycookies is a proprietary binary format. We shell out * to AppleScript via osascript to tell Safari to delete cookies for * the IDP domain through its own API. Requires Safari to be running * or launchable. * * Firefox: * Per-profile cookies.sqlite. Same SQL pattern as Chromium. * * All SQLite ops go through `sqlite3` as a subprocess so we don't need * to bundle better-sqlite3 into the skill build. If `sqlite3` is not * on PATH, the entry is skipped with an "unavailable" error; the user's * consent card still shows every cookie that would have been cleared on * other browsers. */import { z } from "zod";import * as fs from "fs";import { execAsync, isDarwin } from "./_shared/platform";import { listCookieStores, type CookieStore, type Browser } from "./_shared/browser";// -- Meta ---------------------------------------------------------------------export const meta = { name: "clear_browser_sso_cookies", description: "Deletes cookies for a single IDP domain from Chrome, Edge, Safari " + "(macOS), and Firefox profiles. The domain must be an exact host string " + "(no wildcards). Use ONLY after a cloud IDP password reset has been " + "confirmed. Dry-run returns the count of cookies that would be cleared " + "per profile without deleting anything.", riskLevel: "medium", destructive: true, requiresConsent: true, supportsDryRun: true, affectedScope: ["user"], auditRequired: true, schema: { domain: z .string() .min(1) .refine( (v) => !/[*?]/.test(v), { message: "Wildcard domains are not permitted — supply an exact host string." }, ) .refine( (v) => /^[a-zA-Z0-9.\-]+$/.test(v), { message: "Domain must contain only letters, digits, dots and dashes." }, ) .describe("Exact host string for the IDP domain (e.g. 'okta.com')."), dryRun: z .boolean() .optional() .describe("When true, return per-profile cookie counts without deleting."), },} as const;// -- Types --------------------------------------------------------------------export interface StoreResult { browser: Browser; profile: string; path: string; /** cookies matched at dry-run time. */ matched: number; /** cookies actually deleted (0 on dry-run or failure). */ deleted: number; skipped?: boolean; error?: string;}export interface ClearCookiesResult { domain: string; dryRun: boolean; stores: StoreResult[]; totalMatched: number; totalDeleted: number; /** True when a required tool (sqlite3 on Windows) isn't on PATH. */ missingSqlite?: boolean;}// -- SQLite helpers via the sqlite3 CLI ---------------------------------------/** * Probe for the sqlite3 CLI on every run. We deliberately do NOT cache * this result — the tool runs at most once per plan step (~seconds of * work), so saving a single exec call is not worth the test-ordering * hazard a module-level cache introduces. */async function isSqlite3OnPath(): Promise<boolean> { try { await execAsync("sqlite3 -version", { timeout: 3_000 }); return true; } catch { return false; }}function buildHostPredicate(column: string, domain: string): string { const esc = domain.replace(/'/g, "''"); return ( `(${column} = '${esc}' OR ${column} = '.${esc}' ` + `OR ${column} LIKE '%.${esc}' OR ${column} LIKE '%.${esc}.')` );}async function sqliteCount(dbPath: string, query: string): Promise<number> { const { stdout } = await execAsync( `sqlite3 ${JSON.stringify(dbPath)} ${JSON.stringify(query)}`, { maxBuffer: 1 * 1024 * 1024, timeout: 5_000 }, ); const n = parseInt(stdout.trim(), 10); return isNaN(n) ? 0 : n;}async function sqliteExec(dbPath: string, query: string): Promise<void> { await execAsync( `sqlite3 ${JSON.stringify(dbPath)} ${JSON.stringify(query)}`, { maxBuffer: 1 * 1024 * 1024, timeout: 10_000 }, );}async function clearSqliteStore( store: CookieStore, domain: string, dryRun: boolean,): Promise<StoreResult> { const column = store.browser === "firefox" ? "host" : "host_key"; const predicate = buildHostPredicate(column, domain); const countSql = `SELECT COUNT(*) FROM cookies WHERE ${predicate};`; const deleteSql = `DELETE FROM cookies WHERE ${predicate};`; try { if (!fs.existsSync(store.path)) { return { browser: store.browser, profile: store.profile, path: store.path, matched: 0, deleted: 0, skipped: true, error: "Cookies database no longer exists", }; } const matched = await sqliteCount(store.path, countSql); if (dryRun || matched === 0) { return { browser: store.browser, profile: store.profile, path: store.path, matched, deleted: 0, }; } await sqliteExec(store.path, deleteSql); return { browser: store.browser, profile: store.profile, path: store.path, matched, deleted: matched, }; } catch (err) { const msg = (err as Error).message; return { browser: store.browser, profile: store.profile, path: store.path, matched: 0, deleted: 0, error: `sqlite error: ${msg}`, }; }}// -- Safari via osascript -----------------------------------------------------async function clearSafariCookies(domain: string, dryRun: boolean): Promise<StoreResult> { const safariStore: CookieStore = { browser: "safari", path: "~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies", profile: "Default", format: "binary", }; if (!isDarwin()) { return { ...safariStore, matched: 0, deleted: 0, skipped: true, error: "Safari cookie clearing is macOS-only", }; } if (dryRun) { // Safari's binary format is awkward to count without parsing it; return 0 // and note the limitation so the consent card can surface it. return { ...safariStore, matched: 0, deleted: 0, skipped: true, error: "Safari cookie count is unavailable in dry-run — will be cleared via " + "Safari API when the user confirms.", }; } // Use osascript + Safari to clear cookies for the domain. Newer macOS // versions expose "remove cookies with name / domain" via AppleScript. // If Safari refuses, report the failure rather than throwing. const script = `tell application "Safari"\n` + ` try\n` + ` set cookiesList to (cookies whose domain is "${domain}")\n` + ` set theCount to count of cookiesList\n` + ` repeat with c in cookiesList\n` + ` delete c\n` + ` end repeat\n` + ` return theCount\n` + ` on error errMsg\n` + ` return "error: " & errMsg\n` + ` end try\n` + `end tell`; try { const { stdout } = await execAsync( `osascript -e ${JSON.stringify(script)}`, { maxBuffer: 1 * 1024 * 1024, timeout: 10_000 }, ); const out = stdout.trim(); if (out.startsWith("error:")) { return { ...safariStore, matched: 0, deleted: 0, error: out, }; } const n = parseInt(out, 10); if (isNaN(n)) { return { ...safariStore, matched: 0, deleted: 0, error: `osascript returned non-numeric output: ${out}`, }; } return { ...safariStore, matched: n, deleted: n }; } catch (err) { return { ...safariStore, matched: 0, deleted: 0, error: `osascript failed: ${(err as Error).message}`, }; }}// Exported for unit tests.export const __testing = { clearSqliteStore, clearSafariCookies, isSqlite3OnPath };// -- Exported run function ----------------------------------------------------export async function run({ domain, dryRun = false,}: { domain: string; dryRun?: boolean;}): Promise<ClearCookiesResult> { const stores = listCookieStores(); const sqliteOk = await isSqlite3OnPath(); const out: StoreResult[] = []; let totalMatched = 0; let totalDeleted = 0; for (const s of stores) { if (s.format === "sqlite") { if (!sqliteOk) { out.push({ browser: s.browser, profile: s.profile, path: s.path, matched: 0, deleted: 0, skipped: true, error: "sqlite3 command-line tool is not available on PATH — cannot " + "clear cookies for SQLite-backed browsers.", }); continue; } const r = await clearSqliteStore(s, domain, dryRun); totalMatched += r.matched; totalDeleted += r.deleted; out.push(r); } else { const r = await clearSafariCookies(domain, dryRun); totalMatched += r.matched; totalDeleted += r.deleted; out.push(r); } } return { domain, dryRun, stores: out, totalMatched, totalDeleted, missingSqlite: sqliteOk ? undefined : true, };}// -- CLI smoke test -----------------------------------------------------------if (false) { run({ domain: "okta.com", dryRun: true }) .then((r) => console.log(JSON.stringify(r, null, 2))) .catch((err: Error) => { console.error(err.message); process.exit(1); });}