Skip to content

feat: Add tmp-cleanup plugin to mitigate /tmp file leak (#8856)#33015

Open
qbit-glitch wants to merge 1 commit intoanthropics:mainfrom
qbit-glitch:fix/tmp-cleanup-plugin-issue-8856
Open

feat: Add tmp-cleanup plugin to mitigate /tmp file leak (#8856)#33015
qbit-glitch wants to merge 1 commit intoanthropics:mainfrom
qbit-glitch:fix/tmp-cleanup-plugin-issue-8856

Conversation

@qbit-glitch
Copy link
Copy Markdown

Summary

Adds a new tmp-cleanup plugin that mitigates #8856 — the accumulation of orphaned /tmp/claude-{hex}-cwd files created by the Bash tool.

As documented in #8856, every Bash tool invocation creates a temporary file to track the working directory, but fs.unlink is never called afterward. Users report 174+ files per day accumulating indefinitely until systemd-tmpfiles-clean runs (Linux) or manual cleanup.

This plugin does not patch the core runtime (which is closed-source). It provides a hook-based workaround that cleans up stale files automatically.

What it does

Two hooks, sharing a common module:

Hook Event Behavior
tmp_cleanup_post_tool.py PostToolUse (Bash only) Deletes matching files older than 60 seconds
tmp_cleanup_stop.py Stop (session exit) Same stale-threshold cleanup on exit

Both hooks use the same 60-second staleness threshold. Neither hook performs unconditional deletion — files from concurrent Claude Code sessions are preserved.

Safety measures

Each of these was specifically tested (see Test Plan below):

  • Symlink protection: os.lstat() checks before os.unlink() — symlinks are detected and skipped, never followed. Prevents the classic /tmp symlink attack on shared systems.
  • Regular file validation: stat.S_ISREG() check ensures only regular files are deleted (not directories, pipes, devices).
  • Strict filename matching: Two-layer validation — glob("claude-????-cwd") narrows to exactly 4 wildcard characters, then re.compile(r"^claude-[0-9a-f]{4}-cwd$") validates the basename contains only lowercase hex.
  • Stale threshold: Only files with mtime older than 60 seconds are removed. The runtime reads the file immediately after command execution, so a 60-second window provides ample safety margin.
  • Concurrent session safe: Because both hooks use the stale threshold (not unconditional deletion), files actively in use by other sessions are preserved.
  • Timeout: 10-second timeout on both hooks in hooks.json prevents blocking Claude Code if /tmp is slow or contains many files.
  • Graceful failure: try/except ImportError on module import with JSON systemMessage fallback. try/except OSError on all file operations. Hook failures don't affect Claude Code operation.
  • Cross-platform: Uses tempfile.gettempdir() instead of hardcoded /tmp. Works on Linux (/tmp) and macOS (/private/tmp). On Windows the plugin has no effect (no matching files exist).

Files added (6 files, 201 lines)

plugins/tmp-cleanup/
├── .claude-plugin/
│   └── plugin.json                  # Plugin metadata
├── hooks/
│   ├── hooks.json                   # Hook event configuration
│   ├── cwd_file_cleanup.py          # Shared cleanup logic (all safety checks here)
│   ├── tmp_cleanup_post_tool.py     # PostToolUse entry point
│   └── tmp_cleanup_stop.py          # Stop entry point
└── README.md

Convention compliance

  • Follows the same plugin structure as security-guidance, hookify, and other existing plugins
  • hooks.json format matches existing plugins (with timeout field as used by hookify)
  • Stop hook omits matcher field (consistent with hookify and ralph-wiggum Stop hooks)
  • Entry point scripts include #!/usr/bin/env python3 shebang (consistent with security-guidance)
  • JSON systemMessage output for observability

Limitations (stated explicitly)

  • This is a workaround, not a fix. The root cause is in the minified cli.js runtime where fs.unlink is never called after reading the cwd file. Only an Anthropic engineer with source access can fix the actual leak.
  • Windows: The plugin has no effect on Windows because Claude Code on Windows uses different temp path patterns.
  • TOCTOU window: There is a theoretical time-of-check/time-of-use gap between os.lstat() and os.unlink(). This is not practically exploitable — os.unlink() on a symlink removes the link (not the target), and hardlink creation in /tmp is restricted on modern Linux (kernel 3.6+, fs.protected_hardlinks=1) and macOS.
  • First install with large backlog: If a user has thousands of accumulated files, the first few cleanup runs may take longer than usual. The 10-second timeout ensures Claude Code is never blocked.

Test plan

All tests were run and passed locally (Python 3.14, macOS Darwin 25.2.0):

  • Syntax validation: py_compile passes on all 3 Python files
  • JSON validation: json.load() passes on hooks.json and plugin.json
  • Import test: from cwd_file_cleanup import run_hook succeeds
  • Import guard test: Both hook files contain try/except ImportError with JSON fallback
  • Symlink protection test: Created a symlink in a temp directory, confirmed os.lstat() detects it and skips deletion, confirmed the symlink target file is preserved
  • Pattern strictness test: Validated regex against 7 cases — accepts claude-abcd-cwd and claude-1234-cwd, rejects claude-ABCD-cwd (uppercase), claude-abcde-cwd (5 chars), claude-abc-cwd (3 chars), claude-my-backup-cwd (arbitrary string), claude--cwd (empty)
  • Stale vs fresh test: Created 25 simulated files, set 15 to 120s old and 10 to current time. Cleanup correctly removed exactly 15 stale files and preserved all 10 fresh files
  • Timeout present: Verified both hooks have "timeout": 10 in hooks.json
  • Convention compliance: Verified Stop hook has no matcher field, both entry points have shebangs, no hardcoded /tmp in metadata files
  • Cross-platform path: Verified tempfile.gettempdir() resolves correctly on macOS

Closes #8856

🤖 Generated with Claude Code

…nthropics#8856)

Adds a new plugin that cleans up orphaned working directory tracking
files created by the Bash tool. These files are never deleted by the
runtime, accumulating hundreds per day as reported in anthropics#8856.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yurukusa
Copy link
Copy Markdown

A Notification start + Stop hook pair can handle tmp cleanup:

find /tmp -name 'claude-*' -mmin +120 -delete 2>/dev/null
find /tmp -name 'cc-*' -mmin +120 -delete 2>/dev/null
echo "Cleaned stale tmp files" >&2
exit 0

Also as a Stop hook for post-session cleanup:

find /tmp -name 'cc-*' -user $(whoami) -delete 2>/dev/null
exit 0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: Missing cleanup for /tmp/claude-*-cwd working directory tracking files

2 participants