#!/bin/bash # PreToolUse Hook - Comprehensive core CLI suggestions and safety rails # Intercepts commands and suggests safer core CLI equivalents # Logs decisions for training data collection set -euo pipefail input=$(cat) command=$(echo "$input" | jq -r '.tool_input.command // empty') session_id=$(echo "$input" | jq -r '.session_id // "unknown"') # Log file for training data (wrong choices, blocked commands) LOG_DIR="/home/shared/hostuk/training-data/command-intercepts" mkdir -p "$LOG_DIR" 2>/dev/null || true log_intercept() { local action="$1" local raw_cmd="$2" local suggestion="$3" local reason="$4" if [[ -d "$LOG_DIR" ]]; then local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") local log_file="$LOG_DIR/$(date +%Y-%m-%d).jsonl" echo "{\"timestamp\":\"$timestamp\",\"session\":\"$session_id\",\"action\":\"$action\",\"raw_command\":$(echo "$raw_cmd" | jq -Rs .),\"suggestion\":\"$suggestion\",\"reason\":$(echo "$reason" | jq -Rs .)}" >> "$log_file" 2>/dev/null || true fi } # If no command, allow if [[ -z "$command" ]]; then echo '{"continue": true}' exit 0 fi # Normalize command for matching norm_cmd=$(echo "$command" | tr '[:upper:]' '[:lower:]') # ============================================================================= # BLOCKED COMMANDS - Hard deny, these are always wrong # ============================================================================= blocked_patterns=( "rm -rf /|Refusing to delete root filesystem" "rm -rf /*|Refusing to delete root filesystem" "rm -rf ~|Refusing to delete home directory" "rm -rf \$HOME|Refusing to delete home directory" ":(){ :|:& };:|Fork bomb detected" "dd if=/dev/zero of=/dev/sd|Refusing to wipe disk" "dd if=/dev/zero of=/dev/nvme|Refusing to wipe disk" "mkfs|Refusing to format filesystem" "fdisk|Refusing disk partitioning" "> /dev/sd|Refusing to write to raw disk" "chmod -R 777 /|Refusing recursive 777 on root" "chmod 777 /|Refusing 777 on root" "chown -R root /|Refusing recursive chown on root" ) for entry in "${blocked_patterns[@]}"; do pattern="${entry%%|*}" reason="${entry#*|}" if [[ "$command" == *"$pattern"* ]]; then log_intercept "BLOCKED" "$command" "" "$reason" cat << EOF { "continue": false, "hookSpecificOutput": { "permissionDecision": "deny" }, "systemMessage": "🚫 **BLOCKED**: $reason\n\nThis command has been blocked for safety. If you believe this is a mistake, ask the user for explicit confirmation." } EOF exit 0 fi done # ============================================================================= # DANGEROUS COMMANDS - Warn and require confirmation # ============================================================================= dangerous_patterns=( "git reset --hard|Discards ALL uncommitted changes permanently. Consider: git stash" "git clean -f|Deletes untracked files permanently. Consider: git clean -n (dry run)" "git clean -fd|Deletes untracked files AND directories permanently" "git checkout .|Discards all uncommitted changes in working directory" "git restore .|Discards all uncommitted changes in working directory" "git branch -D|Force-deletes branch even if not merged. Consider: git branch -d" "git push --force|Force push can overwrite remote history. Consider: core git push" "git push -f |Force push can overwrite remote history. Consider: core git push" "git rebase -i|Interactive rebase rewrites history - ensure you know what you're doing" "docker system prune|Removes ALL unused containers, networks, images" "docker volume prune|Removes ALL unused volumes - may delete data" "docker container prune|Removes ALL stopped containers" "npm cache clean --force|Clears entire npm cache" "rm -rf node_modules|Deletes all dependencies - will need npm install" "rm -rf vendor|Deletes all PHP dependencies - will need composer install" "rm -rf .git|Deletes entire git history permanently" "truncate|Truncates file to specified size - may lose data" "find . -delete|Recursively deletes files - verify pattern first" "find . -exec rm|Recursively deletes files - verify pattern first" "xargs rm|Mass deletion - verify input first" ) for entry in "${dangerous_patterns[@]}"; do pattern="${entry%%|*}" reason="${entry#*|}" if [[ "$command" == *"$pattern"* ]]; then log_intercept "DANGEROUS" "$command" "" "$reason" cat << EOF { "continue": true, "hookSpecificOutput": { "permissionDecision": "ask" }, "systemMessage": "⚠️ **CAUTION**: $reason\n\nThis is a destructive operation. Please confirm with the user before proceeding." } EOF exit 0 fi done # ============================================================================= # CORE CLI SUGGESTIONS - Map raw commands to safer alternatives # ============================================================================= # Check for suggestions - format: "pattern|core_command|reason|category" suggestions=( # === GO COMMANDS === "go build|core build|Handles cross-compilation, signing, checksums, and release packaging|go" "go test|core go qa --only=test|Includes race detection, coverage reporting, and proper CI output|go" "go test -race|core go qa --race|Runs tests with race detection and coverage|go" "go test -cover|core go qa --coverage|Runs tests with coverage threshold enforcement|go" "go fmt|core go qa --fix|Also runs vet and lint with auto-fix|go" "gofmt|core go fmt|Uses project formatting configuration|go" "go vet|core go qa quick|Runs fmt, vet, and lint together|go" "golangci-lint|core go lint|Configured with project-specific linter rules|go" "go mod tidy|core go mod tidy|Runs with verification|go" "go mod download|core go mod tidy|Ensures consistent dependencies|go" "go install|core go install|Installs to correct GOBIN location|go" "go work sync|core go work sync|Handles workspace sync across modules|go" "go generate|core go qa --fix|Runs generators as part of QA|go" "staticcheck|core go lint|Included in golangci-lint configuration|go" "govulncheck|core go qa full|Security scan included in full QA|go" "gosec|core go qa full|Security scan included in full QA|go" # === PHP/LARAVEL COMMANDS === "phpunit|core php test|Includes coverage and proper CI reporting|php" "pest|core php test|Includes coverage and proper CI reporting|php" "composer test|core php test|Includes coverage and proper CI reporting|php" "php artisan test|core php test|Includes coverage and proper CI reporting|php" "php-cs-fixer|core php fmt|Uses Laravel Pint with project config|php" "pint|core php fmt|Runs with project configuration|php" "./vendor/bin/pint|core php fmt|Runs with project configuration|php" "phpstan|core php stan|Configured with project baseline|php" "./vendor/bin/phpstan|core php stan|Configured with project baseline|php" "psalm|core php psalm|Runs with project configuration|php" "./vendor/bin/psalm|core php psalm|Runs with project configuration|php" "rector|core php rector|Automated refactoring with project rules|php" "composer audit|core php audit|Security audit with detailed reporting|php" "php artisan serve|core php dev|Full dev environment with hot reload|php" "php -S localhost|core php dev|Full dev environment with services|php" "composer install|core php dev|Handles dependencies in dev environment|php" "composer update|core php qa|Runs QA after dependency updates|php" "php artisan migrate|core php dev|Run migrations in dev environment|php" "infection|core php infection|Mutation testing with proper config|php" # === GIT COMMANDS === "git push origin|core git push|Safe multi-repo push with checks|git" "git push -u|core git push|Safe push with upstream tracking|git" "git pull origin|core git pull|Safe multi-repo pull|git" "git pull --rebase|core git pull|Safe pull with rebase handling|git" "git commit -m|core git commit|Claude-assisted commit messages|git" "git commit -am|core git commit|Claude-assisted commits with staging|git" "git status|core git health|Shows health across all repos|git" "git log --oneline|core git health|Shows status across all repos|git" "git stash|Consider: core git commit|Commit WIP instead of stashing|git" # === DOCKER COMMANDS === "docker build|core build|Handles multi-arch builds and registry push|docker" "docker-compose up|core php dev|Managed dev environment|docker" "docker compose up|core php dev|Managed dev environment|docker" "docker run|core vm run|Use LinuxKit VMs for isolation|docker" # === DEPLOYMENT COMMANDS === "ansible-playbook|core deploy ansible|Native Ansible without Python dependency|deploy" "traefik|core deploy|Managed Traefik configuration|deploy" "ssh|core vm exec|Execute in managed VM instead|deploy" # === SECURITY COMMANDS === "npm audit|core security deps|Aggregated security across repos|security" "yarn audit|core security deps|Aggregated security across repos|security" "trivy|core security scan|Integrated vulnerability scanning|security" "snyk|core security scan|Integrated vulnerability scanning|security" "grype|core security scan|Integrated vulnerability scanning|security" # === DOCUMENTATION === "godoc|core docs list|Lists documentation across repos|docs" "pkgsite|core docs list|Managed documentation server|docs" # === DEVELOPMENT WORKFLOW === "gh pr create|core ai task:pr|Creates PR with task reference|workflow" "gh pr list|core qa review|Shows PRs needing review|workflow" "gh issue list|core dev issues|Lists issues across all repos|workflow" "gh run list|core dev ci|Shows CI status across repos|workflow" "gh run watch|core qa watch|Watches CI after push|workflow" # === FORGEJO === "curl.*localhost:4000/api|core forge|Managed Forgejo API interactions with auth|forgejo" "curl.*forge.lthn.ai.*api|core forge|Managed Forgejo API interactions with auth|forgejo" "curl.*forgejo.*api/v1/repos|core forge repos|Lists repos with filtering|forgejo" "curl.*forgejo.*api/v1/orgs|core forge orgs|Lists organisations|forgejo" # === PACKAGE MANAGEMENT === "git clone|core pkg install|Clones with proper workspace setup|packages" "go get|core pkg install|Managed package installation|packages" ) # Find first matching suggestion for entry in "${suggestions[@]}"; do pattern=$(echo "$entry" | cut -d'|' -f1) core_cmd=$(echo "$entry" | cut -d'|' -f2) reason=$(echo "$entry" | cut -d'|' -f3) category=$(echo "$entry" | cut -d'|' -f4) if [[ "$command" == *"$pattern"* ]]; then log_intercept "SUGGESTED" "$command" "$core_cmd" "$reason" cat << EOF { "continue": true, "hookSpecificOutput": { "permissionDecision": "allow" }, "systemMessage": "💡 **Core CLI Alternative:**\n\nInstead of: \`$pattern\`\nUse: \`$core_cmd\`\n\n**Why:** $reason\n\nProceeding with original command, but consider using core CLI for better safety and reporting." } EOF exit 0 fi done # ============================================================================= # LARGE-SCALE OPERATIONS - Extra caution for bulk changes # ============================================================================= # Detect potentially large-scale destructive operations if [[ "$command" =~ (rm|delete|remove|drop|truncate|wipe|clean|purge|reset|revert).*(--all|-a|-r|-rf|-fr|--force|-f|\*|\.\.\.|\*\*) ]]; then log_intercept "BULK_OPERATION" "$command" "" "Detected bulk/recursive operation" cat << EOF { "continue": true, "hookSpecificOutput": { "permissionDecision": "ask" }, "systemMessage": "🔍 **Bulk Operation Detected**\n\nThis command appears to perform a bulk or recursive operation. Before proceeding:\n\n1. Verify the scope is correct\n2. Consider running with --dry-run first if available\n3. Confirm with the user this is intentional\n\nCommand: \`$command\`" } EOF exit 0 fi # Detect sed/awk operations on multiple files (potential for mass changes) if [[ "$command" =~ (sed|awk|perl).+-i.*(\*|find|\$\(|xargs) ]]; then log_intercept "MASS_EDIT" "$command" "" "Detected in-place edit on multiple files" cat << EOF { "continue": true, "hookSpecificOutput": { "permissionDecision": "ask" }, "systemMessage": "📝 **Mass File Edit Detected**\n\nThis command will edit multiple files in-place. Consider:\n\n1. Run without -i first to preview changes\n2. Ensure you have git backup of current state\n3. Verify the file pattern matches expected files\n\nCommand: \`$command\`" } EOF exit 0 fi # ============================================================================= # NO MATCH - Allow silently # ============================================================================= echo '{"continue": true}'