Cross-platform Krunker.io game client forked from Krunker Police Client with all KPD/moderator features stripped: no KPD auth, OBS recording, evidence uploads, yt-dlp, bytenode, or code obfuscation. Retained: unlimited FPS (custom Electron 42), ad blocking, resource swapper, matchmaker, userscripts, chat translator, Discord RPC, alt account manager, configurable keybinds, and advanced Chromium flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Krunker Civilian Client
Cross-platform Electron-based game client for Krunker.io. Loads the game directly in a BrowserWindow with GPU optimizations, unlimited FPS, persistent sessions, ad blocking, resource swapping, custom matchmaker, userscript engine, and Discord Rich Presence.
Quick Start
npm install
npm start # Build all targets + launch Electron
For development with sourcemaps:
npm run dev # Builds in dev mode + launches Electron
Scripts
| Script | Description |
|---|---|
npm run dev |
Development build + launch |
npm start |
Full build (main + preload) + launch |
npm run build |
Build both Vite targets |
npm run build:main |
Build main process only |
npm run build:preload |
Build preload script only |
npm run dist:win |
Build + package for Windows (NSIS installer + portable) |
npm run dist:linux |
Build + package for Linux (AppImage + deb) |
npm run dist:all |
Build + package for all platforms |
npm run clean |
Remove dist/ and out/ directories |
Architecture
Custom Patched Electron 42
This client uses a custom-patched Electron 42 (Chromium 134, Node 24). The --disable-frame-rate-limit Chromium flag — required for unlimited FPS — causes input starvation ("aim freeze") on Chromium 84+. At uncapped frame rates (300+ FPS), the compositor floods the main thread task queue and input events get delayed 50-300ms, then snap to catch up. The ImplLatencyRecovery/MainLatencyRecovery features mitigated this on Chromium 87–93 but were removed in Chromium 94.
The Patch
A single-line change in Chromium's main thread scheduler (main_thread_scheduler_impl.cc) demotes input tasks from kHighestPriority to kNormalPriority, allowing the scheduler's built-in anti-starvation logic to fairly interleave input and compositor work:
case MainThreadTaskQueue::QueueTraits::PrioritisationType::kInput:
- return TaskPriority::kHighestPriority;
+ return TaskPriority::kNormalPriority;
Benchmarked via CDP Input.dispatchMouseEvent:
| Metric | Before | After |
|---|---|---|
| p99 latency | 97ms | 34ms |
| Max latency | 308ms | 38ms |
| Events >50ms | 8.6% | 0% |
| Frames rendered | baseline | +21% |
| Mouse events processed | baseline | +9% |
The full patch file, GN build args, and step-by-step build instructions are in electron-build/.
Binary Distribution
The patched binary is downloaded automatically during npm install via scripts/download-electron.js from a Gitea release asset (electron-patched tag). The electron npm dependency (npm:electron-nightly@42.0.0-nightly.20260227) provides TypeScript types and the CLI entry point, but the actual binary comes from the patched download.
FPS uncap flags (always applied when enabled):
disable-frame-rate-limit + disable-gpu-vsync + max-gum-fps=9999
Building from Source
Prerequisites: Windows 10/11 x64, ~100 GB disk, 16+ GB RAM, Visual Studio 2022 with C++ workload.
# 1. Install depot_tools and add to PATH
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
# 2. Check out Electron source (~40-60 GB)
mkdir C:\electron && cd C:\electron
gclient config --name "src/electron" --unmanaged https://github.com/nicedayzhu/electron.git@v42.0.0-nightly.20260227
gclient sync --with_branch_heads --with_tags
# 3. Apply the patch
cd src
git apply path/to/electron-build/input-priority-fix.patch
# 4. Configure and build (~2-4 hours for release)
gn gen out/Release
cp path/to/electron-build/args.release.gn out/Release/args.gn
gn gen out/Release
ninja -C out/Release electron
# 5. Create distributable zip
ninja -C out/Release electron:dist_zip
See electron-build/BUILD.md for detailed instructions, testing builds, and verification steps.
How the Game Loads
The game is loaded directly in the main BrowserWindow via win.loadURL('https://krunker.io'). This gives the game full GPU compositing, identical to Chrome.
Process Model
Main Process (src/main/index.ts)
|
|-- Creates BrowserWindow loading krunker.io directly
|-- Manages session (persist:krunker), UA cleaning, permissions
|-- Ad blocking + resource swapping via webRequest.onBeforeRequest
|-- Configurable keybinds via before-input-event
|-- Matchmaker IPC trigger (sends config to preload on hotkey)
|-- CSS injection (ad hiding, client settings, matchmaker popup, keybind dialog)
|-- File-based logging (electron.log in userData/logs/)
|-- IPC handlers for window controls, config, devtools, userscripts
|-- Userscript manager (filesystem scanning, tracker, preferences)
|-- Auto-updater (Gitea releases API check, Setup.exe download + launch)
|-- Discord Rich Presence via raw IPC socket
|-- Consent dismiss via polling (no MutationObserver on main frame)
|
+-- Preload Script (src/preload/index.ts)
|-- IPC bridge (window.kpc)
|-- Action button grid (folder shortcuts, log viewers, reset/restart)
|-- Settings UI hooks (collapsible sections: swapper, matchmaker, chat, translator, accounts, advanced, userscripts)
|-- Userscript engine (metadata parser, execution, CSS injection, settings)
|-- Matchmaker IPC listener -> fetchGame() in matchmaker.ts
|-- Chat translator (Google Translate API, MutationObserver on #chatList)
|-- Exit button (exposes Krunker's native #clientExit element)
|-- Keybind capture dialog (Crankshaft-style)
Source Files
src/
main/
index.ts Main process — window, IPC, session, ad blocking, keybinds
platform.ts OS detection, Chromium GPU/performance flags (per-platform)
config.ts electron-store schema, defaults, DEFAULT_KEYBINDS
client-ui.ts Injected CSS for settings panel, keybind dialog, matchmaker popup
swapper.ts Resource swapper — local asset overrides via custom protocol
userscripts.ts Userscript manager — filesystem scanning, tracker.json, preferences
discord-rpc.ts Discord Rich Presence via raw IPC socket
logger.ts File logging with daily rotation and 7-day retention
updater.ts Auto-updater — Gitea releases API check, download, installer launch
update-window.ts Update progress BrowserWindow with inline HTML + progress bar
preload/
index.ts IPC bridge, settings UI hooks, keybind capture, matchmaker listener, alt manager
utils.ts Shared types (SavedConsole, KeybindDef), helpers (escapeHtml, genChatMsg)
matchmaker.ts Custom matchmaker — lobby fetch, filtering, popup UI
translator.ts Real-time chat translation via Google Translate API
userscripts.ts Userscript engine — metadata parser, execution, CSS injection, state
Build System
Two Vite configs build independent targets:
| Config | Target | Output | Notes |
|---|---|---|---|
vite.main.config.ts |
Main process | dist/main/index.js (CJS) |
Externalizes electron, electron-store, and Node builtins. Targets Node 20. |
vite.preload.config.ts |
Preload script | dist/preload/index.js (CJS) |
Externalizes electron and uuid. Targets Node 20. |
Custom Electron Binary
The patched Electron binary is managed by scripts/download-electron.js:
- Downloads from Gitea release tag
electron-patched, assetelectron-v42.0.0-nightly-patched-win32-x64.zip - Replaces
node_modules/electron/dist/with the patched binary - Runs automatically as a
postinstallscript duringnpm install - Skips download if the version file already matches
Configuration (electron-store)
Settings are persisted in krunker-civilian-config.json (OS-specific app data directory). Schema defined in src/main/config.ts:
interface AppConfig {
window: { width, height, x, y, maximized, fullscreen }
performance: { fpsUnlocked, hardwareAccel, gpuPreference }
game: { lastServer, socialTabBehaviour, joinAsSpectator }
swapper: { enabled, path }
userscripts: { enabled, path }
matchmaker: { enabled, regions, gamemodes, minPlayers, maxPlayers, minRemainingTime, openServerBrowser }
keybinds: { reload, newMatch, copyGameLink, joinFromClipboard, devTools, matchmaker,
matchmakerAccept, matchmakerCancel, pauseChat, fullscreenToggle }
ui: { showExitButton, deathscreenAnimation, hideMenuPopups }
discord: { enabled }
translator: { enabled, targetLanguage, showLanguageTag }
advanced: { removeUselessFeatures, gpuRasterizing, helpfulFlags, disableAccelerated2D, increaseLimits,
lowLatency, experimentalFlags, angleBackend }
accounts: SavedAccount[]
platform: { detectedOS, gpuBackend }
}
Platform Handling
| Feature | Windows | Linux |
|---|---|---|
| Window chrome | Standard OS frame (frame: true) |
Standard OS frame (frame: true) |
| GPU backend | D3D11 via ANGLE (default, configurable) | Default (+ ozone-platform-hint auto for Wayland) |
| GPU sandbox | Default | Disabled (AppImage FUSE + Mesa driver compat) |
Common flags always applied: disable-backgrounding-occluded-windows, ignore-gpu-blocklist. FPS uncap flags (disable-frame-rate-limit, disable-gpu-vsync, max-gum-fps=9999) applied when enabled. The ANGLE backend is configurable (default D3D11 on Windows; options include OpenGL, Vulkan, D3D9, D3D11on12). Additional Chromium flags (GPU rasterization, useful features, low latency, etc.) are configurable via the Advanced settings panel.
Session & User-Agent
- Uses
partition: 'persist:krunker'for persistent login/settings across restarts - User-Agent is cleaned to strip the app name identifier (keeps
Electron/so Krunker enables its Client settings tab)
Features
Core Client
- Unlimited FPS via custom-patched Electron 42 build
- Ad blocking (network-level URL filter + CSS hiding)
- Resource swapper (replace game textures, sounds, models with local files)
- Custom matchmaker (filter lobbies by region, gamemode, player count, remaining time)
- Userscript system (Tampermonkey-style metadata, custom per-script settings, instant toggle via unload)
- Chat translator (real-time translation via Google Translate API with language tags)
- Exit button in Krunker's sidebar menu (exposes native
#clientExitelement) - Selectable + pausable chat (always-on text selection, F10 to freeze auto-scroll for reading history)
- Configurable keybinds with Crankshaft-style rebinding dialog
- Configurable ANGLE backend (D3D11, OpenGL, Vulkan, D3D9, D3D11on12 — platform-filtered)
- Advanced Chromium flag settings (GPU rasterization, low latency, experimental features, and more)
- Client settings tab in Krunker's settings panel with collapsible sections
- Action button grid in settings (folder shortcuts, log viewers, reset/restart options)
- File-based logging (electron.log in userData/logs/, daily rotation with 7-day retention)
- Persistent game session, window state persistence, cookie consent auto-dismiss
- Auto-updater (Gitea releases API check, Setup.exe download with progress window, installer launch)
- Discord Rich Presence (game state, map, mode, player count)
- Death screen animation blocker (prevents CSS animation performance impact at uncapped FPS)
- Menu popup hider (hides store ads, stream containers, news popups)
Alt Manager (Account Switching)
- Account storage — Save multiple Krunker accounts (label, username, password)
- Quick switch — In-game "Accounts" menu item opens Krunker's native popup with saved accounts
- Login automation — Auto-logout +
loginOrRegister()credential fill - Settings section — Full account CRUD in the collapsed "Accounts" settings section
- Credential obfuscation — Credentials stored with character-shift encoding to prevent casual config reading
Known Constraints
MutationObserver Breaks WebGL on Main Frame
A MutationObserver with { childList: true, subtree: true } on Krunker's main frame DOM causes the WebGL 3D engine to hang indefinitely. Consent dismiss uses polling (setInterval) on the main frame instead. MutationObserver is safe in iframes and targeted elements like #menuWindow.
MutationObserver Infinite Loops (Chromium 94+)
Observers that modify elements within their observed subtree can cause infinite loops. In Chromium 94+, even textContent assignments that don't change the value still create new text nodes (triggering childList mutations). Observers must disconnect before DOM modifications and reconnect after.
Synthetic Events Lack User Activation (Chromium 94+)
Events created via new MouseEvent() / new KeyboardEvent() have isTrusted: false and do not provide user activation. Krunker ignores untrusted input events. The client uses webContents.sendInputEvent() via IPC to inject trusted input events from the main process.
Custom Electron Build Required
The --disable-frame-rate-limit flag causes compositor input starvation on all Chromium versions 84+. The ImplLatencyRecovery/MainLatencyRecovery features that mitigated this were removed in Chromium 94. Our custom Electron 42 build patches the Chromium main thread scheduler to fairly interleave input and compositor work. See Building from Source above and electron-build/BUILD.md for full instructions.
Uncapped FPS and CSS Animations
At uncapped frame rates (600+ FPS), Krunker's CSS animations (e.g. death screen slide-in) force a layout recalculation on every frame, which can saturate the renderer main thread. The death screen animation blocker (ui.deathscreenAnimation) is enabled by default to prevent this.
Tech Stack
- Electron 42 (Chromium 134, Node 24) — Custom-patched build for unlimited FPS
- TypeScript 5.7 — Type-safe source code
- Vite 6 — Fast bundler (2 build targets: main + preload)
- electron-store 8 — JSON config persistence (CJS)
- uuid 9 — Unique ID generation
- electron-builder 26 — Cross-platform packaging (NSIS, portable, AppImage, deb)
Project Structure
Krunker-Civilian-Client/
src/
main/ Main process source (10 modules)
preload/ Preload script source (5 modules)
dist/ Vite build output
main/
preload/
out/ electron-builder packaged output
build/ Build resources (icons, .desktop file)
scripts/ Build scripts (Electron patched binary download)
electron-build/ Custom Electron build instructions and patches
vite.main.config.ts
vite.preload.config.ts
electron-builder.yml
tsconfig.json
package.json