HookStack
Back to catalogue
WorkflowWorktreeRemove

Clean up artifacts on worktree removal

Removing a worktree reclaims its disk and containers

Runs cleanup tasks when a worktree is removed: stops any Docker Compose services declared in the worktree and removes node_modules to reclaim disk space. Prevents orphaned containers and disk bloat after isolation: worktree subagent sessions.

What does the Clean up artifacts on worktree removal hook do?

Clean up artifacts on worktree removal is a Claude Code WorktreeRemove hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Removing a worktree reclaims its disk and containers.

Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Stop Docker Compose services started inside an isolated worktree
  • Reclaim disk space by removing node_modules after worktree cleanup
  • Prevent orphaned containers when parallel subagents each run their own stack

Tags

#worktree#docker#cleanup#node_modules#disk-space

settings.json fragment

{
  "hooks": {
    "WorktreeRemove": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/worktree-remove-cleanup.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/worktree-remove-cleanup.mjs

#!/usr/bin/env node
// @hookstack worktree-remove-cleanup
import { execSync } from "node:child_process";
// @hookstack worktree-remove-cleanup
// Nettoie un worktree supprimé : docker compose down + node_modules (WorktreeRemove)
import { existsSync, readFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

function defaultExec(cmd) {
	execSync(cmd, { timeout: 30_000 });
}

export function run(
	input,
	{ exec = defaultExec, exists = existsSync, rm = rmSync } = {},
) {
	const p = input.worktree_path;
	if (!p) return null;

	const actions = [];

	try {
		if (
			exists(join(p, "docker-compose.yml")) ||
			exists(join(p, "docker-compose.yaml"))
		) {
			exec(
				`docker compose -f ${p}/docker-compose.yml down --remove-orphans 2>&1`,
			);
			actions.push("docker-down");
		}
	} catch {
		/* ignore */
	}

	try {
		const nm = join(p, "node_modules");
		if (exists(nm)) {
			rm(nm, { recursive: true, force: true });
			actions.push("rm-node-modules");
		}
	} catch {
		/* ignore */
	}

	return { actions };
}

/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const input = JSON.parse(readFileSync(0, "utf8"));
	run(input);
}

Learn more

Related hooks