Stops Claude from starting a heavy job (typecheck, tests, builds) while another one is already running on the machine.

A Claude Code mod that stops Claude from starting a heavy job (typecheck, tests, lint, builds) while another one is already running on the machine. It helps most on laptops with little memory, where two tsc runs at once can freeze the machine.
Before every Bash command that matches its pattern, it lists the machine's processes. If a heavy job is already running, it refuses the command and tells Claude what is running and for how long, so Claude waits instead of piling on. Because it reads the process list rather than its own history, it also sees jobs started in the background, by subagents, by other Claude sessions and in your own terminals.
/plugin install heavy-job-guard --marketplace azoof-ahmed/claude-heavy-job-guard
Answer y to add the marketplace, then pick a scope.
Set them in /config, or in settings under pluginConfigs["heavy-job-guard"].options.
| Option | Default | What it does |
|---|---|---|
pattern | tsc, vitest, jest, playwright, oxlint, eslint, xcodebuild, gradlew, bun/npm/pnpm/yarn [run] typecheck/test/build/lint/check, turbo run …, cargo build/test/check | Regex for a heavy job. It is matched against both the new command and every running process. |
ignore | --lsp, --watch, watch, language servers, tsserver | Regex for running processes that match pattern but never finish on their own. They never block. |
mode | block | block refuses the command; warn runs it and shows a toast. |
subagents | allow | deny refuses every heavy job a subagent starts, so only the main agent runs them. |
claude plugin validate .
claude plugin test .
claude --plugin-dir .
MIT
hooks/register.ts 96 lines1import type { EngineInterface, Register } from 'claude-code'
2
3type Job = { pid: number; elapsed: string; command: string }
4
5const SHOWN_JOBS = 3
6const SHOWN_CHARS = 120
7
8/** Claude's Bash tool wraps each command as `zsh -c source … && eval '<command>' …`. */
9function unwrap(command: string): string {
10 const evaled = /\beval '((?:[^']|'\\'')*)'/.exec(command)
11
12 return (evaled?.[1] ?? command).replaceAll(`'\\''`, `'`)
13}
14
15function clip(text: string): string {
16 return text.length > SHOWN_CHARS ? `${text.slice(0, SHOWN_CHARS - 1)}…` : text
17}
18
19/** Every process on the machine whose command line matches `pattern` and not `ignore`, parents first. */
20export async function runningJobs(
21 $: EngineInterface,
22 pattern: RegExp,
23 ignore: RegExp,
24): Promise<Job[]> {
25 const ps = await $.process.run(['ps', '-axo', 'pid=,etime=,command='])
26
27 if (ps.exitCode !== 0) {
28 return []
29 }
30
31 const jobs: Job[] = []
32
33 for (const line of ps.stdout.split('\n')) {
34 const match = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line)
35 const command = match?.[3] ?? ''
36
37 if (match === null || !pattern.test(command) || ignore.test(command)) {
38 continue
39 }
40
41 jobs.push({ pid: Number(match[1]), elapsed: match[2] ?? '', command: unwrap(command) })
42 }
43
44 return jobs.sort((a, b) => a.pid - b.pid)
45}
46
47export function describeJobs(jobs: readonly Job[]): string {
48 const shown = jobs
49 .slice(0, SHOWN_JOBS)
50 .map(job => ` pid ${job.pid} (running ${job.elapsed}): ${clip(job.command)}`)
51 const more = jobs.length > SHOWN_JOBS ? [` …and ${jobs.length - SHOWN_JOBS} more`] : []
52
53 return [...shown, ...more].join('\n')
54}
55
56export const register: Register = (on, options) => {
57 const pattern = new RegExp(String(options.pattern))
58 const ignore = new RegExp(String(options.ignore))
59
60 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
61 if (!pattern.test(e.command)) {
62 return next(e)
63 }
64
65 if (options.subagents === 'deny' && e.agentId !== undefined) {
66 return {
67 deny:
68 `${$.plugin.name}: subagents don't run heavy jobs here (typecheck, tests, builds). ` +
69 'Finish your edits and report back; the main agent runs this one.',
70 }
71 }
72
73 const jobs = await runningJobs($, pattern, ignore)
74
75 if (jobs.length === 0) {
76 return next(e)
77 }
78
79 if (options.mode === 'warn') {
80 $.ui.toast(`${$.plugin.name}: starting a heavy job while ${jobs.length} already run`)
81
82 return next(e)
83 }
84
85 return {
86 deny: [
87 `${$.plugin.name}: a heavy job is already running on this machine, and two at once ` +
88 'compete for its memory and CPU:',
89 describeJobs(jobs),
90 'Wait for it to finish (watch it with Monitor, or come back to this later), then run ' +
91 'the command again. Do not reword the command to get past this check.',
92 ].join('\n'),
93 }
94 }).catch(($, e, next) => next(e))
95}
96