Performance

Memory Leak Hunter

Find what's holding a reference it shouldn't -- from a heap snapshot diff or growth pattern, not a guess about 'probably the cache.'

When to use this

  • Memory usage grows over time in a long-running process and doesn't plateau.
  • A heap snapshot/profile is available and needs interpreting.
  • Not for a one-off high-memory spike that's expected behavior (e.g. processing a large batch) -- this is for unbounded growth, not a single high-water mark.

The skill file

Copy this verbatim. It's written in the SKILL.md format (YAML frontmatter + markdown instructions) that Claude Code, and increasingly other agent tools, read directly.

SKILL.md
---
name: memory-leak-hunter
description: Diagnose unbounded memory growth in a long-running process by analyzing heap snapshots or growth patterns to find what's retaining references it shouldn't, rather than guessing at the cause. Use for confirmed memory growth over time, not a single expected high-water mark.
---

# Memory Leak Hunter

## Confirm it's actually a leak

Growth that plateaus (e.g. cache filling to its configured max, or normal
warm-up) isn't a leak. A leak is memory that grows without bound over
time under steady-state load. Confirm the pattern with actual measurements
over a long enough window before hunting for a cause.

## Find the retainer

1. If heap snapshots are available at two points in time, diff them --
   what object type/count grew, and (if the tool supports it) what's
   holding a reference to those objects (the retainer path).
2. Common real-world causes to check first: event listeners registered
   but never removed, a cache/map with no eviction policy or size bound,
   closures capturing more than they need (especially in long-lived
   callbacks/timers), a connection/resource pool that isn't releasing
   entries, global/module-level collections that only grow.
3. For a language with manual memory management, also check for missing
   `free`/`dispose`/`close` calls on the growing resource type.

## Verify the fix

After a candidate fix, re-run under the same steady-state load for long
enough to confirm growth actually stopped (or plateaued at a sane bound),
not just that it grew more slowly.

## Output

The retained object type, the retainer chain/reason it's not being
released, the fix, and confirmation (or a plan to confirm) that memory
plateaus after the fix under sustained load.

Installing it elsewhere

The frontmatter/body split above is Claude Code's convention. Here's how to carry the same instructions into other tools:

Claude Code
.claude/skills/memory-leak-hunter/SKILL.md

Save the file below verbatim (frontmatter included) at that path, project-local or in ~/.claude/skills/ for a user-level skill. Claude Code loads the name/description pair to decide when to pull it in, or you invoke it directly as /memory-leak-hunter.

Cursor
.cursor/rules/memory-leak-hunter.mdc

Convert the YAML frontmatter to Cursor's rule format (description, globs, alwaysApply: false) and keep the markdown body as the rule content. Cursor surfaces it by description match, same idea as Claude Code's auto-load.

Codex CLI / Copilot
AGENTS.md

Codex CLI (and increasingly other agentic CLIs) read AGENTS.md at the repo root as always-on instructions. Paste the markdown body under a heading like ## {title}; for GitHub Copilot's coding agent, the equivalent file is .github/copilot-instructions.md.

Windsurf
.windsurfrules

Append the markdown body to .windsurfrules at the repo root. Windsurf treats the whole file as always-on context, so keep only the instructions you want applied on every request.

Where this goes wrong
  • Treating a plateauing cache as a leak instead of confirming genuinely unbounded growth.
  • Guessing at the cause ("probably event listeners") without a heap diff to confirm which object type is actually growing.
  • Declaring victory after a fix reduces growth rate without confirming it actually plateaus over a long enough test window.