Skip to content

Add tmp-cwd-cleanup plugin: Stop hook for /tmp/claude-*-cwd leak (#8856)#37236

Open
YoshKoz wants to merge 1 commit intoanthropics:mainfrom
YoshKoz:fix/tmp-cwd-cleanup-plugin
Open

Add tmp-cwd-cleanup plugin: Stop hook for /tmp/claude-*-cwd leak (#8856)#37236
YoshKoz wants to merge 1 commit intoanthropics:mainfrom
YoshKoz:fix/tmp-cwd-cleanup-plugin

Conversation

@YoshKoz
Copy link

@YoshKoz YoshKoz commented Mar 21, 2026

Summary

Adds a new tmp-cwd-cleanup plugin that works around the memory leak described in #8856.

The bug: The Bash tool appends pwd -P >| /tmp/claude-<hex>-cwd to every shell command to track the working directory after execution, but unlinkSync is never called on the file. On active systems, users report 174+ orphaned files per 12 hours that accumulate indefinitely until the OS clears /tmp (daily on most distros).

This plugin: Registers a Stop hook that deletes all /tmp/claude-*-cwd files owned by the current user when the session ends.

/tmp/claude-02a6-cwd  (22 bytes) → deleted on Stop
/tmp/claude-1f3b-cwd  (22 bytes) → deleted on Stop
...

Key properties:

  • Only removes files owned by the current user (safe on multi-user/shared systems)
  • Always exits 0 — never blocks session teardown
  • ~50 lines of straightforward Python with no dependencies

This is an explicit stopgap: the README.md and inline comment both point to the upstream fix (unlinkSync after reading the cwd file) and note this plugin can be removed once that lands.

Files changed

File Purpose
plugins/tmp-cwd-cleanup/.claude-plugin/plugin.json Plugin metadata
plugins/tmp-cwd-cleanup/hooks/hooks.json Registers Stop hook
plugins/tmp-cwd-cleanup/hooks/cleanup_cwd_files.py Cleanup logic
plugins/tmp-cwd-cleanup/README.md User-facing docs
plugins/README.md Added entry to the plugin table

Test plan

  • Trigger several Bash tool calls; confirm /tmp/claude-*-cwd files accumulate as expected
  • Install the plugin and end the session; confirm files are removed
  • Verify no files belonging to other users are touched (UID check)
  • Verify session teardown is not delayed or blocked (exit code is always 0)

Closes #8856 (workaround until upstream unlinkSync fix)

🤖 Generated with Claude Code

…nthropics#8856)

The Bash tool appends `pwd -P >| /tmp/claude-<hex>-cwd` to every shell
command to capture the resulting working directory, but never deletes the
file. On active systems these files accumulate into the thousands and slow
down /tmp access (reported in anthropics#8856 with 174+ files per 12 h of usage).

This plugin registers a Stop hook that deletes all /tmp/claude-*-cwd files
owned by the current user when the session ends, providing a clean-up path
until the upstream unlinkSync fix lands in the CLI binary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 21, 2026 20:29
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new tmp-cwd-cleanup plugin intended as a stopgap workaround for the /tmp/claude-*-cwd file leak described in #8856 by cleaning up orphaned cwd-tracking temp files when a session stops.

Changes:

  • Introduces a new plugin with metadata, docs, and a Stop hook registration.
  • Adds a Python Stop-hook script that deletes /tmp/claude-*-cwd files owned by the current user.
  • Documents the plugin and lists it in plugins/README.md.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
plugins/tmp-cwd-cleanup/.claude-plugin/plugin.json Adds plugin manifest metadata for tmp-cwd-cleanup.
plugins/tmp-cwd-cleanup/hooks/hooks.json Registers a Stop command hook to run the cleanup script.
plugins/tmp-cwd-cleanup/hooks/cleanup_cwd_files.py Implements deletion of leaked /tmp/claude-*-cwd files on Stop.
plugins/tmp-cwd-cleanup/README.md Documents what the plugin does and how to install it.
plugins/README.md Adds tmp-cwd-cleanup to the plugin listing table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6 to +7
"name": "Claude Code Community",
"email": ""
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

author.email is set to an empty string. Since email is optional in the manifest, it would be better to either omit the field entirely or provide a real contact address; keeping it empty can fail validation/format checks and is inconsistent with other plugin manifests in this repo.

Suggested change
"name": "Claude Code Community",
"email": ""
"name": "Claude Code Community"

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +33
uid = os.getuid() if hasattr(os, "getuid") else None # no-op on Windows
removed = 0
errors = 0

for path in glob.glob("/tmp/claude-*-cwd"):
try:
# Only remove files owned by the current user (safety check).
if uid is not None and os.stat(path).st_uid != uid:
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On platforms without os.getuid() (e.g., Windows), uid becomes None and the ownership check is skipped, meaning the script may unlink any matching /tmp/claude-*-cwd file regardless of owner. To preserve the “current-user only” safety guarantee, consider making the script a no-op when getuid isn’t available, or implement an equivalent ownership check for that platform.

Suggested change
uid = os.getuid() if hasattr(os, "getuid") else None # no-op on Windows
removed = 0
errors = 0
for path in glob.glob("/tmp/claude-*-cwd"):
try:
# Only remove files owned by the current user (safety check).
if uid is not None and os.stat(path).st_uid != uid:
# On platforms without os.getuid (e.g., Windows), skip cleanup entirely
# to avoid deleting files owned by other users.
if not hasattr(os, "getuid"):
return
uid = os.getuid()
removed = 0
errors = 0
for path in glob.glob("/tmp/claude-*-cwd"):
try:
# Only remove files owned by the current user (safety check).
if os.stat(path).st_uid != uid:

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +51
# Consume stdin (required by the hook protocol) but we don't need it.
try:
json.load(sys.stdin)
except Exception:
pass

uid = os.getuid() if hasattr(os, "getuid") else None # no-op on Windows
removed = 0
errors = 0

for path in glob.glob("/tmp/claude-*-cwd"):
try:
# Only remove files owned by the current user (safety check).
if uid is not None and os.stat(path).st_uid != uid:
continue
if os.path.isfile(path):
os.unlink(path)
removed += 1
except OSError:
errors += 1

# Exit 0 so the Stop event is never blocked.
# Optionally surface a brief summary via systemMessage.
if removed > 0:
result = {"systemMessage": f"tmp-cwd-cleanup: removed {removed} orphaned file(s)."}
if errors:
result["systemMessage"] += f" ({errors} error(s) skipped)"
print(json.dumps(result))

sys.exit(0)


Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script intends to “always exit 0”, but any unexpected exception outside the per-file OSError handler (e.g., JSON parsing edge cases, stdout write errors, or other runtime exceptions) will currently propagate and can return a non-zero exit code. Wrap the main body in a broad try/except (like other hook scripts in this repo) and ensure you sys.exit(0) in finally so Stop teardown is never blocked.

Suggested change
# Consume stdin (required by the hook protocol) but we don't need it.
try:
json.load(sys.stdin)
except Exception:
pass
uid = os.getuid() if hasattr(os, "getuid") else None # no-op on Windows
removed = 0
errors = 0
for path in glob.glob("/tmp/claude-*-cwd"):
try:
# Only remove files owned by the current user (safety check).
if uid is not None and os.stat(path).st_uid != uid:
continue
if os.path.isfile(path):
os.unlink(path)
removed += 1
except OSError:
errors += 1
# Exit 0 so the Stop event is never blocked.
# Optionally surface a brief summary via systemMessage.
if removed > 0:
result = {"systemMessage": f"tmp-cwd-cleanup: removed {removed} orphaned file(s)."}
if errors:
result["systemMessage"] += f" ({errors} error(s) skipped)"
print(json.dumps(result))
sys.exit(0)
try:
# Consume stdin (required by the hook protocol) but we don't need it.
try:
json.load(sys.stdin)
except Exception:
# Ignore any malformed or unexpected stdin payloads.
pass
uid = os.getuid() if hasattr(os, "getuid") else None # no-op on Windows
removed = 0
errors = 0
for path in glob.glob("/tmp/claude-*-cwd"):
try:
# Only remove files owned by the current user (safety check).
if uid is not None and os.stat(path).st_uid != uid:
continue
if os.path.isfile(path):
os.unlink(path)
removed += 1
except OSError:
errors += 1
# Optionally surface a brief summary via systemMessage.
if removed > 0:
result = {"systemMessage": f"tmp-cwd-cleanup: removed {removed} orphaned file(s)."}
if errors:
result["systemMessage"] += f" ({errors} error(s) skipped)"
print(json.dumps(result))
except Exception:
# Swallow any unexpected errors to ensure we always exit 0.
pass
finally:
# Exit 0 so the Stop event is never blocked.
sys.exit(0)

Copilot uses AI. Check for mistakes.
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