Add tmp-cwd-cleanup plugin: Stop hook for /tmp/claude-*-cwd leak (#8856)#37236
Add tmp-cwd-cleanup plugin: Stop hook for /tmp/claude-*-cwd leak (#8856)#37236YoshKoz wants to merge 1 commit intoanthropics:mainfrom
Conversation
…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>
There was a problem hiding this comment.
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-*-cwdfiles 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.
| "name": "Claude Code Community", | ||
| "email": "" |
There was a problem hiding this comment.
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.
| "name": "Claude Code Community", | |
| "email": "" | |
| "name": "Claude Code Community" |
| 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: |
There was a problem hiding this comment.
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.
| 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: |
| # 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) | ||
|
|
||
|
|
There was a problem hiding this comment.
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) |
Summary
Adds a new
tmp-cwd-cleanupplugin that works around the memory leak described in #8856.The bug: The Bash tool appends
pwd -P >| /tmp/claude-<hex>-cwdto every shell command to track the working directory after execution, butunlinkSyncis 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
Stophook that deletes all/tmp/claude-*-cwdfiles owned by the current user when the session ends.Key properties:
0— never blocks session teardownThis is an explicit stopgap: the
README.mdand inline comment both point to the upstream fix (unlinkSyncafter reading the cwd file) and note this plugin can be removed once that lands.Files changed
plugins/tmp-cwd-cleanup/.claude-plugin/plugin.jsonplugins/tmp-cwd-cleanup/hooks/hooks.jsonplugins/tmp-cwd-cleanup/hooks/cleanup_cwd_files.pyplugins/tmp-cwd-cleanup/README.mdplugins/README.mdTest plan
/tmp/claude-*-cwdfiles accumulate as expectedCloses #8856 (workaround until upstream
unlinkSyncfix)🤖 Generated with Claude Code