Last week my Mac crashed. Not from a video render or a compile job. From using Claude Code the wrong way for months.
Last Tuesday afternoon my machine ground to a halt. macOS started killing processes — Teams, Chrome, OneDrive — just to stay alive. I dug into the system crash logs expecting something exotic. What I found was eight simultaneous Claude Code processes consuming over 50 GB of combined memory, with only 239 MB of RAM left free and 6.8 GB compressed in swap. The system was on life support.
The cause wasn’t a bug in Claude Code. It was a habit I’d built without realizing it was a habit.
The pattern that broke my machine
I run Claude Code the way most power users run it: always open, always available, one or two terminal windows with active sessions running throughout the day. When I finish a task, I type /clear and move on to the next thing. Clean slate, fresh context, keep working.
That’s the wrong mental model.
/clear clears the conversation context — the messages Claude can see. It does not close the process. The Node.js process keeps running. Every MCP server that was connected stays connected. Any memory the process has accumulated in its heap stays there. You’ve cleared the whiteboard, but the room is still full of people.
I had six MCP servers configured globally: Slack, internal productivity tools, email, Perplexity, and Playwright. Every Claude Code session starts all six as background processes. Playwright alone — which loads a headless browser engine — runs at 300–600 MB even when idle. Multiply that across the several sessions I’d left running across terminal tabs, and the math stops working quickly.
The crash logs identified the culprit precisely: the process named 2.1.92 (Claude Code v2.1.92, the version I was on that day) appeared five times in the top memory consumers, each instance between 7 and 11 GB of resident memory. Three separate Node.js processes added another 13 GB on top. My Mac didn’t fail — it was doing exactly what macOS is designed to do when RAM runs out. It just happened to take down everything else with it.
What /clear actually does
When you run /clear in Claude Code, you’re telling the model to forget the conversation. That’s it. The process that’s running Claude Code, the MCP servers it spawned, the filesystem handles it has open, the heap memory it’s accumulated reading files and processing tool outputs — none of that goes away.
Think of it like a browser tab. Clearing your browser’s history doesn’t close the tab. The tab is still consuming memory, the JavaScript is still running, the connections are still open. You’ve just cleared what’s visible.
/exit closes the tab. That’s the operation that actually frees resources.
Long-running sessions also accumulate context heap in ways that aren’t obvious. Every file Claude reads, every tool output, every bash command result stays resident in the Node.js process memory for the life of the session. After hours of heavy work — reading dozens of files, running code, calling MCP tools — a single session can grow to 6 GB or more. That’s not a leak. That’s just how a stateful process works when you keep feeding it work.
The right pattern
The fix is straightforward once you name it: one session per task, exit when done.
Not per day. Per task. Starting a new Claude Code session costs about three seconds. The overhead is negligible. What you get in return is a clean process, fresh MCP connections, and a predictable memory footprint.
For genuinely long single tasks — research that spans hours, a coding session that goes deep — /compact is the right tool mid-session. It compresses the conversation history while preserving the current context and key findings. That’s different from /clear: compact summarizes and shrinks; clear discards. On a long task, compact every 90–120 minutes keeps the session lean without losing the thread.
For switching contexts — going from KB editing to Slack analysis to a coding task — exit and relaunch. The context switch is the natural break point. The few seconds it takes to restart is nothing compared to what accumulates if you don’t.
The maximum I’d run simultaneously: two sessions. One active, one in the background. Any more than that on a 32 GB machine with Teams, Chrome, OneDrive, and the usual enterprise software running is borrowing against tomorrow’s crash.
One more lever: scope heavy MCP servers to projects instead of global. If you have Playwright configured globally, it starts a headless browser engine for every session — including the ones that never touch a browser.
The way project-scoped MCPs work: Claude Code reads your working directory at launch time and loads any .mcp.json it finds there, on top of your global config. That’s the only moment that matters — not when the agent later reads files in different folders, but where you were standing when you typed claude. Launch from a repo that has a .mcp.json with Playwright in it, and Playwright starts. Launch from anywhere else, and it doesn’t.
So the fix is: remove Playwright from global config, drop a .mcp.json in the repos that need browser automation, and leave everywhere else clean. The command to add a project-scoped MCP is claude mcp add --scope project. For servers you use everywhere (Slack, email), global is fine. For anything that loads a heavy runtime — browser engine, database, local model — project scope is the right default.
The infrastructure was already right — and you can build it too
Here’s the part I find interesting in retrospect: I’d built exactly the right infrastructure for proper session hygiene. I just wasn’t using it.
Claude Code has a hooks system that most people don’t know exists. You can attach shell commands to lifecycle events: SessionStart, Stop, PostCompact, PostToolUse. These run outside the model — they’re just bash, executed by the Claude Code process itself. No API calls, no special permissions. You configure them in ~/.claude/settings.json.
The pattern I built uses three hooks working together.
Hook 1 — Stop: save the thread on exit
When a session exits, this hook reads the session’s transcript file (a JSONL file Claude Code maintains automatically), extracts the last 50 user/assistant message pairs, filters out noise, and writes a clean markdown file to ~/.claude/session-states/<session-id>.md. One file per session. Every exit overwrites it — no duplicates, no append accumulation.
The logic that does the work is a short Python script:
import json, datetime
lines = open(TRANSCRIPT).readlines()
out = []
for line in lines:
obj = json.loads(line.strip())
msg = obj.get('message', {})
role = msg.get('role', '')
if role not in ('user', 'assistant'):
continue
# Extract text content
content = msg.get('content', '')
text = ''
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get('type') == 'text':
text = block.get('text', '').strip()
break
elif isinstance(content, str):
text = content.strip()
# Filter noise
if not text: continue
if text.startswith('<'): continue # system XML injections
if text.startswith('Base directory for this skill:'): continue # skill content
limit = 300 if role == 'user' else 2000
out.append(role.upper() + ': ' + text[:limit])
if out:
with open(STATE, 'w') as f:
f.write('## Session Thread (updated ' + datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + '\n\n')
f.write('\n---\n'.join(out[-50:]))
This gets embedded as an inline command in ~/.claude/settings.json under the "hooks" → "Stop" key, wrapped in the bash that extracts the session ID and transcript path from the hook payload. The full settings.json entry with escaping is available in the gist linked at the end of this post.
A few things worth noting: the hook runs async: true so it doesn’t block the exit. The transcript path comes from Claude Code’s hook payload automatically — you don’t track it yourself. The two noise filters matter in practice: < catches system XML that Claude Code injects as context, and Base directory for this skill: catches skill framework content that appears as user turns if you use Claude Code’s plugin/skill system.
Hook 2 — SessionStart: reload on launch
When a new session starts, this hook checks if a state file exists for the current session ID. If it does, it prints the contents to stdout — which Claude Code reads as context injected before the first user message. You pick up exactly where you left off.
"SessionStart": [{
"hooks": [{
"type": "command",
"command": "SID=$(cat | jq -r '.session_id // empty' 2>/dev/null); if [ -n \"$SID\" ] && [ -f ~/.claude/session-states/${SID}.md ]; then echo '=== Resumed from previous session ==='; cat ~/.claude/session-states/${SID}.md; echo '=== End of previous session state ==='; fi"
}]
}]
This only fires when a state file exists for that session ID — which only happens if you’d previously exited a session with the same ID. In practice, new sessions get new IDs, so this is specifically for the /claude --continue resume flow.
Hook 3 — PostCompact: capture the summary
When /compact runs mid-session, Claude Code generates a summary of the conversation history and replaces the full thread with it. This hook intercepts that summary and writes it to the same session state file. So if you compact mid-session and then exit, the state file contains the compact summary rather than a raw thread slice.
"PostCompact": [{
"hooks": [{
"type": "command",
"command": "mkdir -p ~/.claude/session-states && INPUT=$(cat); SID=$(echo \"$INPUT\" | jq -r '.session_id // empty'); SUMMARY=$(echo \"$INPUT\" | jq -r '.compact_summary // empty'); if [ -n \"$SID\" ] && [ -n \"$SUMMARY\" ]; then printf '%s' \"$SUMMARY\" > ~/.claude/session-states/${SID}.md; fi"
}]
}]
What this gives you
The three hooks together implement what I’d call continuity without persistence. Each session is a fresh process — clean heap, fresh MCP connections, predictable memory. But you don’t lose the thread. Exit after a task, relaunch for the next one, and the relevant context comes back automatically.
The session breadcrumbs file (~/.claude/session-breadcrumbs.jsonl) is a side effect — a lightweight audit log of when sessions started and stopped and which working directories they ran from. I use it occasionally to retrace what I was doing. It’s one JSON object per line, easy to query with jq.
This pattern works for any agent that has a lifecycle hook system and can read/write files. The specific commands are Claude Code-flavored, but the architecture — save state on exit, reload on start, intercept compaction — is replicable in any agentic tool that gives you lifecycle hooks. Cursor, Windsurf, any custom agent running in a loop: if you can attach a shell command to “session ended”, you can build this.
This isn’t just behavior — there are real leaks underneath
Before I close: I want to be precise about what’s behavioral and what’s a bug, because both are at play.
The session accumulation I described — heap growth from file reads, tool outputs, MCP responses — is expected behavior. It’s how stateful processes work. Exiting between tasks is the right mitigation regardless of anything else.
But there are also confirmed memory leaks in Claude Code that compound the problem. The GitHub issue tracker has multiple open bugs with reproductions: ArrayBuffers from streaming responses that aren’t released between turns, growth rates measured at 480 MB to 6 GB per hour depending on workload intensity. There’s also a reported macOS-specific kernel memory leak. These are known to the team and unresolved as of the time I’m writing this.
The interaction between behavioral accumulation and actual leaks is what made my session numbers so extreme. A well-behaved long session might grow to 2–3 GB. A session hitting the ArrayBuffer leak at the same time grows much faster. Eight of them running simultaneously tips the system.
The behavioral fix — exit between tasks — is within your control today. The leak fixes are coming. Both matter.
Two symptoms, one cause
There’s a related phenomenon that gets discussed separately but has the same root: “why does Claude get worse the longer the session runs?” Responses get vaguer, it starts ignoring earlier instructions, outputs feel less precise. This is the context window filling up — not RAM, but the model’s attention span. When the conversation history gets long enough, earlier context gets pushed out or diluted.
Both problems — degraded outputs and system instability — come from the same habit: treating Claude Code sessions as persistent workspaces instead of task-scoped processes. The context window filling is the early warning sign. The memory crash is what happens when you ignore it long enough.
If you’ve noticed Claude getting worse during long sessions and chalked it up to the model being inconsistent, you were seeing the context symptom. What happened to my machine last week was the memory symptom of the same underlying pattern.
What this changes
Most productivity advice about AI coding tools focuses on prompting — how to phrase requests, how to structure tasks, how to get better outputs. Almost none of it talks about the operational layer: how to run the tool sustainably over an eight-hour workday without degrading your machine.
I searched specifically for practitioner writing on session management, /clear vs /exit, and memory hygiene for heavy Claude Code users before writing this. I found GitHub issues and official troubleshooting docs that say “restart between major tasks.” I didn’t find anyone explaining why — or what the right architecture looks like to make exiting actually sustainable without losing your place.
Session hygiene is a real discipline. It’s boring. It doesn’t show up in demos. But it’s the difference between a tool that works well all day and one that works well until it doesn’t.
The mental shift is small: stop thinking of Claude Code as a persistent assistant that lives in a terminal. Start thinking of it as a process you launch for a task and close when done. The assistant’s memory is in the files, the session states, the CLAUDE.md context — not in the process heap.
The question I’m sitting with: how many other people running Claude Code heavily have hit this and just blamed the hardware?