Consolidates three codebases into a single agent orchestration repo: - agentci (from go-scm): Clotho dual-run verification, agent config, SSH security (sanitisation, secure commands, token masking) - jobrunner (from go-scm): Poll-dispatch-report pipeline with 7 handlers (dispatch, completion, auto-merge, publish draft, dismiss reviews, send fix command, tick parent epic) - plugins marketplace (from agentic/plugins): 27 Claude/Codex/Gemini plugins with shared MCP server All 150+ tests passing across 6 packages. Co-Authored-By: Virgil <virgil@lethean.io>
277 lines
13 KiB
Bash
Executable file
277 lines
13 KiB
Bash
Executable file
#!/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}'
|