SLOPSHOPPER

devserver-limit

Stops a third dev server from starting

newguardtoastprocess
v0.1.0no licenseupdated 2026-10-16arjunkambj/claude-mods/devserver-limit
A shopper browsing a rack in a slop shop
README

Arjun's Claude Code mods

Four small mods for Claude Code. Install the ones you want.

ModWhat it does
codexClaude can hand a task to Codex (OpenAI's gpt-6.1-sol), like a code review or a bug hunt. It runs in the background so the chat isn't blocked.
imagegenClaude can make and edit images through Codex, and shows the finished images right in the chat.
devserver-limitStops Claude from starting a dev server when 2 are already running. Good for laptops with little memory.
done-soundPlays a chime when a task that took over a minute finishes.

Install

In a Claude Code terminal session, type one line per mod:

/plugin install codex --marketplace arjunkambj/claude-mods
/plugin install imagegen --marketplace arjunkambj/claude-mods
/plugin install devserver-limit --marketplace arjunkambj/claude-mods
/plugin install done-sound --marketplace arjunkambj/claude-mods

The first time, it asks to add the marketplace: press y. Then pick a scope (Enter picks "user", so it works in every project).

What you need

  • A Mac. imagegen and done-sound use macOS tools and sounds.
  • For codex and imagegen: the Codex CLI installed and signed in (codex login). Images count against your Codex plan.
Source 1 files
hooks/register.ts 66 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3const LIMIT = 2
4const STARTS = [
5  /\b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|preview)\b/,
6  /\b(next|astro|nuxt|remix|wrangler)\s+dev\b/,
7  /\bvite\b(?!\s+build)/,
8  /\bpython3?\s+-m\s+http\.server\b/,
9  /\b(uvicorn|flask\s+run|rails\s+s(erver)?)\b/,
10]
11const SERVER_PROCESSES = /^(node|bun|deno|python\S*|Python|ruby|php|workerd)$/
12
13type Server = { cwd: string; ports: string[] }
14
15// Listening dev processes, grouped by the folder they run in, so one server
16// that opens several ports (wrangler's node + workerd) counts once.
17async function runningServers($: EngineInterface): Promise<Server[]> {
18  const { stdout } = await $.process.run(['lsof', '-nP', '-iTCP', '-sTCP:LISTEN', '-Fcpn'], { timeoutMs: 5000 })
19  const ports = new Map<string, string[]>()
20  let pid = ''
21  let isServer = false
22  for (const line of stdout.split('\n')) {
23    if (line.startsWith('p')) pid = line.slice(1)
24    else if (line.startsWith('c')) isServer = SERVER_PROCESSES.test(line.slice(1))
25    else if (line.startsWith('n') && isServer) ports.set(pid, [...(ports.get(pid) ?? []), line.slice(1).replace(/^.*:/, '')])
26  }
27
28  const byCwd = new Map<string, string[]>()
29  for (const [id, list] of ports) {
30    const { stdout: out } = await $.process.run(['lsof', '-a', '-p', id, '-d', 'cwd', '-Fn'], { timeoutMs: 5000 })
31    const cwd = out.split('\n').find(l => l.startsWith('n'))?.slice(1) ?? `pid ${id}`
32    byCwd.set(cwd, [...new Set([...(byCwd.get(cwd) ?? []), ...list])])
33  }
34
35  return [...byCwd].map(([cwd, list]) => ({ cwd, ports: list }))
36}
37
38function describe(servers: Server[]): string {
39  return servers.map(s => `${s.cwd.split('/').pop()} (port ${s.ports.join(', ')})`).join('; ')
40}
41
42export const register: Register = on => {
43  on('tool.call', async ($, e, next) => {
44    const input = e as { tool: string; command?: string; name?: string; run_in_background?: boolean }
45    const isStart =
46      (input.tool === 'Bash' && STARTS.some(re => re.test(input.command ?? ''))) ||
47      (input.tool.endsWith('preview_start') && typeof input.name === 'string')
48
49    if (!isStart) {
50      return next(e)
51    }
52
53    const servers = await runningServers($)
54    if (servers.length >= LIMIT) {
55      $.ui.toast(`Blocked a dev server: ${servers.length} already running`)
56      return {
57        deny:
58          `Dev server limit: ${servers.length} dev servers are already running (${describe(servers)}). ` +
59          `The limit is ${LIMIT}. Reuse one of them, or stop one you started before starting another.`,
60      }
61    }
62
63    return next(e)
64  }).catch(($, e, next) => next(e)) // if the check itself fails, let the server start
65}
66