Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
| [pr-review-toolkit](./pr-review-toolkit/) | Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification | **Command:** `/pr-review-toolkit:review-pr` - Run with optional review aspects (comments, tests, errors, types, code, simplify, all)<br>**Agents:** `comment-analyzer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer`, `code-reviewer`, `code-simplifier` |
| [ralph-wiggum](./ralph-wiggum/) | Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly until completion | **Commands:** `/ralph-loop`, `/cancel-ralph` - Start/stop autonomous iteration loops<br>**Hook:** Stop - Intercepts exit attempts to continue iteration |
| [security-guidance](./security-guidance/) | Security reminder hook that warns about potential security issues when editing files | **Hook:** PreToolUse - Monitors 9 security patterns including command injection, XSS, eval usage, dangerous HTML, pickle deserialization, and os.system calls |
| [tmp-cwd-cleanup](./tmp-cwd-cleanup/) | Cleans up orphaned `/tmp/claude-*-cwd` files on session exit, working around a Bash tool memory leak ([#8856](https://github.com/anthropics/claude-code/issues/8856)) | **Hook:** Stop - Deletes accumulated working-directory tracking files owned by the current user |

## Installation

Expand Down
9 changes: 9 additions & 0 deletions plugins/tmp-cwd-cleanup/.claude-plugin/plugin.json
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": ""
Comment on lines +6 to +7
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.
}
}
41 changes: 41 additions & 0 deletions plugins/tmp-cwd-cleanup/README.md
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 |
53 changes: 53 additions & 0 deletions plugins/tmp-cwd-cleanup/hooks/cleanup_cwd_files.py
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
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.
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)


Comment on lines +20 to +51
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.
if __name__ == "__main__":
main()
15 changes: 15 additions & 0 deletions plugins/tmp-cwd-cleanup/hooks/hooks.json
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"
}
]
}
]
}
}
Loading