A Claude Code SessionEnd hook that automatically kills dev-server processes Claude Code started as background tasks, so they don't linger after your session ends.
- Targets process trees Claude Code spawned as background tasks; ordinary dev servers started manually in a terminal are not candidates during normal session cleanup.
- Framework-agnostic: works with Astro, Next, Vite, Python, Go — anything. No command patterns to maintain.
- Interactive install: choose the hook scope (
user/project/local). - Minimal dependencies:
commander(CLI) and@clack/prompts(interactive prompts). macOS / Linux.
npm install -g claude-dev-cleanup
# or
npx claude-dev-cleanup installThen install the hook (run without arguments to choose the scope interactively):
claude-dev-cleanup install # interactive: user / project / local
claude-dev-cleanup install --scope user # user scope: ~/.claude/settings.json, applies to all projects
claude-dev-cleanup install --scope project # project scope: .claude/settings.json, committed to the repo
claude-dev-cleanup install --scope local # local scope: .claude/settings.local.json, this machine only| Command | Behavior |
|---|---|
install [--scope] |
Install the SessionEnd hook (interactive menu or --scope) |
uninstall [--scope] |
Remove the hook |
clean [--list] [--force] |
Run cleanup once; orphan mode lists by default and requires --force to terminate |
check |
Show install status for all three scopes |
Claude Code runs background tasks under a shell-snapshots zsh wrapper whose parent is the claude process. At session end, the hook finds this session's claude process, collects its marker-wrapped subtrees, and terminates them.
- Per-session isolation — only the ending session's background tasks are killed; other open sessions are untouched.
- Claude's own helpers (MCP servers, transient subprocesses) hang directly off
claudewithout the wrapper, so they are never touched. - Orphan mode — if you run
cleanmanually after Claude was force-killed (noclaudeancestor left), wrapper subtrees are found by the marker for the current user. Wrappers that still belong to another live Claude session are excluded. Orphan mode only lists matches unless you pass--force. - A normal dev server started manually in a terminal (
terminal → login shell → pnpm → astro) has no Claude wrapper ancestry and is not a candidate during session cleanup. As with any marker-based fallback, inspect orphan-mode output before using--force.
Before signaling a candidate, the script records and rechecks its process start time. It sends TERM from the deepest child toward the wrapper, waits briefly, then uses KILL only if the same process is still running. This reduces PID-reuse and parent-exit races.
| Env var | Default | Purpose |
|---|---|---|
CLAUDE_DEV_CLEANUP_MAX_DEPTH |
100 |
Ancestor/descendant walk limit; must be an integer from 1 to 1000 |
- Fires on a graceful session end (
/exit, closing the session). Force-killing Claude Code skips all hooks — runclaude-dev-cleanup clean --forcemanually. - Everything in a background task's subtree is terminated, including services the task spawned (databases, tunnels, SSH, containers). Run
cleanorclean --listfirst before confirming orphan cleanup withclean --force. - The marker (
shell-snapshots) is a Claude Code internal detail; if Claude Code changes internals, detection may need updating. --listshows pid / ppid / executable only — command arguments (which may contain tokens) are never printed.- The script refuses to run as root. As a normal user, orphan discovery is limited to your own processes.
MIT