Skip to content

Repository files navigation

Kick Focus

Version License Platform Dependencies

Kick Focus is a desktop-only userscript that makes Kick calmer and easier to control. It adds a consistent graphite-and-lime shell, compact discovery, focus and theater modes, accessibility controls, a complete settings center, content filters, and document-start ad defense without shipping remote code.

An optional Manifest V3 companion extension adds the one thing a userscript on Chromium can no longer do for itself: blocking ad requests at the browser network layer, before they are sent.

Kick Focus premium Home direction

What it changes

  • Restyles Kick's current semantic desktop shell with clearer type, tighter spacing, quieter borders, rectangular status labels, and text-first route tabs. Stream and category cards use imagery and whitespace instead of boxed perimeters.
  • Reclaims the permanent discovery rail with Kick, Compact, Auto-hide, and Hidden modes. Auto-hide is the default. It leaves a 12-pixel edge target and opens the full rail on hover or keyboard focus.
  • Keeps every live channel you follow in the sidebar. Kick's five-channel preview expands automatically, stays open through page changes, and continues paging when the site returns the list in batches. Hover and keyboard focus stay on the channel row without opening a popup.

Every live followed channel visible in the sidebar

  • Adds Standard, Theater, and Focus stream layouts, plus Right, Left, Auto-hide, Docked, and Hidden chat. Auto-hide overlays chat from a 12-pixel right edge without shrinking the player. Fixed and revealed chat can be set from 280 to 520 px.
  • Widens browse grids, trims the channel information row, and offers a 48-pixel header. Essential header actions keep Focus and Menu visible while Multi-stream stays inside Menu. Following and Drops are classified as first-class routes instead of being mistaken for channels.

Compact channel layout with both rails tucked away

Layout controls for rails, chat, density, and header

  • Treats Profile/Settings, Collectibles, and Subscriptions as first-class signed-in routes too: account tabs gain a clear selected state, form focus is stronger, disabled actions read as disabled, long explanatory copy becomes easier to scan, and collectible tiles get consistent hover/focus feedback without hiding or replacing Kick's controls.
  • Adds a native-sized Stats button beside Follow on every channel profile. It opens that exact channel at StreamerStats in a centered compact popup, reuses the popup while you browse profiles, and offers a normal-tab fallback if the browser blocks it. StreamerStats forbids iframe embedding, so nothing is framed inside Kick and no extra extension permission is needed.

Kick Focus Stats action on a channel profile

  • Adds a searchable command menu, reached from a button in Kick's own header, for the session toggles that have no other home.
  • Adds three complete surface systems. Studio uses layered graphite with a quiet green undertone, OLED is true black with minimal lift, and Slate uses cool blue graphite with stronger separation. Theme boards preview the actual hierarchy before you choose one. Four ready-made viewing directions and five accent choices sit beside the same density, radius, thumbnail, contrast, and scale controls.

Studio, OLED, and Slate appearance boards

  • Keeps home-page previews silent, blurs marked mature cards, and can filter casino, Drops, sponsored, and promoted content using Kick's own category and badge markup. Filtering suspends itself and says so rather than emptying a page.
  • Shows elapsed live time beside Kick's own LIVE marker on discovery cards when the page has already supplied a valid start time. It reuses Kick's discovery response, makes no second request, and leaves the card alone when the time is unknown.
  • Adds an opt-in Poor mode that removes Subscribe, Gift Subs/Dubs, Get KICKs, gift-shop controls, and spend-based leaderboards while preserving Follow, chat, and free daily rewards. It identifies exact controls instead of hiding arbitrary text. It also reaches the two spend surfaces that are not controls, the KICKs balance in the chat footer and the gift-shop panel, by test id, which is why the free channel-points counter sitting directly beside the balance is left alone.
  • Says what your account can actually send, and where. Kick's emote endpoint answers differently depending on who asks: read with your own session it returns every channel you subscribe to and the collectibles you have pulled, not just the channel you are looking at. That answer is used as the entitlement it is, so an emote you own reads as available and one you do not is marked as such rather than left unconfirmed. Reach is shown separately from ownership, because they are different facts, a free channel emote works only in that channel, while a subscriber emote you own works in every chat. Kick's own picker states neither, and only ever shows the channel you are standing in.
  • My Emotes turns that account answer into a real collection. One control opens every emote Kick says the signed-in account can use anywhere, grouped by subscribed source channel or global collectible set. Search, favorites, local custom groups, Copy name, Type in chat, access labels, and artwork actions keep working inside the collection. Signed out or before a channel has supplied the catalog, the view explains exactly how to load it instead of displaying a false zero.
  • Shows how long the stream has been live. Kick sends the start time with every channel and displays it nowhere. The clock costs no extra request, and falls back to the start time in Kick's own structured data in the page, so it survives the channel API rate-limiting your tab.
  • Remembers volume, mute state, quality, and finite VOD position locally with separate privacy toggles; adds favorite and not-interested card actions, configurable Following/Recommended rails, an accessible search summary and clear action, useful Drops-empty guidance, and mini-player collision recovery.
  • Switches off Kick's own controls you never use. A grid on the Layout page turns off eight player controls, miniplayer, clip, theater, fullscreen, the quality gear, volume, share, report, and six sidebar entries: the Home, Browse, Following and Drops links, and the followed and recommended channel lists. Each is hidden with styling only: the control stays in the page with everything Kick wired to it intact, and switching it back on restores it without a reload. They are located through the same ordered probe list the rest of the mod uses, so a Kick rename is reported by the live gate instead of quietly hiding nothing.
  • Always start at the highest quality (off by default). Opens every stream at the best rung Kick offers on that channel, taking precedence over remembered quality. The rungs are learned from Kick's own quality menu rather than hard-coded, because the set differs per channel, so it does nothing until that menu has been opened once, and it will not open the menu for you. A rung Kick has badged as unavailable to your session is never recorded and never selected; signed out, that is the 1080p60 row, and the best rung becomes 720p60.
  • Chat can sit on either side of the player, reveal from the right edge, float as a dock, or stay hidden. Its separator remains adjustable from 280 to 520 px in both directions, including Theater mode.
  • Adds a chat pause that arms when you scroll the transcript up and holds the message you were reading in place while Kick recycles the transcript, plus per-channel keyword highlights and private notes, optional playback diagnostics, and a panic switch that restores Kick's native page without a reload.
  • Optional composer recall keeps this tab's last five public sends in memory. A visible Recall button beside the message box cycles them, stays disabled until there is something to recover, and never writes the ring to storage.
  • Continuously records emotes seen in live chat and every emote Kick exposes in the open picker, including locked metadata. Click an emote in chat to save it. If Kick explicitly marks it as follow-gated, the same click follows its source channel and offers Undo. Subscriber access is never bypassed. The hover card explains the set and access, when it was first captured, whether you own it, plus any shadowed name.
  • Emotes have their own full library. Search now matches names, sources, and custom groups. Filters sit beside a permanent group panel, while multi-select, direct group assignment, Remove, and a real Removed view keep large collections manageable. Removed entries retain their artwork and history until you restore them. Public artwork from a named channel can still be added without treating it as account access.

Batch emote grouping in the dedicated library

  • The chat player's picker is now a stable organizer. Kick's search stays at the top. Favorites, Recent, All, Removed, custom groups, and Kick's own groups sit directly below it. Manage opens a fixed detail tray inside the picker, so actions no longer jump outside the rail or cover the artwork. You can favorite an emote, assign it to a group, or remove it there. Groups can be created, renamed, reordered, and deleted without leaving chat. Dragging reorders All, Favorites, or a custom group, while Earlier and Later remain available in Organize mode. A normal emote click inserts its plain name and never sends it. Back to chat closes the library and restores focus to the composer.

  • Compact still fits eight 40-pixel tiles across a 340-pixel chat rail. Balanced and Roomy offer more artwork space, and shelf height is independent. Collectible rarity stays inside the tile. Expired subscriber emotes leave active views but remain recoverable in Locked with a source link. Only the visible slice of a large library enters the page, so the 2,400-emote limit stays responsive.

In-player emote organizer

  • A new chat emote dock sits beside the message box. Compact and Standard modes replace Kick's fragile quick strip with saved emotes from Mixed, Favorites, or Recent. Choose 4, 6, 8, 10, or every matching emote. Clicking one inserts its plain name at the caret and stops there. The folder control opens the full library, then returns focus to the draft when you come back.

Chat emote dock beside the composer

Chat emote dock and picker settings

  • Emote suggestions as you type (off by default). A colon and two or more letters offers matching emotes from your library, names that start with what you typed ahead of names that merely contain it, then your favourites, then what you actually send in that channel. Click one and its plain name goes in at your cursor. Suggestions are accepted by click only: nothing here listens for a keystroke, so it cannot take a key meant for Kick's own composer, and it never sends the message.
  • Clears the ad flags out of Kick's /playback response before the player reads them, so the ad SDKs are never started, and blocks known ad and optional telemetry requests through early page-realm fetch, XHR, beacon, and dynamic-element hooks. A persistent observer removes ad scripts, frames, and containers after reinsertion, and the Content & Ads page warns if Kick's ad stack stops matching what this build knows.
  • Reads Kick's own API instead of scraping the page for it, read-only and same-origin using the session you are already signed into. Public emote catalogs are treated as artwork catalogs, not proof of account entitlement; chat events come from whichever realtime provider Kick's own broker names; removed messages say why they were removed, which the page itself discards; emote usage is counted per channel and globally, which Kick does not do at all; collectible rarity is resolved and shown only where the match is confident; wide collectibles render un-squashed; and emote names shadowed across your sets are reported. Every one of these degrades to the previous DOM behaviour if the response changes shape, and each has its own switch.
  • A Viewer page gathers account readings and a local watch clock. Daily reward, channel points, collectibles, Drops, level and streak each name their source and how old the reading is. Session watch time is separate, labelled “This browser session only,” counts visible active playback, and resets on reload. A card that could not read its value says so instead of showing a false zero. Nothing is requested while the page is closed.
  • Saved views for discovery pages. A named snapshot of density, thumbnail size, rails and content filters, optionally applied when you open Home, Browse, a category, Following or Search. Local, editable, and limited to settings this build already has. It does not change what Kick recommends.
  • Five chat comfort switches, off until you ask for them. Message times (Kick's own, revealed rather than re-created), people worth noticing, a short synthesised tone on a mention, hiding a single message for yourself, and a search over what this session has seen. Each works without the others.
  • The session chat search keeps 400 messages, 200 KB, or one hour, whichever ends first. In memory only, so a reload clears it. No whispers. A message a moderator deletes leaves the log as soon as the deletion arrives, and the log leaves your machine only through the button that saves it as a file.
  • Says when a daily reward is waiting, once. The Focus button and the Viewer tab carry a dot and a short line of text, and the same words go into the button's accessible name so the status never depends on colour or on the dot being seen. Reduced Motion stops the pulse. Signed out there is no marker, because there is no reward state to report.
  • Claims Kick's daily reward for you (off by default). When one is waiting, it opens only the dialog controlled by Kick's reward button, waits for the dialog to finish mounting, and clicks an enabled Claim once. It records success only after Kick changes the action to Share and prints the next reset, then closes the dialog and gives focus back. It never touches the private claim endpoint. A disabled Claim is left alone, and a reward claimed manually updates the schedule without being counted as an automatic claim. The dialog's own watch-time and reset text sets the next look, shared across every open tab.

Daily reward ready, opening, roulette, and confirmed states

  • Multi-stream: up to nine channels in one viewing board, built on Kick's own embedded player and popout chat. The focused tile owns audio and chat. The empty board now starts with one clear action, the toolbar reflows cleanly at narrower desktop widths, and saved boards stay grouped in a compact footer. Reachable from the header control, the command menu, or settings. Every stream card on Home, Browse, Following, and Search carries a chip that adds it without opening the channel first. Adds and removes converge across tabs as they happen. Merged chat reports one live count, reconnects a closed or silent channel through a two-slot queue, and cancels work for a removed tile. A shared ?kf-multi= link says what it replaced and offers Undo.

Kick Focus multi-stream viewing board

  • States the channel-points limit before it matters. The multi-stream footer and popout-chat control repeat Kick's current guidance that Picture-in-Picture and mirrored viewing do not accrue channel points, so a detached viewing setup cannot quietly cost progress.
  • Starts playback without waiting for blocked ad preflight scripts. Kick waits on Google PAL, Datazoom, and OM before requesting playback, so blocking them, which this build does, otherwise leaves the player sitting out the full timeout.
  • Can freeze animated emotes and collectibles to a static frame, applied automatically when your system asks for reduced motion.
  • Stores settings locally in the userscript manager, and the emote library in IndexedDB, which holds orders of magnitude more than the ~5MB localStorage ceiling a growing library eventually reaches. A small synchronous copy is kept where the page can read it before the first render, so startup is unchanged, and a browser that refuses IndexedDB (a private window, a locked-down profile) keeps working on that copy alone. A full reset keeps emote provenance and the local reward-check record; the latter stops reset from reopening a reward this browser already handled. There is no analytics, network update code, @require, or remote executable code. An optional, off-by-default subscription accepts only user-supplied JSON data containing channels, categories, and keywords.
  • Keeps settings controls, save states, import and storage errors, and Viewer source notes readable in English, Spanish, or Portuguese.

Install

  1. Install a current desktop userscript manager such as Tampermonkey or Violentmonkey.
  2. Open dist/kick-focus.user.js in the manager, or create a new userscript and paste that file into the editor.
  3. Save it, ensure it is enabled for https://kick.com/*, and reload Kick.
  4. Press the Focus button in Kick's header to open settings, or Menu beside it for the command menu. A userscript manager also lists both under its own menu.

The script is not published or auto-updated. dist/kick-focus.user.js is the canonical install artifact in this repository.

On Chromium 138 and later, a userscript manager also needs its Allow user scripts toggle enabled on its own entry in chrome://extensions. Without it the manager silently runs nothing.

Install the companion extension (optional)

The companion is unsigned and installs unpacked. It is not published to any store.

  1. Run npm run build to produce dist/extension/.
  2. Open chrome://extensions, enable Developer mode, choose Load unpacked, and select dist/extension/.
  3. Reload Kick. The Content & Ads settings page now reports Network + page instead of Page only.

If you configure a remote blocklist feed, open the companion popup and choose Approve this feed. The browser asks for access to that feed's origin only. Approval is tied to the full HTTPS URL, so changing the path or host requires another click.

The popup and browser metadata follow the selected Kick Focus language in English, Spanish, or Portuguese. A saved Portuguese setting is exposed to the browser as pt-BR.

dist/kick-focus-extension-v<version>.zip is the same package for sharing or for browsers that accept a zip.

Firefox companion

The build also emits dist/extension-firefox/ and dist/kick-focus-firefox-v<version>.zip. Firefox users can open about:debugging#/runtime/this-firefox, choose Load Temporary Add-on, and select dist/extension-firefox/manifest.json. This unsigned Manifest V2 package injects the same page bundle through a local web-accessible bridge and blocks the same Kick-initiated hosts.

The Firefox package declares its page bundle as a world: "MAIN" content script, so Firefox injects it into the page's own realm. Nothing about the extension reaches the page: no moz-extension:// URL, which carries a per-install UUID that is stable for the life of the install and would work as an identifier surviving a cookie clear, and no inline script, which would have stopped loading the day Kick shipped a script-src without 'unsafe-inline'. That is why the package needs Firefox 128 or newer. It was still Manifest V2 the last time this was checked, verified in Firefox 153.0.3 on 2026-08-26.

Firefox channel limitations: Temporary add-ons loaded through about:debugging are removed on restart. Firefox Release and Beta cannot install unsigned XPIs persistently at all. For a persistent unsigned install, use Firefox Developer Edition, Nightly, or ESR with xpinstall.signatures.required set to false in about:config. This is a Mozilla policy, not a limitation of this project.

Kick Focus companion popup

The companion is self-contained: it carries the same page-world script as the userscript, so install one or the other, not both. If both are present the first to run claims the page and the second stands down, but only the extension gives you the network layer.

Userscript Companion extension
Layout, settings, filters, accessibility Yes Yes
Page-realm ad interception Yes Yes
Ad requests blocked before they are sent No Yes (declarativeNetRequest)
Guaranteed document-start timing Manager-dependent Yes
Install effort Paste one file Load unpacked, survives as a folder

Every network rule is scoped to kick.com initiators, so the companion never changes how any other site loads. Required Chromium host access remains limited to Kick. Firefox needs webRequest access to the enumerated hosts it blocks and still filters by Kick initiator. Both packages declare optional HTTPS access so the popup can request one user-chosen feed origin, but that access is not granted at install time. Both packages contain no remote code.

Keyboard

Kick Focus doesn't claim a page-wide shortcut. Focus, pause, settings, and every other action remain available from visible controls and the command menu in Kick's header.

Earlier versions captured six configurable chords plus a fixed pause chord. Stored custom shortcuts are discarded on load without an error.

Inside Kick Focus's own surfaces the standard keys work as standard keys: Tab and Shift+Tab move within whichever panel is on top, Escape closes it, and the chat separator answers Left, Right, Home and End once you tab to it.

Ad-defense boundary

Kick Focus is deliberately honest about the userscript boundary:

  • @run-at document-start starts as early as a userscript manager supports, but another page script can still run first.

  • On Chromium Manifest V3, current Tampermonkey versions no longer expose the experimental pre-script @webRequest path, and chrome.userScripts injection can land after the page's own first scripts. The page-realm hooks are written to be idempotent so they still install when they lose that race.

  • Violentmonkey 2.47.0+ (the first MV3 release, 2026-08-06) does not provide real document-start injection under MV3 Chromium unless Alternative page mode is enabled in its extension settings. That mode is off by default and limited to approximately 1 MB of injected script. The About page measures actual injection timing and reports it.

  • The userscript alone therefore blocks requests it can separate in the page realm and continuously removes ad DOM. It does not claim browser-network control over parser requests that occur first, worker-only requests, or server-side stitched media.

  • The companion extension closes the network gap. Its declarativeNetRequest ruleset refuses the known ad hosts in the browser network stack, which was verified by observing ERR_BLOCKED_BY_CLIENT on a Kick-initiated request to securepubads.g.doubleclick.net (npm run verify:extension).

  • Ads are disabled at their source in the playback response. Kick gates client-side ad behaviour on flags it sends with each stream; those are rewritten in flight, so the ad SDKs never initialise.

  • Server-side stitched ads remain unverified and out of the current page-layer reach. Measured on 2026-08-14: the HLS manifest is fetched inside the IVS WASM worker and never appears in page-realm traffic, so the existing page interceptor cannot inspect it. Safe worker-level instrumentation remains in ROADMAP.md; this project does not claim to remove media bytes it cannot observe.

  • The Firefox companion really does block, as of 1.5.0. Before that its listener gated on a Chromium-only field, so it cancelled nothing while reporting active rulesets.

This boundary is reflected directly in the Content & Ads settings page and protection log, which report Network + page or Page only depending on what is actually installed.

Known limitations

  • Multi-stream chat is read-only. Kick's popout chat refuses to send from inside an iframe, it answers with a CSRF error by design (KickDevDocs #262). The grid says so in the panel rather than letting you find out by typing. Kick Focus deliberately does not work around this or attempt to send from an embedded chat.
  • If Kick sign-in, sign-up, or Follow stops working, check your ad blocker, not this extension. Since Kick began serving ads on 2026-08-06, ad-blocker filter lists have been reported to break those actions until the blocker is disabled and the browser restarted. Kick Focus blocks eleven third-party ad and telemetry hosts and no kick.com host at all, so pausing it will not change that behaviour.

What this project reads from Kick

Since 1.5.0 Kick Focus calls Kick's own endpoints rather than only scraping the rendered page. The rules it holds to:

  • Read-only by default. The only account-changing request is the normal Follow action after you deliberately click a chat emote that Kick explicitly marks as follow-gated. It never follows from background discovery, public artwork, or a guessed source, and Undo reverses a follow created by that click.
  • One account-changing action is not a request at all. The opt-in daily-reward claim presses Kick's own button in Kick's own dialog; the claim call is made by Kick's bundle, not by this project. That is deliberate, replaying the endpoint would be exactly the private-endpoint replay the rules above forbid, and it is also what bounds the feature: a reward that has not earned enough watch time shows a disabled button, and that refusal is obeyed rather than worked around, so it can never claim something the account has not earned.
  • Same-origin, with your own session. Requests inherit the cookies the page already has. Nothing handles, stores, or transmits a credential, and nothing is sent anywhere but Kick.
  • Only actions Kick's own site already performs. No wire-token injection, chat auto-send, subscription automation, or entitlement bypass.
  • Local only. Emote usage counts, the library, and multi-stream layouts stay on your machine and travel only through the existing JSON export.
  • Fails back, never fails open. Every response is validated; an unexpected shape falls back to the DOM path and says so in diagnostics rather than showing an empty surface as success.

Multi-stream embeds Kick's own player and popout chat, so playback, subscriptions, and entitlements remain entirely Kick's.

Realtime transport

Chat events arrive over whichever realtime provider Kick's own broker names, so no connection key is written in this source. Two providers are registered: the hosted Pusher path, which this project has run against, and Kick's own gateway (websockets.kick.com), which it has not. They share one wire protocol, the same subscribe frames and event payloads, so only the handshake differs, and adding a third is one registry entry rather than a rewrite.

When the broker offers both, the verified one is used. If it ever offers only the gateway, Kick Focus attempts it and reports the transport as unverified; if that connection fails it degrades to reading the page and says so rather than retrying a path it cannot vouch for.

One caveat worth stating plainly, because it is widely reported the other way round: a cross-origin WebSocket is not blocked by CORS. The handshake carries an Origin header and the server decides; there is no preflight and no Access-Control-Allow-Origin requirement. What can actually block the gateway from a page context is the server rejecting the origin, or its Cloudflare front requiring a token the page has not been issued. Which of those applies is untested here, so the userscript build's ability to follow a forced migration remains unproven.

Distribution and listing posture

Nothing is listed anywhere today, and publishing to any catalogue or store needs explicit approval. This section is written down now rather than during a review, because every answer below is a property of the code as it already stands.

Single purpose. Kick Focus has one: make watching kick.com on a desktop browser better for the viewer. Layout, accessibility, content filtering, the emote library, the multi-stream grid and the ad defense are all features of that one purpose applied to one site, they are not separable products bundled together, and none of them works anywhere but kick.com. Neither companion gets broad host access at install time, and neither requests <all_urls> or tabs. The Chromium package's required host permissions and both content scripts are kick.com and www.kick.com and nothing else. Firefox additionally names the ten ad and telemetry hosts it can cancel, because Manifest V2 blocking webRequest requires a host permission for each host it refuses. Those ten are enumerated rather than wildcarded. The eleventh host the page realm filters is never cancelled, so Firefox asks for no permission covering it. Both manifests declare HTTPS origins as optional so a deliberate popup click can authorize the one blocklist origin the user configured. The full approved URL is kept in extension storage, and the page cannot substitute another address.

What is collected and transmitted: nothing in the background. There is no analytics, telemetry, error reporting, remote logging, or account anywhere but Kick's own. Settings, the emote library, usage counts and grid layouts are stored on the machine and leave it only through the export file you ask for. Runtime data reads go only to kick.com and web.kick.com, carrying the session the page already has, see What this project reads from Kick. The one explicit third-party handoff is the channel-profile Stats button: pressing it navigates a popup to streamerstats.com/kick/channels/{public-channel-slug}. No request happens before that click, and no account token, Kick session, settings, or library data is sent by Kick Focus. Once opened, StreamerStats is a separate site governed by its own privacy policy. The Firefox package declares data_collection_permissions: { required: ["none"] } in its manifest, which is Mozilla's machine-readable form of the extension's no-collection statement; Chrome has no manifest equivalent, so the same disclosure there is a listing-form answer rather than something the artifact can carry.

No remote code, ever. Every artifact is self-contained and readable. The build concatenates src/, strips comments and code indentation, and leaves identifiers and statement boundaries intact. There is no obfuscator, bundler, runtime package, or build dependency to audit. Nothing is eval'd, no script element points at a remote URL, and the opt-in blocklist subscription treats its response strictly as data. This meets Mozilla's no-remote-code rule and Greasy Fork's no-obfuscation rule because of how the project is built.

Why @connect *, and why it cannot currently be narrowed. It exists for exactly one feature: the opt-in remote blocklist subscription, which fetches a filter list over GM_xmlhttpRequest. There is no shipped default host to narrow to, blocklistUrl defaults to empty and the subscription is off by default, so the destination is whatever HTTPS URL you type into the setting. A wildcard is what makes a user-chosen host reachable at all. normalizeBlocklistUrl refuses anything that is not a well-formed https: URL, so javascript:, data: and plain http: are rejected before the value ever reaches a transport. Dropping @connect entirely and letting the manager prompt per-host at runtime would tighten this further, but manager-prompt behaviour differs between Tampermonkey and Violentmonkey and cannot be verified here (no userscript manager is installed; see the cold-start matrix in the blocked items). Changing it untested could silently break the feature, which is worse than a documented wildcard.

Where each artifact could be listed.

Artifact Channel Standing
kick-focus.user.js Greasy Fork Meets the code rules as built: no minification or obfuscation, one update check per day at most (blocklistRefreshHours defaults to 24), and comfortably inside the 2 MB cap, the size gate holds it under 1 MB for Violentmonkey's MV3 injection ceiling. Needs the update channel decided first (blocked, operator sign-off).
kick-focus-extension-*.zip Chrome Web Store Single purpose as argued above; the tightened Limited Use and Disclosure rules that took effect 2026-08-01 are satisfied trivially, because no user data is handled at all. declarativeNetRequest is used with block actions only and no feedback permission in the release manifest.
kick-focus-firefox-*.zip addons.mozilla.org No remote code; userScripts is not requested (that API is restricted to script managers); no broad host permissions. Signing is what currently blocks permanent installation, not policy.

Credits

Desktop support

  • Primary verified viewport: 1440×900
  • Secondary verified viewport: 1920×1080
  • Authenticated recon routes: Home, Browse, Categories, Category, Following, Drops, Search, Channel/chat, native emote picker, Daily Reward, account menu, Profile, Preferences, and Notifications (re-captured 2026-08-19). The eight that need a session are tracked in the signed-in journey matrix described below.
  • Isolated companion proof: Chromium 151, logged out, headed and off-screen, 106/106 live checks pass with 11 skips at both 1440×900 and 1920×1080 (2026-09-05, against this exact build). The matrix loads a public emote catalog into the real organizer, inserts through the dock and completion list with zero sends, and renders a 900-emote stress library through a 240-tile window. It also covers the Theater split, a real separator drag, every route contract, a 300-message chat burst, Content-Security-Policy, held chat scroll, narrow settings, and contrast across both themes and the companion popup. Checks whose subject Kick did not render are reported as skips that name what was missing and are counted apart from the total.
  • Firefox companion proof: Firefox 153.0.3, logged out and headless, the Manifest V2 package is installed as a temporary add-on over WebDriver BiDi and asserted against live Kick by npm run verify:firefox. The current run passes 8/8 with one documented popup-navigation skip (2026-08-27, against this exact build).
  • The automated live gate runs logged out, so it needs no credentials and cannot touch an account. Everything Kick renders only for a session is listed in a matrix the gate reads: account menu, Daily Reward, Profile, Preferences, Notifications, Drops, Collectibles, and the authenticated emote catalog. With no session the run prints one skip per journey saying which selectors a signed-in run would assert and why the route needs an account, so a release states what it covered instead of implying it covered everything. Point the gate at a profile that is already signed in with KF_USER_DATA_DIR=/path/to/profile and the skips become assertions. Every one of those checks is read-only: it counts selector matches and reads no display name, balance, notification text, or chat. The build's only account writes are the follow request behind the click-to-save emote gesture and the unfollow that undoes it, and npm run check fails if a third one ever appears.
  • The Kick site remains desktop-focused; the settings UI is also checked at 375×812 so narrow windows do not clip controls or hide the active section.

Kick changes frequently. The most brittle hooks are the sidebar and chat selectors documented in RESEARCH.md. If the player or chat fails, disable Kick Focus first; Kick’s own help center notes that ad/privacy/script blockers can interfere with playback and chat.

What it cannot do

  • It cannot remove Kick's in-stream video ads. Kick serves those through server-side ad insertion (SSAI): they are stitched into the video manifest itself, parsed inside an opaque Amazon IVS WASM worker the page world cannot reach, and the ad opt-out lives in a server-signed playback token. Kick Focus blocks the separable ad stack (display and tracking hosts) at the network and page layers and never touches playback. Kick's own help currently disagrees with itself on whether a channel subscription skips those ads (the ads articles say subscribers will not see them on subscribed channels; the subscriptions article dated 2026-07-16 says subscribing does not remove ads). This build cannot observe SSAI, so it cannot verify skip, and it never promises in-stream ad removal.
  • Installation is manual and unsigned. Chromium rejects self-hosted .crx files on Windows and macOS, so the companion loads via Developer Mode → Load unpacked. The userscript is the artifact that actually reaches most people; install it in Tampermonkey or Violentmonkey. On Chromium a userscript manager needs its own "Allow user scripts" toggle (Chrome 138+), and Violentmonkey's true document-start injection needs its "Alternative page mode" enabled. The Firefox package is unsigned: it runs temporarily via about:debugging, or permanently only on Nightly/DevEdition/ESR with signature enforcement off.
  • Account actions are deliberately narrow. A click on a chat emote saves it locally. Only an explicit follow-gate from Kick can add the matching channel Follow in the same action; public artwork alone never triggers it, and subscriber access is unchanged. Copying an emote name is always available. Typing one into the chat box is off by default, inserts the plain name at your cursor and stops there, never Kick's [emote:id:name] wire token, never an id, and no build ever sends a chat message. The daily-reward claim is off by default, presses only Kick's own claim button, and stops for the night once it claims.

Build and verify

No runtime or development dependencies are required beyond Node.js on the 24 LTS line: >=24.19.0 <25, which is the engines range. The upper bound is deliberate rather than lazy. The suite uses --experimental-test-tag-filter, and an experimental flag is allowed to change behaviour between majors, so the build and the gates refuse an unsupported major with a message instead of finding out during a release.

npm run build              # userscript + dist/extension/ + shareable zip
npm run verify             # offline: artifact checks + core tests
npm run verify:extension   # live: loads the extension in Chromium against Kick
npm run verify:firefox     # live: loads the Manifest V2 package in Firefox against Kick
npm run coverage:floors    # offline: the suite, then fail if coverage fell
npm run release:check      # offline gate + live 1440×900 and 1920×1080 checks

npm run release:check drives a real browser window for the two live runs, because several of those checks measure layout and paint rather than markup. It parks that window off-screen so a release run does not sit on top of your work. Set KF_WINDOW_POSITION to coordinates on a display if you want to watch it.

A skip is a legitimate answer to a lot of what the live gates cover: a run is logged out, or Kick did not render a surface on the route it was asked for. The checks that cannot skip for an environmental reason are named in scripts/live-contract.mjs, and release:check refuses to package when one of them is anything but a pass, at each viewport separately and in Firefox. A check that never reported at all counts as a refusal too, because silence is the one outcome that used to read as success.

The build concatenates the metadata block and the eight source modules into dist/kick-focus.user.js (see Repository map). It strips comments and blank code lines, plus indentation outside template content, then emits the same page-world bundle into both companion packages. The extension network rules are generated from the same host lists the page-realm classifier uses, so the layers cannot drift apart; npm run verify fails if they do.

npm run verify:extension opens a throwaway Chromium profile, loads the unpacked extension, visits Kick, and asserts that the service worker is running, the rulesets match the manifest's promises, the page world booted and sees the companion, cards are detected, an ad-host request is refused by the network stack, and the popup renders. It also fails when Kick's DOM drifts: each shell hook is located through an ordered list of probes, a stable id first, then structural and accessible fallbacks, and everything keeps working when the first stops matching, which is exactly why it needs catching. The check reads the same probe list the runtime uses, so there is no second list to fall out of date. It needs a Chromium binary, Google Chrome stable will not work, because it ignores --load-extension without reporting an error. It finds Playwright's Chromium automatically, or set CHROME_PATH.

It runs headed, because Kick answers headless browsers with a short JSON error instead of the site; DOM checks are skipped rather than reported as passing when the real page was not reached. KF_WINDOW_POSITION=x,y places the outer window, KF_WINDOW_SIZE=1440,900 applies and asserts the exact CSS viewport at DPR 1, and KF_HEADLESS=1 restricts the run to the network checks that do not depend on page content.

npm run release:check repeats the live proof at both supported desktop sizes and captures kick-focus-1440x900.png and kick-focus-1920x1080.png in a temporary directory (or KF_RELEASE_SCREENSHOT_DIR). Compare those captures with the current design references after each Kick deployment, checking shell geometry, overflow, clipped controls, and player/chat collisions. The command remains useful without Chromium: the offline gate still runs and the live portion reports SKIP.

Repository map

design/mockups/       Selected visual references for Kick routes and settings pages
design/screenshots/   Captured UI, re-taken when the interface changes
dist/                 Installable userscript, unpacked extension, and zip
scripts/              Deterministic build, artifact checks, live proof, release gate
src/core.mjs          Settings, routing, blocklist, storage registry, and validation logic
src/api.mjs           Kick API endpoint shapes, emote/catalog parsing, chat frame normalization
src/compatibility.mjs Shell/selector probes that detect Kick DOM drift
src/storage.mjs       Library storage providers: IndexedDB record, bounded synchronous seed, blob store
src/live.mjs          Same-origin reads of Kick's own endpoints, realtime chat, badges, deletions
src/multistream.mjs   Multi-stream grid, tile lifecycle, and the cross-tab roll-call
src/settings.mjs      Settings navigation, page composition, search, and viewer summaries
src/runtime.js        Live DOM, layout, commands, emote library, and request hooks
src/extension/        Chromium/Firefox manifests, bridges, service workers, popup
test/                 Node test suite (offline gate + vm boot/companion gates)

The build concatenates core.mjsapi.mjscompatibility.mjsstorage.mjslive.mjsmultistream.mjssettings.mjsruntime.js into one IIFE, in that order, stripping import/export as it goes: concat order is what supplies an imported name, so each module can declare its real dependencies and still load on its own under node --test.

live.mjs, multistream.mjs, and settings.mjs each export a factory that takes its page-owned collaborators (state, storage, toasts, translation) as an argument rather than reading them out of the bundle scope. That boundary is what lets all three be exercised by the test suite without a browser.

See RESEARCH.md for the dated audit and evidence, ROADMAP.md for prioritized follow-up work, and CHANGELOG.md for release history.

Kick Focus is an independent project and is not affiliated with or endorsed by Kick Streaming.

About

Desktop-first layout, accessibility, content filters, and best-effort ad defense for Kick.com. Zero-dependency userscript plus optional Chromium/Firefox companion, built from one source.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages