Skip to content

win-nice

CI npm version npm downloads License platform node dependencies

Windows equivalent of the unix nice / renice / cpulimit family, built for a specific pain point: running several parallel AI coding agents (Claude Code, etc.) on one Windows box without their builds/tests pegging every core and freezing the desktop — mouse, keyboard, window dragging, audio, all of it.

No compiled binaries, no runtime dependencies. Just .bat + .ps1 files that call the relevant Win32 APIs directly (Job Objects, process priority classes). npm is used only as a distribution/version channel and for the install CLI — none of the tools themselves need Node.js to run.

Argument handling

Every tool tries to launch the wrapped command directly first (CreateProcess, no shell involved at all) and only falls back to cmd.exe /c when the target turns out to be a .bat/.cmd file or a cmd.exe builtin that genuinely needs one.

Direct-launch path (the common case: the target is a real .exe): arguments are immune to cmd.exe's special characters entirely — &, |, <, >, ^, %, quotes, spaces, empty strings all pass through exactly as given, standard MSVCRT/CommandLineToArgvW quoting. cmd.exe is never invoked on this path, so there's nothing to expand.

cmd.exe /c fallback path (only reached for .bat/.cmd targets or cmd.exe builtins): &|<>^/quotes/spaces/empty strings are still fully protected. A literal % used to be able to trigger environment-variable expansion here — cmd.exe pairs up % characters across the entire command line, even across separate arguments, and there's no reliable per-character escape for that at the cmd.exe /c level. Rather than risk that, this path now fails closed: if any argument contains %, the tool refuses to run, prints an error to stderr, and exits with code 1 — the command never reaches cmd.exe. This is a change from just checking for the character; it's a hard reject, not a best-effort escape.

Separately, and unaffected by the fail-closed fix above: each tool ships as up to three files — name.bat, name.ps1, and a plain extensionless name shim (a #!/bin/sh script) — and which one a bare name ... invocation resolves to depends on the calling shell:

Calling shell Resolves to % handling
PowerShell name.ps1 full argument safety (see above)
cmd.exe, or PATHEXT-based resolution (e.g. Node's child_process, which doesn't include .PS1 in PATHEXT by default) name.bat corrupted before .ps1 ever runs (see below)
POSIX shell (Git Bash only — ignores PATHEXT/bare-name extension resolution entirely) name (no extension) full argument safety — the shim execs straight into name.ps1 via powershell -File, the same direct-to-.ps1 path PowerShell itself uses, with MSYS argument conversion disabled so slash-style switches (/c, /d) and Windows paths arrive untouched; no .bat/cmd.exe hop involved

Known limitation of the extensionless shims: the MSYS2_ARG_CONV_EXCL='*' they set to keep their own arguments intact is inherited by the wrapped command and its whole process tree, so an MSYS program spawned inside the wrapped command (an inner bash/sh, a #!/bin/sh git hook — not a native program like node.exe or cmd.exe) inherits it too and stops converting POSIX-style paths in its own children's arguments — e.g. caps 600 bash -c 'node /c/proj/run.js' fails with Cannot find module where the same command without the shim works. The shim's own documented argument guarantee (its arguments arrive untouched) is unaffected.

The extensionless shims are Git Bash-specific; WSL is not supported — WSL has no bare powershell (only powershell.exe), Windows PowerShell can't resolve the /mnt/... script path such a shim would pass to -File, and a WSL-side npm refuses this package anyway ("os": ["win32"] in package.json). Install through Windows Node/npm and call the tools from Git Bash.

The .bat file corrupts any literal % in its own arguments before your command, and before .ps1, ever runs at all, confirmed with nothing more than a bare echo %1 in a plain .bat. This is cmd.exe's own batch-parameter substitution (%1/%*) rescanning for %...% patterns across the whole line while parsing the .bat entry point itself; there's no fix for it from inside a .bat file, and it happens before the .ps1 (and its fail-closed % check) ever sees the arguments. Every other special character (&|<>^) survives this hop untouched.

Tools

idle <command> [args...]

Runs command at IDLE_PRIORITY_CLASS, waits for it to exit, propagates its exit code. Priority applies to the whole spawned process tree automatically — Windows' CreateProcess inherits IDLE/BELOW_NORMAL priority by default when a child process doesn't request a priority of its own.

belownormal <command> [args...]

Same as idle, at BELOW_NORMAL_PRIORITY_CLASS — a lighter touch than idle.

abovenormal <command> [args...]

Runs command at ABOVE_NORMAL_PRIORITY_CLASS, waits for it to exit, propagates its exit code.

Unlike idle/belownormal, this does not apply to the whole process tree — confirmed empirically. Windows only inherits IDLE/BELOW_NORMAL priority into child processes by default; ABOVE_NORMAL and higher are not, so anything the wrapped command spawns runs back at ordinary Normal priority. Only useful when the command you're wrapping does the actual work itself rather than delegating to child processes.

high <command> [args...]

Same as abovenormal, at HIGH_PRIORITY_CLASS — a stronger boost. Same single-process-only caveat applies.

realtime <command> [args...]

Same shape, at REALTIME_PRIORITY_CLASS. Dangerous, use with caution: REALTIME outranks the OS's own input/audio/UI threads — a busy realtime-priority process can make the entire desktop (mouse, keyboard, everything) stop responding, which is the exact failure mode this project otherwise exists to prevent. It also needs the SeIncreaseBasePriorityPrivilege privilege (elevated/admin processes have it by default); without it, Windows doesn't error out, it silently downgrades the request to HIGH_PRIORITY_CLASS instead — confirmed empirically. Same single-process-only caveat as abovenormal/high applies on top of all that.

capc <percent> <command> [args...]

Hard CPU quota (1-100) for the whole process tree, enforced by a Windows Job Object (JOBOBJECT_CPU_RATE_CONTROL_INFORMATION, hard cap). Unlike idle/ belownormal, this is a real ceiling on total CPU%, not just a scheduling priority — it holds even when nothing else on the machine is contending for CPU.

The cap covers the whole subtree from its very first instruction: the wrapped command is created suspended, assigned to the Job Object, and only then resumed — there's no window where it runs uncapped. Every ordinary descendant created via CreateProcess (and their children, recursively) automatically joins the same job; this is standard Job Object behavior on any supported Windows version, not something specific to newer ones. The known ways out: a descendant explicitly requesting CREATE_BREAKAWAY_FROM_JOB (and since the job here never sets a breakaway-allowed flag, that fails closed — the child just fails to launch rather than silently escaping the cap), or a process brought up through an external broker/service that never goes through the wrapped tree's own CreateProcess calls (Microsoft documents, for example, that a process started via WMI's Win32_Process.Create doesn't join the caller's job).

Windows 8+ specifically matters if something inside the wrapped command creates its own Job Object (some tools do, e.g. Chromium-based ones — or these tools themselves, when chained together, see "Chaining these tools together" below): before Windows 8 a process could belong to only one job at a time, so that inner AssignProcessToJobObject call would fail. Windows 8+ allows nested jobs, so it succeeds instead, and both jobs' limits apply — but how they combine depends on the limit type, not one universal "smaller wins" rule. For CPU rate control specifically, a nested job's rate is relative to what its parent already lets through, so equal caps multiply: capc 50 capc 50 ... yields roughly 25% of total system CPU, not 50% (see "Chaining these tools together" below for the full picture across limit types).

Blocks until the command exits, propagates its exit code.

capc 50 npm run build

capt <thread-count> <command> [args...]

Short for cap threads. Restricts the whole process tree to the first <thread-count> logical processors via Windows process affinity (JOBOBJECT_BASIC_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_AFFINITY) — same suspend-then-assign-then-resume Job Object mechanism as capc, so the same "covers the whole subtree from the first instruction" and "breakaway fails closed" guarantees apply.

Deliberately threads, not cores, in both the name and the semantics: Windows affinity masks address logical processors (hardware threads), not physical cores. On a machine with Hyper-Threading/SMT, capt 4 pins to 4 logical processors — depending on which ones, that could be 2 fully-used physical cores or 4 half-used ones; the affinity API has no concept of "whole core" grouping on its own. <thread-count> must be between 1 and the number of logical processors on the machine ([Environment]::ProcessorCount, capped at 63 — a single affinity mask can't address more). Under 32-bit Windows PowerShell (the SysWOW64 host) the effective cap is additionally 32: the affinity mask is a pointer-sized UIntPtr, so a 32-bit process can only address 32 logical processors — counts of 33-63 are rejected up front with a usage error naming 64-bit PowerShell (the same process-width limit capm enforces on its memory cap).

capt 4 npm run build

capm <size> <command> [args...]

Hard memory ceiling for the whole process tree, enforced by a Windows Job Object (JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_JOB_MEMORY) — same suspend-then-assign-then-resume mechanism as capc/capt, so the same "covers the whole subtree from the first instruction" and "breakaway fails closed" guarantees apply. Caps the whole job's aggregate committed memory, not any single process — like capc's CPU% and capt's affinity, it's one ceiling for the whole tree, not a per-process limit.

<size> accepts three forms:

Form Meaning
50 (bare integer, 1-100) percent of total physical RAM (GlobalMemoryStatusEx), not of whatever's currently free — the cap means the same thing regardless of what else is running on the machine at invocation time. Same convention as capc's own <percent 1-100> — deliberately no % character; see "Chaining these tools together" below for why
512m / 512M megabytes
2g / 2G gigabytes
capm 50 npm run build
capm 512m npm run build
capm 2g npm run build

Unlike capc, exceeding the limit doesn't throttle — it fails the allocation. A CPU cap just makes things slower; a memory cap that's exceeded causes the allocation call itself to fail (VirtualAlloc-family APIs return an error, .NET throws OutOfMemoryException) rather than the OS gracefully degrading anything. Most programs don't handle allocation failure cleanly, so in practice this usually looks like a crash. Set it too low and even the wrapped program's own runtime can fail to start (confirmed: capping Windows PowerShell 5.1 itself at 30 MB crashes it with StackOverflowException before it can run anything) — leave headroom above whatever interpreter/runtime the wrapped command needs just to start, on top of what your actual workload needs.

A capc/capt/capm/capn limit sticks to any daemon the wrapped command leaves running, for that daemon's entire lifetime — not just for the wrapped command's own run. Job Object membership is permanent for a process once assigned (short of an explicit, disallowed breakaway); a background process the command spawns and detaches from is still in the same job, still capped, for as long as it stays alive. This bites build tools that reuse a persistent process across invocations to skip cold-start cost: dotnet build's VBCSCompiler/MSBuild node reuse, a Gradle daemon, file-watcher processes left running by npm run watch-style scripts. A follow-up uncapped dotnet build (or gradle) can end up running inside the previous capc call's Job Object without a new capc/capt/capm invocation of its own, capped because a stale daemon from an earlier call is doing the work. Either don't leave the daemon running across a call whose limit shouldn't persist (dotnet build -p:UseSharedCompilation=false, gradle --no-daemon), or accept that the limit is now effectively attached to the daemon until it's killed.

caps <seconds> <command> [args...]

Runs the command with a wall-clock deadline: if it's still running when <seconds> have passed, it is force-killed and caps exits with code 124 (the unix timeout(1) convention), printing caps: timed out after <seconds>s - job and every process in it were force-killed to stderr. If the command finishes in time, caps propagates its exit code exactly like every other launcher in this family.

The deadline is genuinely wall-clock: caps computes it once as an absolute UTC timestamp and arms a one-shot waitable timer with that absolute due time (SetWaitableTimer), then waits on the timer and the wrapped process together (WaitForMultipleObjects). An absolute timer due time is a property of the wall clock, not of an in-progress wait, so time the machine spends asleep/suspended counts against it: if the deadline passes during sleep, the timer is already signaled when the machine wakes, and caps fires immediately instead of waiting out any leftover countdown (a relative WaitForSingleObject wait does not count sleep time on Windows 8+ and keeps counting its pre-sleep remainder after the wake). If the two signals race - a child exiting right around the deadline - caps asks the kernel for the process's real exit time (GetProcessTimes) and only accepts it as on-time if it actually finished at or before the deadline. Because that due time is a value on the system clock, a manual or service-driven system-clock adjustment during the wait moves it with the clock: the actual wait can come out shorter or longer than the requested <seconds> (sleep/suspend still counts correctly - only clock adjustments shift the deadline).

The same "covers the whole subtree from the first instruction" guarantee applies: same suspend-then-assign-then-resume Job Object mechanism as capc/capt/capm, so at expiry a single TerminateJobObject kernel call kills the direct child and every descendant it spawned — no process-tree walking, no window where a grandchild outlives the child. The job carries only JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, which doubles as the backstop if the caps wrapper itself is killed non-cooperatively (taskkill without /T, a crash): Windows itself then terminates the whole job at that moment. That backstop is released before a normal, inside-the-deadline exit, so a background daemon the wrapped command legitimately left running survives - caps only forces a whole-tree kill on its own timeout or on the wrapper's own non-cooperative death, never on an ordinary successful exit.

<seconds> accepts a positive whole or decimal number (2, 2.5), converted to whole milliseconds (floored; minimum 1 ms). The maximum is 4294967294 ms (~49.7 days) — a deliberate usage ceiling, not a Win32 API limit: the deadline is an absolute 64-bit FILETIME armed into a waitable timer, and the WaitForMultipleObjects wait itself always passes INFINITE (the timer is what bounds it). Anything larger is rejected as a usage error (exit 1), never silently truncated into a different deadline.

caps sets no resource limit of any kind — no CPU, memory, priority, or affinity limit. It is not a quota tool: capc/capt/capm deliberately impose no timeout (a quota tool shouldn't unilaterally decide a legitimate long build is stuck — same precedent as nice/cpulimit); caps is the complement, for when you have already decided that N seconds is the limit (see the system-clock caveat above).

caps 300 npm test

capn <count> <command> [args...]

Runs the command under a hard ceiling on the number of simultaneously active processes in its whole process tree (JOBOBJECT_BASIC_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS) — same suspend-then-assign-then-resume Job Object mechanism as capc/capt/capm, so the same "covers the whole subtree from the first instruction" and "breakaway fails closed" guarantees apply.

The count includes the directly wrapped process itself: it is assigned to the still-empty job before it can spawn anything, so capn 1 <command> runs the command but the instant it tries to spawn any child — including infrastructure children like PowerShell's own Add-Type compiler (csc.exe, plus the CVTRES.EXE that compiler runs) — that spawn attempt fails. Confirmed empirically, not just from docs: under capn 1, a wrapped PowerShell's Start-Process fails with "Not enough quota is available to process this command." and the parent keeps running and exits 0.

Unlike a capc throttle, exceeding the limit doesn't degrade anything — the offending spawn attempt itself is what fails (closer to capm's failed allocation): nothing is killed, nothing already running is affected, and the wrapped process sees an ordinary process-creation failure from its own spawn call. A command that stays within the budget is completely unaffected — capn 3 wrapping a parent that spawns 2 children runs exactly like an uncapped one (also confirmed empirically).

<count> is a positive whole number, minimum 1, maximum 4294967295 — the ActiveProcessLimit struct field is a uint32, so anything larger (or negative, or non-numeric) is a usage error (exit 1), never a silently truncated limit.

capn 10 npm run build

Chaining these tools together

These tools can be stacked by passing one as another's <command>:

capm 50 capc 50 idle npm run build

Each wrapper wraps everything after its own arguments, so the outermost wrapper is whichever one you type first. A few things to know before relying on a combination:

Bare tool names resolve through the same cmd.exe/PATHEXT fallback documented above. None of these ship a .exe, so e.g. capm's attempt to launch capc directly always fails and falls back to cmd.exe, which finds capc.bat via PATHEXT — meaning chaining only works when the tools' install directory is actually on PATH, and any % elsewhere on that command line trips the same fail-closed check described above. capm's own <size> deliberately has no % form for exactly this reason: an earlier version accepted 25%, and capc 50 capm 25% ... failed the fail-closed check while capm 25% capc 50 ... worked fine — an order-dependent foot-gun. A bare percent (capm 50 capc 50 ...) sidesteps it entirely, in any order.

Nested Job Object limits do not follow one universal "smaller wins" rule — each limit type combines differently:

  • CPU (capc): a nested job's CPU rate is relative to what its parent already lets through, so equal caps multiply rather than take the minimum — capc 50 capc 50 ... yields roughly 25% of total system CPU, not 50%. This is documented Windows behavior for JOBOBJECT_CPU_RATE_CONTROL_INFORMATION, not a bug here.
  • Memory (capm): each ceiling applies independently to its own accounting scope, and those scopes are not the same size — a job's committed-memory accounting includes its own processes plus every child job's committed memory, while a child job's own accounting doesn't see the outer wrapper's process at all (Nested Jobs — Resource Accounting). So capm 500m capm 400m ... is not guaranteed to behave like a plain min(500m, 400m) = 400m ceiling on the innermost workload — the outer 500m job can run out of budget first purely from its own wrapper process's memory use, on top of whatever the inner 400m job is using. Don't rely on nested capm ceilings combining to an exact number; treat the outer value as an upper bound that can bind earlier than expected.
  • Priority (idle/belownormal/abovenormal/high/realtime): not a Job Object limit — the last one applied to a given process simply wins, same as invoking any one of them alone.
  • Affinity (capt): nested affinity is an effective-limits chain — a child job's mask can be as tight as it wants but is clamped to whatever the parent already allows, never wider (Nested Jobs — Job Limits). Confirmed empirically: capt 2 capt 3 ... (inner asking for more processors than the outer allows) still comes back pinned to the outer's 2, not the inner's 3 — no error, just silently clamped to the tighter mask.
  • Process count (capn): each job enforces its own ActiveProcessLimit independently against its own simultaneously-active count — a spawn has to fit under every job in the chain at once, and an outer job's count includes the inner wrapper process itself plus everything beneath it. Not a "silently clamped to the tighter limit" rule like affinity, and not a plain min(): confirmed empirically, capn 1 capn 5 ... fails outright (the outer job is already full with the inner wrapper alone, so the inner wrapper can't even start its target — for a PowerShell-based inner tool this surfaces as its own Add-Type/csc.exe child spawn being refused, exit 1); capn 2 capn 5 ... still fails one step later (the compiler's own CVTRES.EXE child doesn't fit); capn 3 capn 5 ... works. In the other direction, capn 5 capn 1 <cmd-that-spawns> runs the command but its child spawn is refused by the inner limit while the outer still has room. Leave real headroom in an outer capn for the chain itself — roughly 3 slots before a PowerShell-based inner tool's actual workload even starts.
  • Timeout (caps): not a Job Object limit being combined at all — each caps enforces its own deadline on its direct child. The innermost caps wrapping the eventual work fires at its own deadline and the outer one propagates the inner's 124 exit code like any other tool's. If the outer deadline fires first, its TerminateJobObject kills the whole subtree including the inner caps wrapper — and everything the inner wrapper had assigned to its own job dies with it via that inner job's own KILL_ON_JOB_CLOSE flag (the same cascade documented above). Either way the caller sees 124.

Test any combination you actually plan to depend on; don't assume "more wrappers, more restrictive" holds uniformly across limit types.

uiup

One-shot priority boost (HIGH) for the live shell/UI/audio processes, intended to improve desktop responsiveness while heavy background work runs underneath: explorer, dwm, sihost, ShellExperienceHost, StartMenuExperienceHost, StartMenu, SearchApp, audiodg. This is a best-effort one-shot tweak, not a guarantee — memory pressure, I/O saturation, driver/GPU stalls, or a realtime-priority workload elsewhere can still make the desktop stutter regardless.

Self-elevates via UAC — dwm/sihost run under a separate account (Window Manager\DWM-1), so raising their priority needs admin rights.

The boost does not propagate to apps you launch from Explorer afterwards: HIGH priority isn't inherited by child processes under Windows' default CreateProcess rules (only IDLE/BELOW_NORMAL are). Confirmed empirically — see the project history for the test.

admin <command> [args...]

Runs command elevated (as Administrator), waits for it to exit, propagates its exit code — a blocking elevation wrapper with the same wait-and-propagate semantics as the other wrappers here, but it does not set a priority class (unlike idle). If it's already elevated, runs inline sharing the current console, with the full direct-launch argument safety described above.

If the calling shell isn't already elevated, triggers the standard UAC consent prompt (via ShellExecute, always opening its own console window, incompatible with sharing the caller's). Unlike a plain ShellExecute("cmd.exe", "/c ..."), this branch also tries a direct launch first: a .bat/.cmd target still needs the cmd.exe /c fallback (no elevation-capable equivalent of CreateProcess's own .bat/.cmd auto-relaunch), but any other target launches directly via -FilePath, never touching cmd.exe — same as the already-elevated branch, a literal % in any argument is only refused when the .bat/.cmd fallback is actually needed, since a direct launch is never exposed to % expansion at all.

admin npm install -g some-package

AI CLI launchers

Not part of the priority/CPU-limiting toolset above — these two are unrelated one-line convenience wrappers that happened to live alongside win-nice's own scripts and got folded into the same install/PATH mechanism.

cy [args...]

Runs claude --dangerously-skip-permissions [args...].

cx [args...]

Runs codex --dangerously-bypass-approvals-and-sandbox [args...].

Both bypass the tool's own permission/approval/sandbox prompts. Only use them in a context where you'd already accept running that AI agent unattended (e.g. inside an already-sandboxed/disposable environment). They do not add any sandboxing of their own — the flag names describe exactly what they do.

Install

npm install -g win-nice

This copies every tool above into %LOCALAPPDATA%\win-nice\bin and adds that directory to your user PATH (via postinstall). Restart your terminal afterwards so the new PATH takes effect.

npm uninstall -g win-nice does not reverse this — npm's uninstall lifecycle script was removed (npm ≥ 7 never runs it at all; there is no supported npm version where a preuninstall script would fire). Run npx win-nice uninstall (see below) before or after the npm uninstall, either order — npx re-fetches the package to run it, so it still works even after the global package itself is gone.

The commands themselves are never registered through npm's own global bin shimming — idle/capc/etc. are too generic a name to risk colliding with someone else's global npm package. npm here is only the delivery mechanism for a dedicated, PATH-managed install directory.

Without npm: copy the contents of bin/ into any directory on your PATH.

Managing an existing install

npx win-nice status      # what's installed, where, which version
npx win-nice reinstall   # re-copy from the current package version
npx win-nice uninstall   # remove files + PATH entry

uninstall/reinstall treat every file recorded in the install manifest (%LOCALAPPDATA%\win-nice\install-manifest.json) as owned by the package and remove it regardless of local edits — these are managed files, not a customization point. If the manifest itself is missing or corrupt, uninstall falls back to scanning the install directory and only removes files that still carry the win-nice: managed-file marker comment, so that scan doesn't delete unrelated files sitting in the same directory.

Claude Code / Codex skill

npx win-nice skill install     # add the win-nice reference skill
npx win-nice skill uninstall   # remove it

Initial installation is a separate, opt-in step — postinstall never creates a skill copy for a user who hasn't run skill install. Once a copy exists, though, every subsequent ordinary package install/upgrade (including postinstall) automatically refreshes that existing marked copy to the new version's content, so it never goes stale after an upgrade. Explicit skill install copies skills/win-nice/SKILL.md (documents every tool above except cy/cx) to ~/.claude/skills/win-nice/SKILL.md and ~/.agents/skills/win-nice/SKILL.md (Codex CLI's personal-skill location) — SKILL.md is an open, cross-agent format (agentskills.io), so the same file works for both unmodified.

Unlike the bin/ install directory, ~/.claude/skills and ~/.agents/skills aren't exclusively win-nice's — a win-nice folder there could belong to something else entirely. install refuses to overwrite a file that's already there without the win-nice: managed-skill marker comment (reports it as skipped rather than clobbering it), and uninstall only removes a copy that still carries that marker.

Exit code reflects this: install/uninstall exit 1 if any target was skipped due to a real conflict (foreign file present for install; marker-stripped/user-modified file for uninstall). A clean install/uninstall exits 0, and so does uninstall finding nothing to remove — a missing target isn't a conflict.

Requirements

Windows 8 / Server 2012 or newer (Job Object CPU rate control). PowerShell is bundled with Windows — no separate install needed to run the tools. Node.js is only needed for the npm-based installer/tests, not for the tools themselves. cy/cx additionally need claude/codex installed and on PATH.

PowerShell execution policy: Windows client editions default to Restricted, which blocks a bare .ps1 invoked directly by PowerShell itself (... cannot be loaded because running scripts is disabled on this system). The .bat files are unaffected (they pass -ExecutionPolicy Bypass explicitly), and so are the Git Bash shims — each one runs powershell -NoProfile -ExecutionPolicy Bypass -File ... itself, so only invoking a bare name.ps1 from PowerShell needs the one-time fix below. Caveat: Group Policy can still override -ExecutionPolicy Bypass in some managed environments. Run once, as the user who'll run these tools:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

Environment variables

  • WIN_NICE_HOME — overrides the install root (default %LOCALAPPDATA%\win-nice). Used by the test suite; also useful for a non-default install location.
  • WIN_NICE_SKILL_HOME — overrides the home directory ~/.claude/skills/... and ~/.agents/skills/... are resolved against (default: the real user home). Mirrors WIN_NICE_HOME, for the skill files instead of bin/. Affects both the explicit skill install/skill uninstall commands and the automatic postinstall/upgrade refresh of an already-installed copy described above.
  • WIN_NICE_NO_PATH — if set (to anything), install/uninstall/ reinstall skip the user PATH update/removal entirely, only managing files under the install directory.

Testing

From a source checkout (test/ and scripts/ aren't part of the published npm package - the test/test:elevated/release-check scripts below only work when run from a git clone, not against an installed package):

npm test                                       # installer logic (fast; isolated scratch registry key, cleaned up automatically)
powershell -Command "Invoke-Pester -Path test\win-nice.Tests.ps1"   # real tool behavior

Pin the Pester version before running the second command by hand: Windows ships Pester 3.4.0 built in, but a system with a newer Pester also installed (GitHub-hosted runners have both 3.4.0 and 5.x side by side) auto-loads the newer one, and this suite uses Pester 3's legacy assertion syntax (Should Be), which 5.x removed entirely. If bare Invoke-Pester fails immediately on every It, import the right version first:

powershell -Command "Import-Module Pester -MaximumVersion 3.99; Invoke-Pester -Path test\win-nice.Tests.ps1"

(this is what CI/publish do; see .github/workflows/ci.yml.)

The Pester suite is an integration suite: it spawns real processes, checks actual PriorityClass, ProcessorAffinity, and Job Object CPU throttling across every tool, and takes roughly 1-2 minutes (more under system load - the CPU-cap test retries a few times if the machine is too busy to get a clean baseline). It never touches the real system PATH/registry; the installer tests use WIN_NICE_HOME to redirect installs into a temp directory instead.

3 of its cases only exercise admin.ps1's already-elevated branch, which needs the whole test-runner process to already be elevated (not just admin.ps1 itself) - a normal, unelevated run reports them Skipped, which is expected, not a failure. A separate, disjoint set of 4 cases only makes sense when NOT elevated, and Skips under elevation instead. npm run test:elevated (test/run-elevated.ps1) is a single entry point for the first group: one UAC prompt elevates the runner once, then the suite runs inside that elevated session, activating those 3 cases (and skipping the other 4). Neither a normal run nor an elevated run alone exercises every case - run both for full coverage.

npm run release-check (scripts/release-check.js) is a maintainer-only, source-checkout-only command that packs the actual npm tarball, installs it into an isolated temp directory, and verifies the real installed artifact (launcher file set, manifest version, capc/capt/capm/caps/capn exit-code smoke tests, and CHANGELOG/tag consistency) - this is what publish.yml runs right before npm publish. It needs scripts/ and git tag history, neither of which is part of the published package, so it can't run against an installed copy.

License

Dual-licensed under MIT or Apache License, Version 2.0, at your option.

About

Windows nice/renice/cpulimit for parallel AI coding agents - process priority, hard CPU quotas & thread affinity via Job Objects, elevation, zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages