MCP server for managing background processes in Claude Code on Windows.
Claude Code on Windows (Git Bash / MSYS2) has a fundamental process management problem. Without this tool, Claude Code will repeatedly:
- Forget PIDs — starts a background process, loses the PID, then can't kill it later
- Try
taskkill— which MSYS/Git Bash flag-mangles (/T /F /PIDbecome Unix paths), failing every time - Try bash
kill— which operates on MSYS PIDs, not Windows PIDs, silently doing nothing - Blanket-kill by process name — desperate, it runs
taskkill /IM node.exe /For equivalent, killing ALL node processes including itself, other dev servers, and unrelated tools - Try
Get-NetTCPConnection— which hangs indefinitely on many Windows configurations - Lose output —
run_in_backgroundand&don't capture logs, so when something fails there's no way to diagnose it
This is not a one-time issue — Claude Code re-discovers these failures every session because it has no persistent memory of what works on Windows. Even with CLAUDE.md instructions saying "use PowerShell Stop-Process", Claude Code still needs to compose the right invocation, track PIDs manually, and handle edge cases like process trees and shell wrappers.
- SQLite database — all process metadata stored in
~/.bg-manager/bg-manager.db(WAL mode), shared across all projects - Web dashboard — live process monitoring at
http://127.0.0.1:7890with xterm.js terminal rendering, SSE live updates, and kill/cleanup actions - Automatic PID tracking — every
bg_runrecords the PID, command, intent, and timestamp - Reliable process killing —
bg_killuses PowerShellStop-Processwith recursive tree kill (children first, then parent). Nevertaskkill, never bashkill - Log capture with colors — all stdout/stderr goes to
~/.bg-manager/logs/, with PTY support for programs that needisatty()=truefor color output - Port management —
bg_port_checkusesnetstat -ano(the only reliable method on Windows), correlates PIDs with tracked processes by walking the parent chain - Cross-project visibility — all projects share one central database, viewable in the web dashboard
- Smart spawning — simple commands spawn directly (PID = actual process), complex commands (pipes,
&&) spawn via Git Bash with proper wrapper tracking. Command parsing usesshell-quoteso quoted paths with spaces ("C:/Program Files/node.exe") work correctly. - Synchronous runs —
sync_runwaits for a command to finish and returns its full output + exit code in one tool call. If the command exceeds its timeout, it's automatically converted to a background process — so long-running commands don't waste tokens on partial polling loops, they just flip toread_logfollow-up. - Python-friendly — automatically sets
PYTHONUNBUFFERED=1andPYTHONIOENCODING=utf-8for every process, and addsPYTHONUTF8=1when the command's executable looks like a Python interpreter (python,python3,py).
| Tool | Description |
|---|---|
bg_run(name, command, intent, triggers?, working_dir?, env?) |
Start a background process with auto-logging, PID tracking, optional triggers, custom working directory, and extra env vars |
sync_run(name, command, intent, timeout_sec?, working_dir?, env?, lines?, raw?, filter?, filter_regex?, max_bytes?) |
Run a command synchronously and return its captured output + exit code + duration when it finishes. Accepts the same log-filtering params as read_log (lines, raw, filter, filter_regex) so you can grep the output in a single call. If it exceeds timeout_sec (default 30, max 3600), the process is automatically converted to a background process and a partial-output response is returned with follow-up hints. The full captured log is persisted on disk — re-read with a different filter via read_log instead of re-running the command. |
bg_list() |
List all tracked processes with alive/dead status |
bg_kill(name) |
Kill a tracked process by name (full process tree) |
read_log(name, lines?, raw?, filter?, filter_regex?) |
Read and filter the log of any tracked process (both bg_run and sync_run). Tails the last N lines (default 50, max 1000). ANSI stripped by default (raw=true preserves). filter is case-insensitive substring by default; set filter_regex=true to switch to regex (e.g. ^FAIL, \\berror\\b, warn.*deprecated). Filter is applied before the line cap, and the response header shows how many lines matched. |
bg_port_check(port) |
Check what's listening on a port (with tracked process correlation) |
bg_port_kill(port) |
Kill whatever is listening on a port |
bg_cleanup() |
Remove dead entries from registry |
bg_status() |
Show dashboard URL, database path, and project info |
- Node.js (v18+) — required for
npx - Git for Windows — provides Git Bash, which bg-manager uses to spawn complex commands (pipes,
&&, redirects). Simple commands spawn directly without a shell.
Click the badge at the top of this README, or use the button below:
# Global (all projects)
claude mcp add -s user bg-manager -- cmd /c npx -y github:AndrewKirkovski/claude-code-bg-process-manager-windows
# Per-project only (creates .mcp.json in current directory)
claude mcp add bg-manager -- cmd /c npx -y github:AndrewKirkovski/claude-code-bg-process-manager-windowsAdd to your MCP client configuration:
{
"mcpServers": {
"bg-manager": {
"command": "cmd",
"args": ["/c", "npx", "-y", "github:AndrewKirkovski/claude-code-bg-process-manager-windows"]
}
}
}git clone git@github.com:AndrewKirkovski/claude-code-bg-process-manager-windows.git
cd claude-code-bg-process-manager-windows
npm install && npm run build
# Then add to Claude Code pointing to local build:
claude mcp add -s user bg-manager node /path/to/claude-code-bg-process-manager-windows/dist/index.js- Database:
~/.bg-manager/bg-manager.db(SQLite, WAL mode) - Logs:
~/.bg-manager/logs/<project-slug>-<name>.log - Web UI:
http://127.0.0.1:7890(auto-increments if port is taken)
All data is centralized in ~/.bg-manager/ — shared across all projects. Legacy .local/bg-processes.json registries are automatically migrated on first run.
The built-in web dashboard at http://127.0.0.1:7890 provides:
- Live process list grouped by project with ALIVE/DEAD status badges
- xterm.js terminal — full terminal emulation with ANSI color rendering
- SSE live updates — process status and log streaming update in real-time
- Kill/cleanup actions — manage processes directly from the browser
- Project filter — focus on a specific project's processes
- Dark/light themes — OneDark Pro (dark) / Bluloco Light (light) with CSS filter inversion
- Hash routing — direct links to processes via
/#/:project/:name
The dashboard starts automatically when the MCP server launches. Use bg_status to get the actual URL (port may increment if 7890 is taken).
If you're an AI agent using this MCP server, here's what to expect:
- Execution environment — env vars come from the IDE that spawned bg-manager (VSCODE_, CURSOR_, ELECTRON_*, etc.), not the user's interactive terminal. PATH may differ from what the user sees in their shell.
- Working directory & env vars — use
working_dirto set the process CWD andenvto pass extra environment variables. These are preferred over chainingcd /path && VAR=val && cmdin the command string:working_dirdefaults to the project root when omitted.envis merged with the base environment (does not replace it). - Spawn behavior — bg-manager never uses cmd.exe or COMSPEC. Simple commands (e.g.
node server.js,python app.py) spawn directly with no shell. Commands containing shell metacharacters (|,&,;,>) spawn via Git Bash (bash -c '...'). - Log contents — logs only contain stdout/stderr from the spawned process. Empty logs mean the process produced no output (wrong path, immediate crash, buffered output, or bad quoting).
- ALIVE vs DEAD — DEAD means the process exited, not necessarily that it failed. Exit codes are captured automatically (exit 0 = success, non-zero = failure). Short-lived commands (builds, probes, one-shot scripts) go DEAD as soon as they complete. Check
read_logfor the actual output. - Shell builtins —
echo,cd, etc. are not executables on Windows. Direct spawn fails for bareecho hello. Add a metacharacter to trigger Git Bash:echo hello && echo done, or use an actual executable:node -e "console.log('hello')". - Smoke test — to verify bg-manager works:
bg_run(name='probe', command='node -e "console.log(42)"', intent='test')thenread_log(name='probe'). Should show42.
bg_run supports an optional triggers parameter for monitoring process events. Trigger notifications are delivered piggybacked on the next tool response — when Claude calls any bg-manager tool, pending alerts are prepended to the result.
bg_run(
name: "server",
command: "node app.js",
intent: "start dev server",
working_dir: "C:/Projects/my-app", // optional: override CWD
env: { "NODE_ENV": "development" }, // optional: extra env vars (merged)
triggers: {
"notifyDead": true, // alert when process exits (default: true)
"notifyReady": true, // detect "ready"/"listening"/"started" patterns
"notifyPort": true, // detect localhost:PORT patterns in output
"logTriggers": [ // custom regex patterns to watch for
{ "pattern": "ERROR", "once": true },
{ "pattern": "warning.*deprecated" }
]
}
)How delivery works: MCP servers cannot push unsolicited messages. Instead, trigger events queue in memory and are prepended to the next tool response as a === TRIGGER ALERTS === block. This means Claude sees them the next time it calls bg_list, read_log, or any other bg-manager tool.
Use sync_run instead of redirecting output to a temp file when you just want a command's output back in one shot:
// Quick test run — returns full output + exit code when it finishes
sync_run(
name: "unit",
command: "npm test -- --run",
intent: "run the unit suite",
timeout_sec: 60, // default 30
filter: "FAIL", // grep the output in one call
lines: 50
)
// Regex filter — show the last 20 stack-frame-looking lines
sync_run(
name: "lint",
command: "pnpm eslint src",
intent: "lint check",
filter: "(error|^\\s+at )",
filter_regex: true,
lines: 20
)
// Long-running command — on timeout, bg-manager hands you control back
sync_run(
name: "heavy-build",
command: "npm run build:all",
intent: "full production build",
timeout_sec: 10
)
// => "sync_run \"heavy-build\" DID NOT FINISH within 10s — converted to background.
// Follow with: read_log name=\"heavy-build\" | stop with: bg_kill name=\"heavy-build\"
// Partial output (...): ..."Key properties:
- Same spawn engine as
bg_run— direct spawn for simple commands, Git Bash fallback for shell features, ConPTY for wippy. Sameworking_dir/envparams. Same Python env defaults. - Same log-filtering params as
read_log—lines,raw,filter,filter_regexwork identically in both tools, so you don't have to learn two APIs. Filter is case-insensitive substring by default; flipfilter_regex: truefor regex patterns like^FAILor\berror\b. - Full output is persisted on disk at
~/.bg-manager/logs/<slug>-<name>.logand the registry entry stays after completion. If your filter dropped too many lines, or you want to re-examine the output with different criteria — do not re-run the command. Callread_log(name=<same name>, filter=..., lines=..., filter_regex=...)to re-filter the already-captured log. This is the intended workflow:sync_runto execute,read_logto iterate. - Timeout → background conversion is transparent: the process keeps running, the log file keeps growing, and it shows up in
bg_listtaggedSYNC. Follow it withread_logexactly like abg_runprocess. - Output is trimmed on a line boundary to
max_bytes(default 256KB, max 1MB). Oversized output gets the last N bytes of the disk file, so you always see the most recent lines. Filter matches are counted against everything read from disk, andlinescaps the returned slice. - Piggyback events queued by triggers on other processes (e.g. a background server dying while
sync_runis waiting for a build to finish) are captured and prepended to thesync_runresponse — no events are lost during the wait. - Strip colors by default —
raw: false(default) strips ANSI codes from the returned output. Passraw: trueto keep them.
Shows bg-manager status at the start of every Claude Code session. The hook calls a script bundled with bg-manager — all logic lives in the script, so you can update it without changing settings.
Add to ~/.claude/settings.json (global) or .claude/settings.json (per-project):
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node \"<path-to-bg-manager>/scripts/session-start.cjs\"",
"timeout": 3000
}
]
}
]
}
}Replace <path-to-bg-manager> with the actual install path. For npm global installs, find it with npm root -g. For local dev: use the repo path directly.
Add the following to your project's CLAUDE.md (or global ~/.claude/CLAUDE.md) to ensure Claude Code always uses bg-manager instead of raw bash:
## Process Management — MANDATORY
- **ALWAYS use `bg-manager` MCP tools** (`bg_run`, `sync_run`, `bg_list`, `bg_kill`, `read_log`) for ALL process execution. NEVER use bash `&`, `run_in_background`, or redirect output to temp files.
- `bg_run` — start a long-lived background process (servers, watchers). Automatically captures PID, logs stdout/stderr to `~/.bg-manager/logs/`, tracks metadata (intent, command, start time).
- `bg_list` shows all tracked processes with alive/dead status — check what's running.
- `bg_kill <name>` kills by exact PID from registry — never kills unrelated processes.
- `read_log <name>` reads and filters the log of ANY tracked process (both `bg_run` and `sync_run` entries). Use instead of `tail -f` / `grep` on unknown files.
- **BEFORE starting ANY server/process**: run `bg_list` to check what's already running. `bg_kill` old one first.
- **BEFORE editing server code**: `bg_list`, `bg_kill` the server, then edit, rebuild, `bg_run`.
- NEVER blanket-kill by process name — always by exact name via `bg_kill`.
- NEVER use bash `&` directly — use `bg_run` (persistent) or `sync_run` (one-shot) instead.
## Capturing command output — use `sync_run`, not redirection — MANDATORY
- **NEVER** redirect command output to a temp file just to read it back. Patterns like `cmd > /tmp/out.log 2>&1 && cat /tmp/out.log`, `cmd | tee /tmp/out.log`, or `cmd 2>&1 > output.txt` are BANNED. They are unreliable on Windows/Git Bash (path handling, cp1252 encoding, partial flushes, orphaned temp files) and they throw away the exit code.
- **ALWAYS use `sync_run`** when you need the output AND exit code of a one-shot command — builds, tests, linters, scripts, `git` operations, `npm run …`, `pytest`, anything you'd normally want to "run and see what happened".
- `sync_run` runs the command, waits for it to finish, and returns the full captured stdout+stderr, exit code, and duration in a single tool call. No temp files, no redirection, no parsing ceremony.
- It uses the same spawn engine as `bg_run` — so `working_dir`, `env`, Python UTF-8 defaults, `.cmd`/`.ps1` shim fallback, and wippy ConPTY all work identically.
- **Timeout handling is automatic.** Pass `timeout_sec` (default 30, max 3600). If the command doesn't finish by then, bg-manager transparently converts it to a background process and returns a "did-not-finish" message with a `read_log name=<name>` hint. You don't lose the process, you don't lose partial output, and you don't have to guess durations up front — start with a modest `timeout_sec`, and if it rolls over, follow up with `read_log` as normal.
- **Filter the output in one call.** `sync_run` accepts the same `lines`, `filter`, `filter_regex`, and `raw` params as `read_log`. Use `filter` to grep for patterns like `"error"`, `"FAIL"`, or `"^WARN"` (with `filter_regex: true`). The response header shows how many lines matched, so you know if there's more.
- **Correct usage patterns:**sync_run(name="build", command="npm run build", intent="production build", timeout_sec=120) sync_run(name="lint", command="pnpm eslint src", intent="lint check", filter="error", lines=50) sync_run(name="pytest", command="python -m pytest tests/ -v", intent="unit tests", filter="FAIL|^E ", filter_regex=true, timeout_sec=300) sync_run(name="git-status", command="git status --short", intent="check working tree")
- **BANNED patterns** (never write these — use `sync_run` instead):
npm run build > /tmp/build.log 2>&1 && cat /tmp/build.log # BANNED pytest tests/ | tee /tmp/pytest.log # BANNED node script.js 2>&1 > .local/out.txt # BANNED
## Re-filter logs — don't re-run — MANDATORY
- **The full output of every `sync_run` is persisted to disk** at `~/.bg-manager/logs/<slug>-<name>.log` and the registry entry stays after completion (tagged `SYNC` in the dashboard). Until you run `bg_cleanup`, you can re-read the already-captured log any number of times.
- **If your first `sync_run` filter was too narrow, too broad, or just wrong — DO NOT re-run the command.** Re-running is slow, wastes tokens, and can give different results (flaky tests, non-deterministic builds, different timestamps). Instead, call `read_log` on the same name with a different `filter` / `lines` / `filter_regex`:
sync_run(name="test", command="npm test", intent="run tests", filter="FAIL", lines=10)
read_log(name="test", filter="Expected|Actual|AssertionError", filter_regex=true, lines=30)
read_log(name="test", lines=200) # no filter = full tail
- `read_log` and `sync_run` share the exact same filter engine, so anything you can do in one works in the other. Learn one, use both.
- The only reason to re-run a `sync_run` is if the source code / inputs actually changed. Before re-running, ask: "could I answer my question by re-filtering the existing log?" If yes, use `read_log`.
## Regex filtering — when substring isn't enough
- By default, `filter` is a case-insensitive substring match — `filter: "error"` catches `ERROR`, `errors`, `SomeError`, etc. Array form (`filter: ["error", "warn"]`) is OR-matched.
- For anchored or structured patterns, pass `filter_regex: true`. Each `filter` entry is then compiled as a case-insensitive regex:
read_log(name="build", filter="^FAIL", filter_regex=true) # anchored read_log(name="build", filter="\berror\b", filter_regex=true) # word boundary read_log(name="build", filter="warn.*deprecated", filter_regex=true) # pattern read_log(name="build", filter=["^E\s", "^F\s"], filter_regex=true) # array-OR regex
- Invalid regex returns a clear error — fix the pattern and retry, no re-run needed.
This is important because without these instructions, Claude Code will default to its built-in run_in_background which loses PID tracking and makes process management unreliable on Windows.
- Commands are parsed with
shell-quote, which handles quoted paths with spaces correctly ("C:/Program Files/node.exe" --version) - Simple commands (no pipes/redirects) are spawned directly — PID is the actual process
- Complex commands (with
&&,|,;, etc.) spawn via Git Bash — PID is the bash wrapper .cmd/.ps1shims (pnpm, npx, etc.) transparently fall back from direct spawn to shell mode on Windowsworking_dirsets the CWD for the spawned process (defaults to project root)envadds extra environment variables, merged on top of the inherited environment (userenvwins over every default)- All output (stdout + stderr) is redirected to
~/.bg-manager/logs/<project-slug>-<name>.log - Every process gets
PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8, andFORCE_COLOR=1by default - Commands whose first token is a Python interpreter (
python,python3,py, or any*.exevariant — detected via shell-quote-parsed tokens so quoted paths work) additionally getPYTHONUTF8=1 bg_runandsync_runshare a single internalspawnProcess()helper, so any fix to the spawn path benefits both tools automatically
- Uses PowerShell
Stop-Processwith recursive tree kill — kills children first, then parent - Never uses
taskkill(MSYS flag mangling) or bashkill(wrong PID namespace) - Port-based kill walks the parent PID chain to find tracked ancestors
- Uses
netstat -ano— the only reliable method on Windows - Never uses
Get-NetTCPConnection(hangs on some configs) - Correlates port PIDs with tracked processes by walking parent chain
MCP Client (Claude/Cursor) <--stdio--> bg-manager <--HTTP:7890--> Web Browser
|
v
~/.bg-manager/
bg-manager.db (SQLite, WAL mode)
logs/ (per-process log files)
Process icons created by Freepik - Flaticon
MIT
