From b611b08283d5011251903d998af33192fdfcd8f2 Mon Sep 17 00:00:00 2001 From: lmiranda Date: Wed, 10 Dec 2025 11:23:13 -0500 Subject: [PATCH 1/2] feat: add project-hygiene plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-task cleanup hook that runs after Claude completes work: - Deletes temp files (*.tmp, *.bak, __pycache__, etc.) - Warns about unexpected files in project root - Identifies orphaned files (test_*, debug_*, *_backup.*) - Logs actions to .dev/logs/ - Supports project-local config via .hygiene.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../.claude-plugin/marketplace.json | 6 + project-hygiene/.claude-plugin/plugin.json | 22 ++ project-hygiene/README.md | 135 +++++++ project-hygiene/hooks/cleanup.sh | 365 ++++++++++++++++++ project-hygiene/hooks/hooks.json | 9 + 5 files changed, 537 insertions(+) create mode 100644 project-hygiene/.claude-plugin/plugin.json create mode 100644 project-hygiene/README.md create mode 100755 project-hygiene/hooks/cleanup.sh create mode 100644 project-hygiene/hooks/hooks.json diff --git a/.claude-plugins/projman-marketplace/.claude-plugin/marketplace.json b/.claude-plugins/projman-marketplace/.claude-plugin/marketplace.json index 0d72a75..21c86a6 100644 --- a/.claude-plugins/projman-marketplace/.claude-plugin/marketplace.json +++ b/.claude-plugins/projman-marketplace/.claude-plugin/marketplace.json @@ -10,6 +10,12 @@ "version": "0.1.0", "description": "Sprint planning and project management with Gitea and Wiki.js integration", "source": "./../../projman" + }, + { + "name": "project-hygiene", + "version": "0.1.0", + "description": "Post-task cleanup hook that removes temp files, warns about unexpected root files, and manages orphans", + "source": "./../../project-hygiene" } ] } diff --git a/project-hygiene/.claude-plugin/plugin.json b/project-hygiene/.claude-plugin/plugin.json new file mode 100644 index 0000000..90f5088 --- /dev/null +++ b/project-hygiene/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "project-hygiene", + "version": "0.1.0", + "description": "Post-task cleanup hook that removes temp files, warns about unexpected root files, and manages orphaned supporting files", + "author": { + "name": "Bandit Labs", + "email": "dev@banditlabs.io" + }, + "license": "MIT", + "keywords": ["cleanup", "hygiene", "automation", "hooks", "maintenance"], + "repository": "https://github.com/bandit-labs/project-hygiene", + "config": { + "default_shell": "bash", + "environment": { + "PLUGIN_HOME": "${CLAUDE_PLUGIN_ROOT}" + } + }, + "permissions": { + "file_access": ["read", "write"], + "shell_access": true + } +} diff --git a/project-hygiene/README.md b/project-hygiene/README.md new file mode 100644 index 0000000..abf5a5b --- /dev/null +++ b/project-hygiene/README.md @@ -0,0 +1,135 @@ +# project-hygiene + +Post-task cleanup hook plugin for Claude Code. Automatically cleans up temporary files, warns about unexpected files in the project root, and manages orphaned supporting files after task completion. + +## Features + +- **Delete temp files**: Removes `*.tmp`, `*.bak`, `__pycache__`, `.pytest_cache`, and other common temporary patterns +- **Root file warnings**: Alerts when unexpected files appear in project root +- **Orphan detection**: Identifies `test_*`, `debug_*`, `*_backup.*` and similar files that may have been left behind +- **Cleanup logging**: Records all actions to `.dev/logs/` +- **Configurable**: Project-local `.hygiene.json` for custom rules + +## Installation + +Add to your Claude Code configuration or install from the marketplace: + +```bash +claude plugin install project-hygiene +``` + +## How It Works + +The plugin registers a `task-completed` hook that runs after Claude completes any task. It: + +1. Scans for and deletes known temporary file patterns +2. Removes temporary directories (`__pycache__`, `.pytest_cache`, etc.) +3. Checks for unexpected files in project root and warns +4. Identifies orphaned supporting files (test files, debug scripts, backups) +5. Optionally moves orphans to `.dev/scratch/` for review +6. Logs all actions to `.dev/logs/hygiene-TIMESTAMP.log` + +## Configuration + +Create `.hygiene.json` in your project root to customize behavior: + +```json +{ + "move_orphans": true, + "allowed_root_files": [ + "custom-config.yaml", + "my-script.sh" + ], + "temp_patterns": [ + "*.cache", + "*.pid" + ], + "ignore_patterns": [ + "important_test_*.py", + "keep_this_backup.*" + ] +} +``` + +### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `move_orphans` | boolean | `false` | Move orphaned files to `.dev/scratch/` instead of just warning | +| `allowed_root_files` | array | (see below) | Additional files allowed in project root | +| `temp_patterns` | array | `[]` | Additional temp file patterns to delete | +| `ignore_patterns` | array | `[]` | Files to never touch during cleanup | + +### Default Allowed Root Files + +The plugin recognizes common project files in root: +- Git files: `.git`, `.gitignore`, `.gitattributes` +- Config: `.editorconfig`, `.env*`, `.nvmrc`, etc. +- Documentation: `README.md`, `LICENSE`, `CHANGELOG.md`, `CLAUDE.md` +- Package managers: `package.json`, `requirements.txt`, `Cargo.toml`, `go.mod`, etc. +- Build configs: `Makefile`, `Dockerfile`, `docker-compose.yml`, `tsconfig.json`, etc. + +### Default Temp Patterns + +Automatically cleaned: +- `*.tmp`, `*.bak`, `*.swp`, `*.swo`, `*~` +- `.DS_Store`, `Thumbs.db` +- `*.log`, `*.orig`, `*.pyc`, `*.pyo` + +### Default Temp Directories + +Automatically removed: +- `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache` +- `node_modules/.cache`, `.next/cache`, `.nuxt/.cache`, `.turbo` +- `*.egg-info`, `.eggs` + +### Orphan Patterns + +Files flagged as orphans: +- `test_*.py` (standalone test files) +- `debug_*` (debug scripts) +- `*_backup.*`, `*_old.*`, `*_bak.*`, `*.backup` +- `temp_*`, `tmp_*` + +## Output + +After each task, you'll see output like: + +``` +[14:32:15] Starting project hygiene cleanup... + +[14:32:15] Cleaning temp files... +[14:32:15] DELETED: ./src/__pycache__/utils.cpython-311.pyc +[14:32:15] DELETED: ./temp.bak + +[14:32:15] Cleaning temp directories... +[14:32:15] DELETED DIR: ./src/__pycache__ + +[14:32:15] Checking root files... +[14:32:15] WARNING: Unexpected root file: random_notes.txt + +[14:32:15] Checking for orphaned files... +[14:32:15] ORPHAN: ./test_scratch.py +[14:32:15] ORPHAN: ./debug_api.py + +=== Cleanup Summary === + Deleted: 3 items + Warnings: 1 unexpected root files + Orphans: 2 files + Log file: .dev/logs/hygiene-20250110-143215.log +``` + +## Directory Structure + +``` +.dev/ +├── logs/ +│ └── hygiene-YYYYMMDD-HHMMSS.log +└── scratch/ # (if move_orphans enabled) + └── debug_api.py +``` + +## Requirements + +- Bash 4.0+ +- Optional: `jq` for JSON config parsing (falls back to defaults if not installed) diff --git a/project-hygiene/hooks/cleanup.sh b/project-hygiene/hooks/cleanup.sh new file mode 100755 index 0000000..5206948 --- /dev/null +++ b/project-hygiene/hooks/cleanup.sh @@ -0,0 +1,365 @@ +#!/bin/bash +# project-hygiene cleanup hook +# Runs after task completion to clean up temp files and manage orphans + +set -euo pipefail + +# Configuration +PROJECT_ROOT="${PROJECT_ROOT:-.}" +PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$(realpath "$0")")")}" +CONFIG_FILE="${PROJECT_ROOT}/.hygiene.json" +LOG_DIR="${PROJECT_ROOT}/.dev/logs" +SCRATCH_DIR="${PROJECT_ROOT}/.dev/scratch" +LOG_FILE="${LOG_DIR}/hygiene-$(date +%Y%m%d-%H%M%S).log" + +# Default allowed root files (can be overridden by .hygiene.json) +DEFAULT_ALLOWED_ROOT=( + ".git" + ".gitignore" + ".gitattributes" + ".editorconfig" + ".env" + ".env.example" + ".env.local" + ".nvmrc" + ".node-version" + ".python-version" + ".ruby-version" + ".tool-versions" + "README.md" + "LICENSE" + "CHANGELOG.md" + "CONTRIBUTING.md" + "CLAUDE.md" + "package.json" + "package-lock.json" + "yarn.lock" + "pnpm-lock.yaml" + "Makefile" + "Dockerfile" + "docker-compose.yml" + "docker-compose.yaml" + "Cargo.toml" + "Cargo.lock" + "go.mod" + "go.sum" + "requirements.txt" + "setup.py" + "pyproject.toml" + "poetry.lock" + "Gemfile" + "Gemfile.lock" + "tsconfig.json" + "jsconfig.json" + ".eslintrc*" + ".prettierrc*" + "vite.config.*" + "webpack.config.*" + "rollup.config.*" + ".hygiene.json" +) + +# Temp file patterns to delete +TEMP_PATTERNS=( + "*.tmp" + "*.bak" + "*.swp" + "*.swo" + "*~" + ".DS_Store" + "Thumbs.db" + "*.log" + "*.orig" + "*.pyc" + "*.pyo" +) + +# Directory patterns to delete +TEMP_DIRS=( + "__pycache__" + ".pytest_cache" + ".mypy_cache" + ".ruff_cache" + "node_modules/.cache" + ".next/cache" + ".nuxt/.cache" + ".turbo" + "*.egg-info" + ".eggs" + "dist" + "build" +) + +# Orphan patterns to identify +ORPHAN_PATTERNS=( + "test_*.py" + "debug_*" + "*_backup.*" + "*_old.*" + "*_bak.*" + "*.backup" + "temp_*" + "tmp_*" +) + +# Initialize +DELETED_COUNT=0 +WARNED_COUNT=0 +ORPHAN_COUNT=0 +MOVE_ORPHANS=false + +# Logging function +log() { + local msg="[$(date +%H:%M:%S)] $1" + echo "$msg" + if [[ -f "$LOG_FILE" ]]; then + echo "$msg" >> "$LOG_FILE" + fi +} + +log_action() { + local action="$1" + local target="$2" + log " $action: $target" +} + +# Load project-local config if exists +load_config() { + if [[ -f "$CONFIG_FILE" ]]; then + log "Loading config from $CONFIG_FILE" + + # Check if move_orphans is enabled + if command -v jq &>/dev/null; then + MOVE_ORPHANS=$(jq -r '.move_orphans // false' "$CONFIG_FILE" 2>/dev/null || echo "false") + + # Load additional allowed root files + local extra_allowed + extra_allowed=$(jq -r '.allowed_root_files // [] | .[]' "$CONFIG_FILE" 2>/dev/null || true) + if [[ -n "$extra_allowed" ]]; then + while IFS= read -r file; do + DEFAULT_ALLOWED_ROOT+=("$file") + done <<< "$extra_allowed" + fi + + # Load additional temp patterns + local extra_temp + extra_temp=$(jq -r '.temp_patterns // [] | .[]' "$CONFIG_FILE" 2>/dev/null || true) + if [[ -n "$extra_temp" ]]; then + while IFS= read -r pattern; do + TEMP_PATTERNS+=("$pattern") + done <<< "$extra_temp" + fi + + # Load ignore patterns (files to never touch) + IGNORE_PATTERNS=() + local ignore + ignore=$(jq -r '.ignore_patterns // [] | .[]' "$CONFIG_FILE" 2>/dev/null || true) + if [[ -n "$ignore" ]]; then + while IFS= read -r pattern; do + IGNORE_PATTERNS+=("$pattern") + done <<< "$ignore" + fi + else + log "Warning: jq not installed, using default config" + fi + fi +} + +# Check if file should be ignored +should_ignore() { + local file="$1" + local basename + basename=$(basename "$file") + + for pattern in "${IGNORE_PATTERNS[@]:-}"; do + if [[ "$basename" == $pattern ]] || [[ "$file" == $pattern ]]; then + return 0 + fi + done + return 1 +} + +# Check if file is in allowed root list +is_allowed_root() { + local file="$1" + local basename + basename=$(basename "$file") + + for allowed in "${DEFAULT_ALLOWED_ROOT[@]}"; do + # Support wildcards in allowed patterns + if [[ "$basename" == $allowed ]]; then + return 0 + fi + done + return 1 +} + +# Check if file matches orphan pattern +is_orphan() { + local file="$1" + local basename + basename=$(basename "$file") + + for pattern in "${ORPHAN_PATTERNS[@]}"; do + if [[ "$basename" == $pattern ]]; then + return 0 + fi + done + return 1 +} + +# Setup directories +setup_dirs() { + mkdir -p "$LOG_DIR" + if [[ "$MOVE_ORPHANS" == "true" ]]; then + mkdir -p "$SCRATCH_DIR" + fi + + # Start log file + echo "=== Project Hygiene Cleanup ===" > "$LOG_FILE" + echo "Started: $(date)" >> "$LOG_FILE" + echo "Project: $PROJECT_ROOT" >> "$LOG_FILE" + echo "" >> "$LOG_FILE" +} + +# Delete temp files +cleanup_temp_files() { + log "Cleaning temp files..." + + for pattern in "${TEMP_PATTERNS[@]}"; do + while IFS= read -r -d '' file; do + if should_ignore "$file"; then + continue + fi + rm -f "$file" + log_action "DELETED" "$file" + ((DELETED_COUNT++)) + done < <(find "$PROJECT_ROOT" -name "$pattern" -type f -print0 2>/dev/null || true) + done +} + +# Delete temp directories +cleanup_temp_dirs() { + log "Cleaning temp directories..." + + for pattern in "${TEMP_DIRS[@]}"; do + while IFS= read -r -d '' dir; do + if should_ignore "$dir"; then + continue + fi + rm -rf "$dir" + log_action "DELETED DIR" "$dir" + ((DELETED_COUNT++)) + done < <(find "$PROJECT_ROOT" -name "$pattern" -type d -print0 2>/dev/null || true) + done +} + +# Warn about unexpected root files +check_root_files() { + log "Checking root files..." + + local unexpected_files=() + + while IFS= read -r -d '' file; do + local basename + basename=$(basename "$file") + + # Skip directories + [[ -d "$file" ]] && continue + + # Skip if in allowed list + is_allowed_root "$basename" && continue + + # Skip if should be ignored + should_ignore "$basename" && continue + + unexpected_files+=("$basename") + log_action "WARNING" "Unexpected root file: $basename" + ((WARNED_COUNT++)) + done < <(find "$PROJECT_ROOT" -maxdepth 1 -print0 2>/dev/null || true) + + if [[ ${#unexpected_files[@]} -gt 0 ]]; then + log "" + log "⚠️ Unexpected files in project root:" + for f in "${unexpected_files[@]}"; do + log " - $f" + done + fi +} + +# Identify and handle orphaned files +handle_orphans() { + log "Checking for orphaned files..." + + local orphan_files=() + + for pattern in "${ORPHAN_PATTERNS[@]}"; do + while IFS= read -r -d '' file; do + if should_ignore "$file"; then + continue + fi + + orphan_files+=("$file") + + if [[ "$MOVE_ORPHANS" == "true" ]]; then + local dest="${SCRATCH_DIR}/$(basename "$file")" + # Handle duplicates + if [[ -f "$dest" ]]; then + dest="${SCRATCH_DIR}/$(date +%Y%m%d%H%M%S)_$(basename "$file")" + fi + mv "$file" "$dest" + log_action "MOVED" "$file -> $dest" + else + log_action "ORPHAN" "$file" + fi + ((ORPHAN_COUNT++)) + done < <(find "$PROJECT_ROOT" -name "$pattern" -type f -print0 2>/dev/null || true) + done + + if [[ ${#orphan_files[@]} -gt 0 && "$MOVE_ORPHANS" != "true" ]]; then + log "" + log "📦 Orphaned files found (enable move_orphans in .hygiene.json to auto-move):" + for f in "${orphan_files[@]}"; do + log " - $f" + done + fi +} + +# Summary +print_summary() { + log "" + log "=== Cleanup Summary ===" + log " Deleted: $DELETED_COUNT items" + log " Warnings: $WARNED_COUNT unexpected root files" + log " Orphans: $ORPHAN_COUNT files" + if [[ "$MOVE_ORPHANS" == "true" ]]; then + log " Orphans moved to: $SCRATCH_DIR" + fi + log " Log file: $LOG_FILE" + log "" +} + +# Main +main() { + cd "$PROJECT_ROOT" || exit 1 + + load_config + setup_dirs + + log "Starting project hygiene cleanup..." + log "" + + cleanup_temp_files + cleanup_temp_dirs + check_root_files + handle_orphans + + print_summary + + # Exit with warning code if issues found + if [[ $WARNED_COUNT -gt 0 || $ORPHAN_COUNT -gt 0 ]]; then + exit 0 # Still success, but logged warnings + fi +} + +main "$@" diff --git a/project-hygiene/hooks/hooks.json b/project-hygiene/hooks/hooks.json new file mode 100644 index 0000000..7d922f1 --- /dev/null +++ b/project-hygiene/hooks/hooks.json @@ -0,0 +1,9 @@ +{ + "hooks": [ + { + "event": "task-completed", + "script": "${CLAUDE_PLUGIN_ROOT}/hooks/cleanup.sh", + "description": "Post-task cleanup: remove temp files, warn about unexpected root files, manage orphans" + } + ] +} From 239a4869b4b9cb4aea8f292657471b7b16a04b3a Mon Sep 17 00:00:00 2001 From: lmiranda Date: Wed, 10 Dec 2025 11:40:10 -0500 Subject: [PATCH 2/2] docs: add comprehensive README with project documentation links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main repository README now includes: - Overview of all plugins (projman, cmdb-assistant, project-hygiene) - MCP server documentation links (Gitea, Wiki.js, NetBox) - Installation and quick start guide - Repository structure overview - Links to reference documentation and testing reports - Development and roadmap sections 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | Bin 0 -> 7488 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/README.md b/README.md index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..76fc4f900fa16654d390e1782d8354bdd71ab65f 100644 GIT binary patch literal 7488 zcmcIp-EZT@5fA#v=RWpf0RzdMB2w|?(k6$bs4VLoC6cX3@?DBUAg#!?#E4(nUCLH* z=-=Mo>@Fozvag4GuG z^0)CO4t`bGf)*=jln&TT@=Rf~DNH~c-inN!f8{$?)fQ+?4uUAg*5hTV`7#mA+77$( z*=#L^@L?tBoWdQ+E33MYZFVEWwZJ;AbKLbGL<<;BC!LTBaL~t zU8?$%Fv*oL56qsFuXb6uZ~+mf(bDHik;0j#xM)6UhR3zg7x@G0jV>>Y8J#~V9$uG8 z!g`b2OO_YHy4i+vCN%l;ID`+;4fjgKd$O~(ubnRA<4duPL(yh~ahoYbm5q4G!%gjk zz|hkamVkvh{LXkNbA{M~PEKcpN0}Ip^zw|(nu$c79P9O4DP(Nux+R@u1W=rqKn~YD zTM2p)Zs4v{7a7Z01&q1PJsb`75BG6#xTft?j-cZREBNc(Di)cjPH62>(-q@fX@ADm z2Dl=4Ru-(g2L4+ZuBEi9Wj+vA(e za}DxAgrgJR!R9QN1#ApjRTf1qH6c(X9s22ci=;+kS+4A5Yrcdt$|7hjNb)dWL9jVN zis4)?0$9J3;Av-p*Kqt@yeg$Zk!{TJ-dP*?lxOfnt3_rr2TsGZer1}-c*!93)FQR# zP1iWlKyQK_Vy(pkfK%o=2kh?w-PoJ}4p8Zk3w9=i1i6qI0+6sDAp*riK#W0cAhiWh z$_-W`1wAqIM+O%tF}VIJ%P~iX4HL%iTVdP1-R6A^km@<=DV` zb}3XSZo}NPr#ln53%(mi>eSv}5+(?i4P>RuwbUp(Kd3^hDk> z_mcYE1~E1o@ieq#MX$C75kj9Hn%%;j2(#A$UhA9@5$X$&4HHWaCpvvFPjJ+ z?HoQ<+3RUX+#|pJs_LG&%)^pWu|b%cM+}PKEIpb=C?2mQFV;T|e~%L^?92<~LgR(0 z#lhu{ZNv`v8>tSmOw$KMTyp{Na}(wFIH1I&;VrAQp>w>(lNa`9R~&{kB)Kj}|rtUDf1HBnU{CgP8Z zszXLH4#2@i!k`F6bs}LBvWSa&e}CbdGjQ1(j6gZXnN)2y0Vd&}O9w4nZ}t3HM`(V zEOp6~_+K{spnp;-I)7j~!k>dS#>M>yBsAhaLwJBOXi)&YjztpT*5*Lq>c{vDtWiZI zi4kni4>l*oZ)HrG4ka-#xK=wO9*A5-cs}0gHOk~KgYz#w$8FbOAz?V`8-PFslE^~_ zr`pUN%y#8v>>(2Z^(bm5Ds_Jw9R_EUyh zb;|+2-*=Z02 z9gB;9u;c?q90Xff_`?L-aPhn^O-#9xLNMetQ^@e-5f`N!)VRzq*uOhLXqI$rlsE&L zm^M2=h4%LoDx2&Ba@)7xvd{WAuRkN=tHG?_odenCx0B&_Ypn}q16J@rJaDS&AT90i zBJOO@n=58FKlZP`+rjs9oo_^Tczg5qx<9dRkZI{Tb+Sld+G#>oftwn1~E_jP}E zar5(u-W7Gz$i%+3;4!T$Q-*8wBkZqVd~xdQ7AZlj07{7q|y^G*YW=H$tR|^M;`4!{ga**`}VV-aeY{S)NlOuPF(W2M@}NrHRga@4^#6gjK%h3R3f zG4@P9PCSXL#8-Q+w`p~GXKZV2jD|WQ^@G%zX&h{#C%jHI`)Y(w!+4vn-6eD8R47U_5}0#d*W@fXT4scVt;Q~ zt1+#yDdPi7_XNcCt(#k7rR(NR6H(`D3e6c!4l$_sl)XXK9l7H7e~J}2ZazcTGPu5) z_ijexVSh#g+I`SC=RQV5nv+J@zkiYc<$Gp3u5i}Z$-)(Le>z&@s_3Oda?W9XeJ8hqOM~aRu-XStHMsH>kZf)DcJbl)m z54#ur;dFj=JGgAP?(x^e(MGYI*;Hd^$KGt6`pIg@Z%r{ZInVQEjg|)WdYIt*1YCcL zf%XG~`6K|~*zG%IjrirpycAELho@Qtg_8EEO;=*W_~DKd?<%y@0l%x4`- zK$W)7u?zO!CFn2NwPM|95o z?J(KSJDtn*aa+ literal 0 HcmV?d00001