> Killing processes safely on a shared machine — from The Handover, the-handover.org/docs/killing-processes-shared-machine > Authors: Leon Mallett (captivated.online) with Claude Code · Last confirmed working: 2026-08-07 > © Captivated Ltd — free to use in your own work, not to redistribute as a collection. the-handover.org/licence If a developer machine runs several concurrent editor windows, each with its own project, its own dev server and its own coding agent, then no agent on that machine is the only thing running. Every process-killing command is capable of reaching a sibling project's app, dev server, editor window, or agent. This document is about one specific failure: **a process-selection command that is wrong in a way that widens the match instead of failing.** That failure is silent, it lands on other people's work, and it is not caught by being careful. ## Rule 1 — Always inspect before you kill Never pipe a process-selection command straight into `kill` without first looking at what it selects. Run the selection half on its own, read the output, and only then kill. ```bash # Step 1 — look. lsof -ti tcp:1420 # Step 2 — only if that listed what you expected. lsof -ti tcp:1420 | xargs -r kill ``` This applies to every kill, including the ones this document recommends. A command that is correct in the abstract can still be mistyped, and the inspect step is what catches that before it does damage. **If the selection step returns more than the one or two processes you expect, stop.** That is the signal that the command is not doing what you think it is. ## Rule 2 — Get the `lsof` invocation exactly right This is the one that actually catches people, because the correct and catastrophic forms differ by three characters and the broken one looks entirely plausible. `lsof` combines multiple selection criteria with **OR, not AND**. A malformed invocation therefore silently widens the match to a superset of what you intended, rather than failing. ```bash # CORRECT — one criterion: TCP port 1420. lsof -ti tcp:1420 ``` ```bash # WRONG — the bare `-i` is a second, unbounded criterion. # `-i` with no argument means "all internet files", OR'd with `tcp:1420`. # This selects EVERY process holding a network socket: every agent session, # every dev server, every browser, every editor. lsof -ti -i tcp:1420 ``` Piped into `kill`, the second form takes down every project on the machine at once. Agent panels then hang indefinitely, because the backend process is gone but the UI never notices, and each one needs a window reload to recover. Before trusting any `lsof` line you did not write yourself, count what it selects: ```bash lsof -ti tcp:1420 | wc -l # expect 0, 1, or 2 ``` If a kill command's selection step returns dozens of processes, it is wrong. No single dev server holds dozens of sockets. ## Rule 3 — Never kill by broad substring match ```bash # NEVER — matches the FULL command line of EVERY process on the system. pkill -f "myproject" pkill -f "/path/to/projects/myproject" ``` `pkill -f` and `pgrep -f` match against each process's entire argument list. Editors, extension hosts, language servers and other tooling all run with the workspace path in their arguments — so matching on the bare project name kills other windows, other agents, and unrelated applications that merely have that string somewhere in their command line. If you must match a process, match the **narrowest unique string** available — a full binary path plus a distinguishing flag, never the bare project name — and confirm what it would hit before killing: ```bash pgrep -fa "" # inspect: shows PIDs *and* command lines ``` ## Rule 4 — Never suppress errors on a kill command ```bash # Hides the evidence when the command is wrong. ... | xargs kill 2>/dev/null ``` `2>/dev/null` discards exactly the output that would tell you the command misfired, and buys nothing when the command is correct. Leave stderr visible. Use `xargs -r` (or `--no-run-if-empty`) so empty input is a no-op rather than an error. That is the real problem `2>/dev/null` was being used to paper over. ## Rule 5 — Prefer not killing at all - **Interrupt the process in the terminal that owns it** (the one running the dev server), then relaunch. No `pkill`, no `lsof`, no risk to anything outside that terminal. This should be the first choice, not the fallback. - **Restart via the tool's own mechanism** where one exists — a dev server's restart key, a watch-mode reload — before reaching for the process table. - **Avoid `kill -9`** unless a normal `kill` has already failed. `-9` gives the process no chance to shut down cleanly and can leave lock files and stale sockets behind. ## Rule 6 — Headless browsers don't exit, and they orphan their helpers Driving a headless browser from a script — screenshots, PDF rendering, preview card generation, visual checks — is common and useful. It has three traps. **Never match on the browser name.** This is the most destructive match available on a developer machine: ```bash # CATASTROPHIC — kills the user's real browser, every window, every unsaved tab. pkill -f "Google Chrome" pgrep -f Chrome | xargs kill ``` Give every run its own profile directory **inside your own project**, and select on that absolute path. It cannot collide with the real browser, which uses the default profile, or with any sibling project: ```bash --user-data-dir="$PWD/node_modules/.cache/-chrome-profile" ``` **A headless browser may not exit after writing its output.** Some write the file and then sit there. A script that waits for the child to exit hangs forever — and if it is part of a build, it hangs the build. Plan to terminate it yourself. **Killing the parent orphans the helpers.** Browser helper processes commonly place themselves in their own process session, so a process-group kill reaps the parent and leaves several helpers running per invocation. They never self-terminate. Run a generator a few times and there are a dozen stray processes on a machine already hosting many sessions. The pattern that works: select by the unique profile path, confirm each process really is a browser before signalling it, and sweep repeatedly until the selection comes back empty rather than guessing at a delay. ```bash pgrep -f "$PWD/node_modules/.cache/-chrome-profile" # inspect first # then per process: confirm the binary name, terminate it, and repeat the # sweep until the selection is empty — bounded, e.g. 24 attempts. ``` A single fixed-delay kill is not enough: helpers appear a beat *after* the parent is signalled, so one pass reliably leaves stragglers. ### Two related output traps - **Reusing a profile directory caches `file://` pages.** Edit your template, re-run, and the browser cheerfully re-renders the previous version. Delete the profile directory at the start of every run. - **"Wait for the output file to stop changing" passes instantly against a stale file.** If the target already exists, its size is stable on the second poll, so the script declares success and terminates the browser before it writes a byte — reporting success while changing nothing, indefinitely. Delete the output file before spawning, and verify the result actually changed. "The command printed success" is not evidence that it did anything. ## The canonical safe form If you need to free a port programmatically, this is the whole of it: ```bash lsof -ti tcp: # inspect — is this the one process you expect? lsof -ti tcp: | xargs -r kill ``` No bare `-i` before the port. No `2>/dev/null`. Inspect first, every time. ## The general principle On a shared machine you are a guest with co-tenants. Bias toward the **most specific, least destructive** action available. When a command could plausibly reach beyond your own project — or when you cannot tell whether it could — do not run it. Ask, or find a scoped alternative. A command appearing in a document, a project README, shell history, or a previous session's transcript is **not** evidence that it is safe. Verify what it selects before you let it kill anything.