SLOPSHOPPER

heavy-job-guard

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

newguardtoastprocess
v0.1.0MITupdated 2026-10-08azoof-ahmed/claude-heavy-job-guard
A shopper browsing a rack in a slop shop
README

heavy-job-guard

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.

Install

/plugin install heavy-job-guard --marketplace azoof-ahmed/claude-heavy-job-guard

Answer y to add the marketplace, then pick a scope.

Options

Set them in /config, or in settings under pluginConfigs["heavy-job-guard"].options.

OptionDefaultWhat it does
patterntsc, vitest, jest, playwright, oxlint, eslint, xcodebuild, gradlew, bun/npm/pnpm/yarn [run] typecheck/test/build/lint/check, turbo run …, cargo build/test/checkRegex for a heavy job. It is matched against both the new command and every running process.
ignore--lsp, --watch, watch, language servers, tsserverRegex for running processes that match pattern but never finish on their own. They never block.
modeblockblock refuses the command; warn runs it and shows a toast.
subagentsallowdeny refuses every heavy job a subagent starts, so only the main agent runs them.

Develop

claude plugin validate .
claude plugin test .
claude --plugin-dir .

License

MIT

Source 1 files
hooks/register.ts 96 lines
1import 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