-
Notifications
You must be signed in to change notification settings - Fork 19k
Add tmp-cwd-cleanup plugin: Stop hook for /tmp/claude-*-cwd leak (#8856) #37236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "tmp-cwd-cleanup", | ||
| "version": "1.0.0", | ||
| "description": "Cleans up orphaned /tmp/claude-*-cwd working directory tracking files on session stop, working around a memory leak in the Bash tool (issue #8856)", | ||
| "author": { | ||
| "name": "Claude Code Community", | ||
| "email": "" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # tmp-cwd-cleanup | ||
|
|
||
| A workaround plugin for [issue #8856](https://github.com/anthropics/claude-code/issues/8856): the Bash tool creates a `/tmp/claude-<hex>-cwd` file after every shell command to track the working directory, but never deletes it. On active systems these files accumulate into the thousands and eventually slow down `/tmp` lookups. | ||
|
|
||
| ## What it does | ||
|
|
||
| Registers a **Stop hook** that deletes all `/tmp/claude-*-cwd` files owned by the current user when the Claude Code session ends. | ||
|
|
||
| ``` | ||
| /tmp/claude-02a6-cwd (22 bytes — deleted on exit) | ||
| /tmp/claude-1f3b-cwd (22 bytes — deleted on exit) | ||
| ... | ||
| ``` | ||
|
|
||
| ## Installation | ||
|
|
||
| Install via the `/plugin` command inside Claude Code: | ||
|
|
||
| ``` | ||
| /plugin install tmp-cwd-cleanup | ||
| ``` | ||
|
|
||
| Or add it manually to your `.claude/settings.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "plugins": ["tmp-cwd-cleanup"] | ||
| } | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| - Only removes files owned by the **current user** (safe on shared systems). | ||
| - Exits `0` so it never blocks session teardown. | ||
| - This plugin is a stopgap; the proper fix is to call `unlinkSync` in the Bash tool implementation immediately after reading the cwd file ([upstream tracking issue #8856](https://github.com/anthropics/claude-code/issues/8856)). | ||
|
|
||
| ## Hook | ||
|
|
||
| | Event | Matcher | Effect | | ||
| |-------|---------|--------| | ||
| | `Stop` | _(all)_ | Deletes `/tmp/claude-*-cwd` files owned by current user | |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Stop hook: clean up orphaned /tmp/claude-*-cwd files. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The Bash tool writes a small temp file to /tmp/claude-<hex>-cwd after each | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| command to capture the resulting working directory, but never deletes it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| On active systems these files accumulate into the thousands and eventually | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slow down /tmp lookups (see issue #8856). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This hook runs on session Stop and removes any such files owned by the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| current user, providing a clean-up path until the upstream fix lands. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import glob | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def main() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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: | |
| # 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
AI
Mar 21, 2026
There was a problem hiding this comment.
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.
| # 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "description": "Cleans up orphaned /tmp/claude-*-cwd working directory tracking files when the session stops", | ||
| "hooks": { | ||
| "Stop": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/cleanup_cwd_files.py" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
author.emailis set to an empty string. Sinceemailis 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.