Skip to content

feat(plugin): add tmp-cleanup plugin for /tmp disk leak mitigation#39977

Open
zvndev wants to merge 1 commit intoanthropics:mainfrom
zvndev:plugin/tmp-cleanup
Open

feat(plugin): add tmp-cleanup plugin for /tmp disk leak mitigation#39977
zvndev wants to merge 1 commit intoanthropics:mainfrom
zvndev:plugin/tmp-cleanup

Conversation

@zvndev
Copy link
Copy Markdown

@zvndev zvndev commented Mar 27, 2026

Summary

Adds a tmp-cleanup plugin that automatically prunes two categories of leaked files from Claude Code's /tmp directory:

  • Task .output files — the primary disk killer. Background tasks and subagents produce .output files with no size cap. Users have reported 46 GB+ per file, 95 GB+ per session, and up to 740 GB total consumption.
  • CWD tracking files (claude-*-cwd) — small 22-byte files that accumulate at ~174/day without cleanup on crash.

Why a plugin?

The root cause is in the compiled runtime (no size cap on output capture, no cleanup on session end). Until that's fixed upstream, this plugin provides immediate relief via the existing hook system — no binary patches, no external dependencies, runs on every session start.

Approach

A SessionStart hook with a three-pass cleanup strategy:

Pass Target Condition Default
1a .output files Size exceeds per-file limit 100 MB
1b .output files Age exceeds max age 72 hours
1c .output files Total remaining exceeds budget (largest first) 5 GB
2 claude-*-cwd files Age exceeds max age + UID ownership check 24 hours

All thresholds configurable via CLAUDE_TMP_CLEANUP_* environment variables. Set CLAUDE_TMP_CLEANUP_DISABLED=1 to opt out entirely.

Safety

Concern Mitigation
Symlink traversal lstat (not stat) + isSymbolicLink() check at every directory and file level
Path traversal All paths resolve()d and verified to start with the tmp base directory
Cross-user deletion CWD files checked for UID ownership before deletion
Non-regular files Only isFile() entries are deleted — never directories or special files
Windows Exits cleanly when process.getuid() is unavailable
Session blocking All filesystem errors caught silently — hook never blocks session start
Timeout 15-second timeout configured in hooks.json

Comparison with existing PRs

This PR #33015 #30721 #34545
Targets .output files (the 740 GB problem) Yes No No Yes (cli.js patch)
Targets cwd files Yes Yes Yes No
Plugin-based (no binary patches) Yes Yes Yes No
Language Node.js (zero deps) Python Python JS (patches minified cli.js)
Hook event SessionStart PostToolUse + Stop Stop N/A
Catches crash leftovers Yes (runs on next start) Partial (Stop doesn't fire on crash) No (Stop only) N/A
Symlink protection Yes Yes No N/A
Configurable thresholds Yes (5 env vars) No No No
Cross-platform safe Yes (Windows exit) Yes (tempfile.gettempdir) No No

Files

plugins/tmp-cleanup/
├── .claude-plugin/
│   └── plugin.json           # Plugin metadata (v1.0.0)
├── hooks/
│   ├── hooks.json            # SessionStart hook with 15s timeout
│   └── cleanup-tmp.mjs       # Cleanup logic (Node.js, zero dependencies)
└── README.md                 # Documentation with safety details and config

Closes

Test plan

  • Script exits cleanly when no tmp directory exists
  • Script exits cleanly when tmp directory is empty
  • Script exits cleanly on platforms without process.getuid() (Windows)
  • Syntax validation passes (node -c)
  • Oversized .output files are deleted (create >100 MB test file)
  • Old .output files are deleted (touch file with old mtime)
  • Total budget pruning removes largest files first
  • Symlinks in tmp directory are not followed
  • CWD files matching claude-XXXX-cwd pattern are cleaned
  • CWD files owned by other users are not deleted
  • CLAUDE_TMP_CLEANUP_DISABLED=1 skips all cleanup
  • Environment variable overrides work for all thresholds

🤖 Generated with Claude Code

Task .output files in /tmp/claude-{uid}/ grow without bound when
background tasks produce infinite output — interactive prompts in
non-interactive shells, verbose builds, or runaway processes. Users
have reported 46 GB+ per file, 95 GB+ per session, and up to 740 GB
total disk consumption. CWD tracking files (claude-*-cwd) also
accumulate at ~174/day without cleanup.

This plugin adds a SessionStart hook that automatically prunes both
file types using a three-pass strategy:

  1. Delete .output files exceeding 100 MB (configurable)
  2. Delete .output files older than 72 hours (configurable)
  3. If remaining total exceeds 5 GB, prune largest files first
  4. Delete stale claude-*-cwd files older than 24 hours

Safety measures:
  - Symlink-safe: lstat + isSymbolicLink checks at every level
  - Path validation: resolved paths verified within tmp base
  - Ownership check: cwd files only deleted if UID matches
  - Regular files only: never deletes directories or specials
  - Cross-platform: exits cleanly on Windows (no getuid)
  - All errors caught silently — never blocks session start

All thresholds configurable via CLAUDE_TMP_CLEANUP_* env vars.
Set CLAUDE_TMP_CLEANUP_DISABLED=1 to opt out entirely.

Closes anthropics#26911, closes anthropics#39909, closes anthropics#8856

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yurukusa
Copy link
Copy Markdown

This is already solvable with a Stop hook today — no plugin needed:

find /tmp -maxdepth 1 -name 'claude-*' -type f -mmin +60 -delete 2>/dev/null
find /tmp -path '*/claude-*/tasks/*.output' -size +100M -delete 2>/dev/null
REMAINING=$(du -sh /tmp/claude-* 2>/dev/null | tail -1)
[ -n "$REMAINING" ] && echo "NOTE: Remaining /tmp/claude files: $REMAINING" >&2
exit 0

Add to settings.json under hooks.Stop. This runs every time a session ends.
For the 740GB+ scenarios, you can also add a Notification hook that checks disk usage periodically:

USAGE=$(df /tmp --output=pcent 2>/dev/null | tail -1 | tr -d ' %')
[ "$USAGE" -gt 90 ] && echo "⚠ /tmp disk usage: ${USAGE}%" >&2
exit 0

Install both: npx cc-safe-setup --install-example temp-file-cleanup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants