From ce169ecf3e833ce1a5bb71bd7687f95e9ad808da Mon Sep 17 00:00:00 2001 From: Mathis Pinsault Date: Tue, 1 Sep 2026 10:17:12 +0200 Subject: [PATCH 1/5] chore: upgrade workflows and action to Node 24 Co-authored-by: Claude --- .../hooks/security-guard/security-guard.sh | 620 +++++++++++++++++ .../security-guard/test-security-guard.sh | 627 ++++++++++++++++++ .../skills/branch-commit-pr-ticket/SKILL.md | 526 +++++++++++++++ .claude/settings.json | 16 + .claude/skills/branch-commit-pr-ticket | 1 + .github/workflows/npm.yml | 18 +- action.yml | 2 +- ai-lock.json | 48 ++ 8 files changed, 1848 insertions(+), 10 deletions(-) create mode 100755 .agents/hooks/security-guard/security-guard.sh create mode 100755 .agents/hooks/security-guard/test-security-guard.sh create mode 100644 .agents/skills/branch-commit-pr-ticket/SKILL.md create mode 100644 .claude/settings.json create mode 120000 .claude/skills/branch-commit-pr-ticket create mode 100644 ai-lock.json diff --git a/.agents/hooks/security-guard/security-guard.sh b/.agents/hooks/security-guard/security-guard.sh new file mode 100755 index 0000000..5c75119 --- /dev/null +++ b/.agents/hooks/security-guard/security-guard.sh @@ -0,0 +1,620 @@ +#!/bin/bash +# +# Security Guard Hook for Claude Code and Cursor +# Blocks dangerous and irreversible commands to enable safe usage with --dangerously-skip-permissions +# +# Supports: +# - Claude Code: PreToolUse (tool_name: Bash, Edit, Write, Read) +# - Cursor: preToolUse (tool_name: Shell, Edit, Write, Read), beforeShellExecution, beforeReadFile +# +# Exit codes: +# 0 - Allow the command +# 2 - Block the command (message sent to Claude via stderr) +# +# No external dependencies required - uses only bash built-ins and standard Unix tools (grep, sed) +# + +set -euo pipefail + +# Read JSON input from stdin +INPUT=$(cat) + +# ============================================================================ +# JSON PARSING (pure bash/grep/sed - no jq required) +# ============================================================================ + +# Extract a simple string value from JSON: "key": "value" +# Handles escaped quotes \" inside values +# Returns empty string if key not found +# Usage: json_get "key" <<< "$json" +json_get() { + local key="$1" + local result + # Match "key": "value" including escaped quotes, then extract and unescape + result=$(grep -oE "\"$key\"[[:space:]]*:[[:space:]]*\"([^\"\\\\]|\\\\.)*\"" 2>/dev/null | head -1 || true) + if [[ -n "$result" ]]; then + # Extract value between quotes and unescape \" + echo "$result" | sed "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"\\(.*\\)\"/\\1/" | sed 's/\\"/"/g' + fi +} + +# Extract tool_name and nested values from input +# Cursor uses "Shell" for shell commands; Claude Code uses "Bash" +# Cursor beforeShellExecution/beforeReadFile send command/file_path at top level (no tool_name) +TOOL_NAME=$(echo "$INPUT" | json_get "tool_name") +COMMAND=$(echo "$INPUT" | json_get "command") +FILE_PATH=$(echo "$INPUT" | json_get "file_path") +HOOK_EVENT=$(echo "$INPUT" | json_get "hook_event_name") +CURSOR_VERSION=$(echo "$INPUT" | json_get "cursor_version") + +# Helper function to block with message +block() { + echo "SECURITY GUARD: $1" >&2 + exit 2 +} + +# Cursor expects JSON output when allowing; Claude Code uses exit code only +allow() { + [[ -n "$CURSOR_VERSION" ]] && echo '{"permission":"allow"}' + exit 0 +} + +# ============================================================================ +# BASH COMMAND VALIDATION +# ============================================================================ +check_bash_command() { + local cmd="$1" + + # Normalize command (lowercase for some checks) + local cmd_lower=$(echo "$cmd" | tr '[:upper:]' '[:lower:]') + + # Strip quoted strings (single, double, heredoc bodies) to avoid false positives + # on content inside commit messages, echo statements, etc. + local cmd_no_quotes + cmd_no_quotes=$(echo "$cmd" | sed -E "s/'[^']*'//g; s/\"[^\"]*\"//g") + + # --- SUDO OPERATIONS --- + # Block all sudo commands - Claude Code should not require elevated privileges + if echo "$cmd" | grep -qE '^\s*sudo\s+|;\s*sudo\s+|\|\s*sudo\s+|&&\s*sudo\s+|\(\s*sudo\s+|\$\(\s*sudo\s+'; then + block "sudo commands are blocked. Claude Code should not require elevated privileges." + fi + + # --- GIT DANGEROUS OPERATIONS --- + + # Force push (blocks all force push variants) + if echo "$cmd" | grep -qE 'git\s+push\s+(.*\s)?(-f|--force|--force-with-lease)(\s|$)'; then + block "git push --force is blocked. If you need to force push, please run the command manually in your terminal." + fi + + # Hard reset + if echo "$cmd" | grep -qE 'git\s+reset\s+--hard'; then + block "git reset --hard is blocked. This discards all uncommitted changes irreversibly." + fi + + # Clean with force + if echo "$cmd" | grep -qE 'git\s+clean\s+-[a-z]*f'; then + block "git clean -f is blocked. This permanently deletes untracked files." + fi + + # Rebase on shared branches + if echo "$cmd" | grep -qE 'git\s+rebase\s+(--\S+\s+)*(main|master|develop|release)(\s|$)'; then + block "Rebasing onto shared branches is blocked. If you need to rebase, please run the command manually in your terminal." + fi + + # --- DATABASE DESTRUCTIVE OPERATIONS --- + + # DROP operations + if echo "$cmd_lower" | grep -qE '(drop\s+(database|table|schema|index|view|trigger|procedure|function))'; then + block "DROP DATABASE/TABLE/SCHEMA is blocked. This is irreversible data loss." + fi + + # TRUNCATE + if echo "$cmd_lower" | grep -qE 'truncate\s+table'; then + block "TRUNCATE TABLE is blocked. This deletes all data irreversibly." + fi + + # DELETE without WHERE (check if DELETE FROM exists without WHERE in the same command) + # + # Known limitation: This pattern may produce false negatives if 'where' appears + # in a SQL comment or string literal (e.g., "DELETE FROM x; -- where is this"). + # We accept this limitation because: + # 1. Claude Code generates straightforward SQL, not obfuscated queries + # 2. Proper SQL parsing in bash would be overly complex and fragile + # 3. This is a heuristic safety net, not a SQL validator + # + if echo "$cmd_lower" | grep -qE 'delete\s+from\s+[a-z_]+' && ! echo "$cmd_lower" | grep -qiE 'where\s+'; then + block "DELETE FROM without WHERE clause is blocked. This deletes all rows." + fi + + # Redis destructive operations + if echo "$cmd_lower" | grep -qE 'redis-cli\s+.*(flushall|flushdb|debug\s+segfault)'; then + block "Redis FLUSHALL/FLUSHDB is blocked. This deletes all data." + fi + + # MongoDB destructive operations (patterns in lowercase since we use cmd_lower) + if echo "$cmd_lower" | grep -qE '(mongo|mongosh).*(dropdatabase|dropcollection)'; then + block "MongoDB dropDatabase/dropCollection is blocked. This deletes all data." + fi + + # --- FILESYSTEM DESTRUCTIVE OPERATIONS --- + + # rm -rf on dangerous paths (but allow ./relative/paths) + # Block: /home, /usr, /var, /etc, /opt, .. + if echo "$cmd_no_quotes" | grep -qE 'rm\s+-[a-zA-Z]*[rR][a-zA-Z]*[fF]?\s+(/home|/usr|/var|/etc|/opt|/boot|/bin|/sbin|/lib)(/([\s*]|$)|\s|$)'; then + block "Recursive delete on system directories is blocked." + fi + + # Block parent directory traversal (..) + if echo "$cmd" | grep -qE 'rm\s+-[a-zA-Z]*[rR][a-zA-Z]*[fF]?\s+\.\.'; then + block "Recursive delete on parent directory (..) is blocked." + fi + + # rm -rf /* or rm -rf / or rm -rf ~, ~/, ~/* (home root only) + # Note: ~/ must NOT match sub-paths like ~/Documents/repo — those are personal + # directories and are intentionally allowed (see FTTECH-11113). Only the home + # root itself (~, ~/, ~/*) is catastrophic. + if echo "$cmd_no_quotes" | grep -qE 'rm\s+-[a-zA-Z]*[rR][a-zA-Z]*[fF]?\s+(/\*|/\s*$|~/\*|~/\s*$|~\s*$)'; then + block "Recursive delete on root or home is blocked. This is catastrophic." + fi + + # Note: We intentionally don't block rm with variable expansion (e.g., rm -rf $DIR) + # because it causes too many false positives for legitimate cleanup operations. + # Users should be careful with unquoted variables in rm commands. + + # chmod 777 + if echo "$cmd" | grep -qE 'chmod\s+(-R\s+)?777'; then + block "chmod 777 is blocked. This is a security vulnerability." + fi + + # chown to root + if echo "$cmd" | grep -qE 'chown\s+(-R\s+)?root'; then + block "chown to root is blocked for safety." + fi + + # Disk operations + if echo "$cmd" | grep -qE '(mkfs|fdisk|parted|dd\s+if=)'; then + block "Disk formatting/partitioning operations are blocked." + fi + + # Fork bomb + if echo "$cmd" | grep -qE ':\(\)\s*\{.*:\s*&\s*\}\s*;?\s*:'; then + block "Fork bomb detected and blocked." + fi + + # Direct write to devices + if echo "$cmd" | grep -qE '>\s*/dev/(sd|hd|nvme|vd)'; then + block "Direct write to disk devices is blocked." + fi + + # --- DOCKER DANGEROUS OPERATIONS --- + + # Block ALL container removal (docker rm, docker remove, docker container rm) + if echo "$cmd" | grep -qE 'docker\s+(rm|remove|container\s+rm|container\s+remove)\s'; then + block "Removing Docker containers is blocked." + fi + + # Block docker stop (can disrupt services) + if echo "$cmd" | grep -qE 'docker\s+(stop|kill|container\s+stop|container\s+kill)\s'; then + block "Stopping/killing Docker containers is blocked." + fi + + # System prune (any variant) + if echo "$cmd" | grep -qE 'docker\s+system\s+prune'; then + block "docker system prune is blocked. This removes unused data." + fi + + # Volume prune or remove + if echo "$cmd" | grep -qE 'docker\s+volume\s+(prune|rm|remove)'; then + block "Docker volume deletion is blocked." + fi + + # Image removal + if echo "$cmd" | grep -qE 'docker\s+(rmi|image\s+rm|image\s+remove)\s'; then + block "Docker image removal is blocked." + fi + + # Network removal + if echo "$cmd" | grep -qE 'docker\s+network\s+(rm|remove)\s'; then + block "Docker network removal is blocked." + fi + + # --- KUBERNETES DANGEROUS OPERATIONS --- + + # Delete namespace + if echo "$cmd" | grep -qE 'kubectl\s+delete\s+(namespace|ns)\s'; then + block "kubectl delete namespace is blocked. This deletes all resources in the namespace." + fi + + # Delete all resources + if echo "$cmd" | grep -qE 'kubectl\s+.*delete\s+.*--all|kubectl\s+delete\s+[a-z]+\s+--all'; then + block "kubectl delete --all is blocked." + fi + + # Delete in production (check for -n prod or --namespace=prod anywhere in command) + if echo "$cmd" | grep -qE 'kubectl.*(-n[= ](prod|production)|--namespace[= ](prod|production)).*delete|kubectl.*delete.*(-n[= ](prod|production)|--namespace[= ](prod|production))'; then + block "kubectl delete in production namespace is blocked." + fi + + # --- CLOUD PROVIDER DANGEROUS OPERATIONS --- + + # AWS S3 recursive delete + if echo "$cmd" | grep -qE 'aws\s+s3\s+(rm|rb)\s+.*--recursive'; then + block "aws s3 rm --recursive is blocked. This deletes all objects." + fi + + # AWS EC2 destructive operations + if echo "$cmd" | grep -qE 'aws\s+ec2\s+(terminate-instances|delete-|modify-instance-attribute)'; then + block "aws ec2 destructive operation is blocked." + fi + + # AWS RDS destructive operations + if echo "$cmd" | grep -qE 'aws\s+rds\s+(delete-db-instance|delete-db-cluster|delete-db-snapshot)'; then + block "aws rds delete operation is blocked. This can cause data loss." + fi + + # AWS Lambda destructive operations + if echo "$cmd" | grep -qE 'aws\s+lambda\s+(delete-function|delete-layer-version)'; then + block "aws lambda delete operation is blocked." + fi + + # AWS IAM destructive operations + if echo "$cmd" | grep -qE 'aws\s+iam\s+(delete-user|delete-role|delete-policy|remove-user-from-group)'; then + block "aws iam destructive operation is blocked." + fi + + # AWS CloudFormation destructive operations + if echo "$cmd" | grep -qE 'aws\s+cloudformation\s+(delete-stack|delete-stack-set)'; then + block "aws cloudformation delete operation is blocked." + fi + + # AWS general delete pattern (catch-all for other services) + if echo "$cmd" | grep -qE 'aws\s+[a-z0-9-]+\s+delete-'; then + block "aws delete operation is blocked. Review and run manually if needed." + fi + + # Terraform destroy + if echo "$cmd" | grep -qE 'terraform\s+destroy'; then + block "terraform destroy is blocked. Run this manually with proper review." + fi + + # --- PACKAGE MANAGER DANGEROUS OPERATIONS --- + + # Global package installations (npm, pnpm, yarn) + # These modify system-wide packages and can install malicious code globally + if echo "$cmd" | grep -qE 'npm\s+(install|i)\s+.*-g|npm\s+(install|i)\s+-g'; then + block "npm global install (-g) is blocked. Install packages locally instead." + fi + + if echo "$cmd" | grep -qE 'pnpm\s+(add|install|i)\s+.*-g|pnpm\s+(add|install|i)\s+-g'; then + block "pnpm global install (-g) is blocked. Install packages locally instead." + fi + + if echo "$cmd" | grep -qE 'yarn\s+global\s+add'; then + block "yarn global add is blocked. Install packages locally instead." + fi + + # npm unpublish + if echo "$cmd" | grep -qE 'npm\s+unpublish'; then + block "npm unpublish is blocked. This can break dependent packages." + fi + + # --- PROCESS/SYSTEM OPERATIONS --- + + # Kill all processes or dangerous killall usage + if echo "$cmd" | grep -qE 'kill\s+-9\s+(-1|0)|killall\s+(-9\s+)?(-1|init|systemd|kernel)'; then + block "Killing system processes is blocked." + fi + + # Shutdown/reboot + if echo "$cmd" | grep -qE '(shutdown|reboot|init\s+[06])'; then + block "System shutdown/reboot is blocked." + fi + + # --- CURL/WGET PIPE TO SHELL --- + + if echo "$cmd" | grep -qE '(curl|wget)\s+.*\|\s*(ba)?sh'; then + block "Piping downloaded content to shell is blocked. Download and review first." + fi + + # --- HISTORY/CREDENTIAL MANIPULATION --- + + # Clear history + if echo "$cmd" | grep -qE '(history\s+-c|>\s*~/\..*history)'; then + block "Clearing shell history is blocked." + fi + + # --- SENSITIVE FILE READING --- + + # Block reading sensitive files via common commands + # Matches: cat, head, tail, less, more, bat, view, vim, nano, etc. + local read_cmds='(cat|head|tail|less|more|bat|view|vim|vi|nano|grep|awk|sed|strings|xxd|hexdump|od|file|stat|wc)' + + # --- SYSTEM AUTHENTICATION FILES --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*/etc/(passwd|shadow|sudoers|gshadow|master\.passwd|login\.defs)"; then + block "Reading system authentication files is blocked." + fi + + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*/etc/(security|pam\.d)/"; then + block "Reading PAM/security configuration is blocked." + fi + + # --- SSH FILES --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*/etc/ssh/"; then + block "Reading SSH server configuration is blocked." + fi + + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*\.ssh/(id_[a-z_]+|identity|config|known_hosts|authorized_keys)"; then + block "Reading SSH files is blocked." + fi + + # --- SSL/TLS PRIVATE KEYS --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*/etc/(ssl/private|pki)/"; then + block "Reading SSL/TLS private keys is blocked." + fi + + # --- CLOUD CREDENTIALS --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*\.(aws|azure|gcloud|config/gcloud)/"; then + block "Reading cloud provider credentials is blocked." + fi + + # --- ENVIRONMENT FILES --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*\.env(\.[a-z]+)?(\s|$|;|\|)"; then + block "Reading .env files is blocked. These may contain secrets." + fi + + # --- GIT CREDENTIALS --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*(\.git-credentials|\.netrc|\.gitconfig)(\s|$|;|\|)"; then + block "Reading git credentials/config is blocked." + fi + + # --- PACKAGE MANAGER CONFIGS --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*(\.npmrc|\.yarnrc|\.pypirc|\.gem/credentials)"; then + block "Reading package manager config is blocked." + fi + + # --- KUBERNETES/DOCKER --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*(\.kube/config|\.docker/config\.json)"; then + block "Reading container orchestration config is blocked." + fi + + # --- BROWSER DATA --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*(Chrome|Firefox|Safari|Brave|Edge).*(Cookies|Login|History|Passwords)"; then + block "Reading browser data is blocked." + fi + + # --- KEYCHAIN/KEYRING --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*(\.gnupg|\.password-store|keyrings|Keychains)/"; then + block "Reading keychain/keyring data is blocked." + fi + + # --- SYSTEM LOGS --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*/var/log/(auth|secure|audit)"; then + block "Reading system authentication logs is blocked." + fi + + # --- SECRETS DIRECTORIES --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*(^|/)(secrets?|credentials?|private)/"; then + block "Reading from secrets/credentials directories is blocked." + fi + + # --- SHELL HISTORY --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*(\.bash_history|\.zsh_history|\.sh_history)"; then + block "Reading shell history is blocked." + fi + + # --- PRIVATE KEYS --- + if echo "$cmd_no_quotes" | grep -qE "$read_cmds\s+.*\.(pem|key|p12|pfx|jks)(\s|$|;|\|)"; then + block "Reading private key files is blocked." + fi +} + +# ============================================================================ +# FILE PATH VALIDATION (for Edit, Write, Read tools) +# ============================================================================ +check_file_path() { + local file_path="$1" + local tool="$2" + + # Block reading sensitive files + if [[ "$tool" == "Read" ]]; then + + # --- SYSTEM AUTHENTICATION FILES --- + if echo "$file_path" | grep -qE '^/etc/(passwd|shadow|sudoers|sudoers\.d/|gshadow|master\.passwd|login\.defs|security/)'; then + block "Reading system authentication files is blocked." + fi + + # PAM configuration + if echo "$file_path" | grep -qE '^/etc/pam\.d/'; then + block "Reading PAM configuration is blocked." + fi + + # --- SSH FILES --- + # Server and user SSH keys and configs + if echo "$file_path" | grep -qE '^/etc/ssh/'; then + block "Reading SSH server configuration is blocked." + fi + + if echo "$file_path" | grep -qE '\.ssh/(id_[^.]+|identity|config|known_hosts|authorized_keys)'; then + block "Reading SSH files is blocked." + fi + + # --- SSL/TLS PRIVATE KEYS --- + if echo "$file_path" | grep -qE '^/etc/ssl/private/|^/etc/pki/'; then + block "Reading SSL/TLS private keys is blocked." + fi + + # --- CLOUD CREDENTIALS --- + if echo "$file_path" | grep -qE '\.(aws|azure|gcloud|config/gcloud)/'; then + block "Reading cloud provider credentials is blocked." + fi + + # --- ENVIRONMENT FILES --- + if echo "$file_path" | grep -qE '\.env(\.[a-z]+)?$'; then + block "Reading .env files is blocked. These may contain secrets." + fi + + # --- GIT CREDENTIALS --- + if echo "$file_path" | grep -qE '(\.git-credentials|\.netrc|\.gitconfig)$'; then + block "Reading git credentials/config is blocked." + fi + + # --- PACKAGE MANAGER CONFIGS (may contain tokens) --- + if echo "$file_path" | grep -qE '\.(npmrc|yarnrc|pip/pip\.conf|pypirc|gem/credentials|composer/auth\.json)'; then + block "Reading package manager config is blocked. May contain auth tokens." + fi + + # --- KUBERNETES/DOCKER --- + if echo "$file_path" | grep -qE '\.(kube/config|docker/config\.json)'; then + block "Reading container orchestration config is blocked." + fi + + # --- BROWSER DATA --- + if echo "$file_path" | grep -qE '(Chrome|Firefox|Safari|Brave|Edge|Opera).*(Cookies|Login Data|History|Passwords|Web Data)'; then + block "Reading browser data is blocked." + fi + + # --- KEYCHAIN/KEYRING --- + if echo "$file_path" | grep -qE '(\.gnupg/|\.password-store/|\.local/share/keyrings/|Library/Keychains/)'; then + block "Reading keychain/keyring data is blocked." + fi + + # --- SYSTEM LOGS (may contain sensitive info) --- + if echo "$file_path" | grep -qE '^/var/log/(auth|secure|audit|syslog|messages)'; then + block "Reading system authentication logs is blocked." + fi + + # --- DATABASE FILES --- + if echo "$file_path" | grep -qE '\.(sqlite|sqlite3|db)$' && echo "$file_path" | grep -qiE '(password|credential|secret|token|key|auth)'; then + block "Reading potentially sensitive database files is blocked." + fi + + # --- SECRETS DIRECTORIES --- + if echo "$file_path" | grep -qE '(^|/)(secrets?|credentials?|private|\.secrets?|\.credentials?)/'; then + block "Reading from secrets/credentials directories is blocked." + fi + + # --- APPLICATION TOKENS/CONFIGS --- + if echo "$file_path" | grep -qE '(\.slack|\.discord|\.telegram|\.config/gh/hosts\.yml)'; then + block "Reading application tokens/configs is blocked." + fi + + # --- SHELL HISTORY (contains command history) --- + if echo "$file_path" | grep -qE '\.(bash_history|zsh_history|sh_history|history|psql_history|mysql_history|node_repl_history)'; then + block "Reading shell/command history is blocked." + fi + + # --- PRIVATE KEYS (generic) --- + if echo "$file_path" | grep -qE '\.(pem|key|p12|pfx|jks)$'; then + block "Reading private key files is blocked." + fi + fi + + # Block writes to sensitive files + if [[ "$tool" == "Write" || "$tool" == "Edit" ]]; then + + # Environment files with secrets + if echo "$file_path" | grep -qE '\.env(\.[a-z]+)?$'; then + block "Writing to .env files is blocked. These may contain secrets." + fi + + # AWS credentials + if echo "$file_path" | grep -qE '\.aws/(credentials|config)$'; then + block "Writing to AWS credentials is blocked." + fi + + # SSH keys + if echo "$file_path" | grep -qE '\.ssh/(id_|authorized_keys|known_hosts)'; then + block "Writing to SSH key files is blocked." + fi + + # Kubernetes config + if echo "$file_path" | grep -qE '\.kube/config$'; then + block "Writing to kubeconfig is blocked." + fi + + # Git credentials + if echo "$file_path" | grep -qE '\.git-credentials$|\.netrc$'; then + block "Writing to git credentials is blocked." + fi + + # npmrc with tokens + if echo "$file_path" | grep -qE '\.npmrc$'; then + block "Writing to .npmrc is blocked. May contain auth tokens." + fi + + # Package lock files (can cause dependency issues) + if echo "$file_path" | grep -qE '(package-lock\.json|yarn\.lock|pnpm-lock\.yaml)$'; then + block "Direct editing of lock files is blocked. Use package manager commands." + fi + + # Docker secrets + if echo "$file_path" | grep -qE '\.docker/config\.json$'; then + block "Writing to Docker config is blocked. May contain registry credentials." + fi + + # System files + if echo "$file_path" | grep -qE '^/(etc|usr|var|boot|sys|proc)/'; then + block "Writing to system directories is blocked." + fi + + # Secrets directories (match as complete directory components) + if echo "$file_path" | grep -qE '(^|/)(secrets?|credentials?|private)/'; then + block "Writing to secrets/credentials directories is blocked. Review manually." + fi + fi +} + +# ============================================================================ +# MCP TOOL VALIDATION +# ============================================================================ +check_mcp_tool() { + local tool_name="$1" + + # Block potentially dangerous MCP operations + # Add specific MCP server checks here based on your setup + + # Example: block destructive database MCP operations + if echo "$tool_name" | grep -qE 'mcp__.*__(delete|drop|truncate|destroy)'; then + block "Destructive MCP operation detected: $tool_name" + fi +} + +# ============================================================================ +# MAIN LOGIC +# ============================================================================ + +# Cursor beforeShellExecution sends { "command": "..." } (no tool_name) +# Cursor beforeReadFile sends { "file_path": "..." } (no tool_name) +if [[ -z "$TOOL_NAME" && -n "$COMMAND" ]]; then + check_bash_command "$COMMAND" + allow +fi +if [[ -z "$TOOL_NAME" && -n "$FILE_PATH" ]]; then + check_file_path "$FILE_PATH" "Read" + allow +fi + +case "$TOOL_NAME" in + Bash|Shell) + if [[ -n "$COMMAND" ]]; then + check_bash_command "$COMMAND" + fi + ;; + + Edit|Write|Read) + if [[ -n "$FILE_PATH" ]]; then + check_file_path "$FILE_PATH" "$TOOL_NAME" + fi + ;; + + mcp__*) + check_mcp_tool "$TOOL_NAME" + ;; + + *) + # Allow other tools by default + ;; +esac + +# If we reach here, the command is allowed +allow diff --git a/.agents/hooks/security-guard/test-security-guard.sh b/.agents/hooks/security-guard/test-security-guard.sh new file mode 100755 index 0000000..1810817 --- /dev/null +++ b/.agents/hooks/security-guard/test-security-guard.sh @@ -0,0 +1,627 @@ +#!/bin/bash +# +# Test suite for security-guard.sh +# Run this script to visualize which commands are blocked or allowed +# + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SECURITY_GUARD="$SCRIPT_DIR/security-guard.sh" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color +BOLD='\033[1m' + +# Counters +PASSED=0 +FAILED=0 + +# Helper to create JSON input for Bash tool +bash_input() { + local cmd="$1" + cat <&1) + exit_code=$? + + if [[ $exit_code -eq 2 ]]; then + echo -e " ${BLUE}✗ BLOCKED${NC} $description" + ((PASSED++)) + else + echo -e " ${GREEN}✓ ALLOWED${NC} $description ${YELLOW}(EXPECTED BLOCKED!)${NC}" + ((FAILED++)) + fi +} + +# Test a command expecting it to be ALLOWED +expect_allowed() { + local description="$1" + local input="$2" + + output=$(echo "$input" | "$SECURITY_GUARD" 2>&1) + exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + echo -e " ${GREEN}✓ ALLOWED${NC} $description" + ((PASSED++)) + else + echo -e " ${BLUE}✗ BLOCKED${NC} $description ${YELLOW}(EXPECTED ALLOWED!)${NC}" + ((FAILED++)) + fi +} + +# Print section header +section() { + echo "" + echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BOLD}${BLUE} $1${NC}" + echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +# Print subsection +subsection() { + echo "" + echo -e " ${YELLOW}▸ $1${NC}" +} + +# ============================================================================ +# START TESTS +# ============================================================================ + +echo "" +echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}${CYAN}║ SECURITY GUARD TEST SUITE ║${NC}" +echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" + +# ============================================================================ +section "SUDO OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "sudo apt install" "$(bash_input 'sudo apt install nginx')" +expect_blocked "sudo rm" "$(bash_input 'sudo rm /tmp/test')" +expect_blocked "sudo systemctl" "$(bash_input 'sudo systemctl restart nginx')" +expect_blocked "sudo chown" "$(bash_input 'sudo chown root:root /etc/passwd')" +expect_blocked "command; sudo" "$(bash_input 'echo hello; sudo rm -rf /')" +expect_blocked "command | sudo" "$(bash_input 'cat file | sudo tee /etc/config')" +expect_blocked "command && sudo" "$(bash_input 'echo ok && sudo systemctl restart nginx')" +expect_blocked "sudo in subshell" "$(bash_input '(sudo apt remove package)')" +expect_blocked "sudo in command substitution" "$(bash_input '$(sudo cat /etc/shadow)')" + +subsection "Should be ALLOWED" +expect_allowed "echo sudo in string" "$(bash_input 'echo "run sudo command"')" + +# ============================================================================ +section "GIT OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "git push --force" "$(bash_input 'git push --force')" +expect_blocked "git push -f" "$(bash_input 'git push -f')" +expect_blocked "git push origin main --force" "$(bash_input 'git push origin main --force')" +expect_blocked "git push --force-with-lease" "$(bash_input 'git push --force-with-lease')" +expect_blocked "git reset --hard" "$(bash_input 'git reset --hard')" +expect_blocked "git reset --hard HEAD~3" "$(bash_input 'git reset --hard HEAD~3')" +expect_blocked "git clean -f" "$(bash_input 'git clean -f')" +expect_blocked "git clean -df" "$(bash_input 'git clean -df')" +expect_blocked "git clean -fd" "$(bash_input 'git clean -fd')" +expect_blocked "git clean -xfd" "$(bash_input 'git clean -xfd')" +expect_blocked "git rebase main" "$(bash_input 'git rebase main')" +expect_blocked "git rebase master" "$(bash_input 'git rebase master')" +expect_blocked "git rebase develop" "$(bash_input 'git rebase develop')" + +subsection "Should be ALLOWED" +expect_allowed "git push" "$(bash_input 'git push')" +expect_allowed "git push origin feature-branch" "$(bash_input 'git push origin feature-branch')" +expect_allowed "git pull" "$(bash_input 'git pull')" +expect_allowed "git fetch" "$(bash_input 'git fetch')" +expect_allowed "git status" "$(bash_input 'git status')" +expect_allowed "git log" "$(bash_input 'git log')" +expect_allowed "git diff" "$(bash_input 'git diff')" +expect_allowed "git commit -m 'message'" "$(bash_input 'git commit -m \"message\"')" +expect_allowed "git checkout -b new-branch" "$(bash_input 'git checkout -b new-branch')" +expect_allowed "git switch -c new-branch" "$(bash_input 'git switch -c new-branch')" +expect_allowed "git merge feature" "$(bash_input 'git merge feature')" +expect_allowed "git stash" "$(bash_input 'git stash')" +expect_allowed "git reset --soft HEAD~1" "$(bash_input 'git reset --soft HEAD~1')" +expect_allowed "git rebase feature-branch" "$(bash_input 'git rebase feature-branch')" +expect_allowed "git rebase feature-maintenance" "$(bash_input 'git rebase feature-maintenance')" +expect_allowed "git rebase fix/release-pipeline" "$(bash_input 'git rebase fix/release-pipeline')" +expect_allowed "git clean -n" "$(bash_input 'git clean -n')" +expect_allowed "git push -u origin fix/false-positives" "$(bash_input 'git push -u origin fix/false-positives')" +expect_allowed "git push origin feat/enforce-flag" "$(bash_input 'git push origin feat/enforce-flag')" +expect_allowed "git push --set-upstream origin fix/genai-detection-false-positives" "$(bash_input 'git push --set-upstream origin fix/genai-detection-false-positives')" +expect_allowed "git push origin branch-with--force-in-name" "$(bash_input 'git push origin branch-with--force-in-name')" + +# ============================================================================ +section "DATABASE OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED (SQL)" +expect_blocked "DROP DATABASE" "$(bash_input 'psql -c \"DROP DATABASE mydb\"')" +expect_blocked "DROP TABLE" "$(bash_input 'mysql -e \"DROP TABLE users\"')" +expect_blocked "DROP SCHEMA" "$(bash_input 'psql -c \"DROP SCHEMA public\"')" +expect_blocked "TRUNCATE TABLE" "$(bash_input 'psql -c \"TRUNCATE TABLE users\"')" +expect_blocked "DELETE FROM without WHERE" "$(bash_input 'psql -c \"DELETE FROM users;\"')" +expect_blocked "delete from (lowercase)" "$(bash_input 'mysql -e \"delete from logs;\"')" + +subsection "Should be BLOCKED (Redis)" +expect_blocked "redis-cli FLUSHALL" "$(bash_input 'redis-cli FLUSHALL')" +expect_blocked "redis-cli FLUSHDB" "$(bash_input 'redis-cli FLUSHDB')" +expect_blocked "redis-cli flushall (lowercase)" "$(bash_input 'redis-cli flushall')" +expect_blocked "redis-cli with host FLUSHALL" "$(bash_input 'redis-cli -h localhost FLUSHALL')" + +subsection "Should be BLOCKED (MongoDB)" +expect_blocked "mongo dropDatabase" "$(bash_input 'mongo mydb --eval \"db.dropDatabase()\"')" +expect_blocked "mongosh dropDatabase" "$(bash_input 'mongosh --eval \"db.dropDatabase()\"')" +expect_blocked "mongo dropCollection" "$(bash_input 'mongo --eval \"db.users.dropCollection()\"')" +expect_blocked "mongosh dropCollection" "$(bash_input 'mongosh mydb --eval \"db.dropCollection(users)\"')" + +subsection "Should be ALLOWED (SQL)" +expect_allowed "DELETE FROM with WHERE" "$(bash_input 'psql -c \"DELETE FROM users WHERE id = 5\"')" +expect_allowed "SELECT query" "$(bash_input 'psql -c \"SELECT * FROM users\"')" +expect_allowed "INSERT query" "$(bash_input 'psql -c \"INSERT INTO users VALUES (1)\"')" +expect_allowed "UPDATE query" "$(bash_input 'psql -c \"UPDATE users SET name = x WHERE id = 1\"')" +expect_allowed "CREATE TABLE" "$(bash_input 'psql -c \"CREATE TABLE test (id INT)\"')" + +subsection "Should be ALLOWED (Redis)" +expect_allowed "redis-cli GET" "$(bash_input 'redis-cli GET mykey')" +expect_allowed "redis-cli SET" "$(bash_input 'redis-cli SET mykey myvalue')" +expect_allowed "redis-cli KEYS" "$(bash_input 'redis-cli KEYS \"*\"')" + +subsection "Should be ALLOWED (MongoDB)" +expect_allowed "mongo find" "$(bash_input 'mongo mydb --eval \"db.users.find()\"')" +expect_allowed "mongosh insert" "$(bash_input 'mongosh --eval \"db.users.insertOne({name: 'test'})\"')" + +# ============================================================================ +section "FILESYSTEM OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "rm -rf /" "$(bash_input 'rm -rf /')" +expect_blocked "rm -rf /*" "$(bash_input 'rm -rf /*')" +expect_blocked "rm -rf /home" "$(bash_input 'rm -rf /home')" +expect_blocked "rm -rf /usr" "$(bash_input 'rm -rf /usr')" +expect_blocked "rm -rf /etc" "$(bash_input 'rm -rf /etc')" +expect_blocked "rm -rf .." "$(bash_input 'rm -rf ..')" +expect_blocked "rm -rf ~/" "$(bash_input 'rm -rf ~/')" +expect_blocked "rm -rf ~ (home root)" "$(bash_input 'rm -rf ~')" +expect_blocked "rm -rf ~/* (home glob)" "$(bash_input 'rm -rf ~/*')" +expect_blocked "rm -Rf /etc (uppercase R)" "$(bash_input 'rm -Rf /etc')" +expect_blocked "rm -Rf /home (uppercase R)" "$(bash_input 'rm -Rf /home')" +expect_blocked "rm -R /usr (uppercase R)" "$(bash_input 'rm -R /usr')" +expect_blocked "rm -Rf .. (uppercase R)" "$(bash_input 'rm -Rf ..')" +expect_blocked "rm -Rf ~/ (uppercase R)" "$(bash_input 'rm -Rf ~/')" +expect_blocked "rm -rf /etc/ (trailing slash)" "$(bash_input 'rm -rf /etc/')" +expect_blocked "rm -rf /usr/ (trailing slash)" "$(bash_input 'rm -rf /usr/')" +expect_blocked "rm -rf /home/ (trailing slash)" "$(bash_input 'rm -rf /home/')" +expect_allowed "rm -r with variable (false positive risk)" "$(bash_input 'rm -rf $DIR')" +expect_blocked "chmod 777" "$(bash_input 'chmod 777 /var/www')" +expect_blocked "chmod -R 777" "$(bash_input 'chmod -R 777 .')" +expect_blocked "chown root" "$(bash_input 'chown root file.txt')" +expect_blocked "chown -R root" "$(bash_input 'chown -R root /app')" +expect_blocked "mkfs" "$(bash_input 'mkfs.ext4 /dev/sda1')" +expect_blocked "fdisk" "$(bash_input 'fdisk /dev/sda')" +expect_blocked "dd if=/dev" "$(bash_input 'dd if=/dev/zero of=/dev/sda')" +expect_blocked "write to /dev/sda" "$(bash_input 'echo x > /dev/sda')" +expect_blocked "fork bomb" "$(bash_input ':(){ :|:& };:')" + +subsection "Should be ALLOWED" +expect_allowed "rm single file" "$(bash_input 'rm file.txt')" +expect_allowed "rm -f single file" "$(bash_input 'rm -f temp.log')" +expect_allowed "rm -rf local dir" "$(bash_input 'rm -rf ./node_modules')" +expect_allowed "rm -rf named dir" "$(bash_input 'rm -rf build')" +expect_allowed "rm -rf /Users/user/non-Documents path" "$(bash_input 'rm -rf /Users/pierresisson/sites/catalyst/src/app')" +expect_allowed "rm -rf /home/user/non-Documents path" "$(bash_input 'rm -rf /home/pierresisson/sites/catalyst/src/app')" +expect_allowed "rm -rf /Users/user/Documents (personal dir protection removed)" "$(bash_input 'rm -rf /Users/foo/Documents/projects')" +expect_allowed "rm -rf /home/user/Documents (personal dir protection removed)" "$(bash_input 'rm -rf /home/foo/Documents/projects')" +expect_allowed "rm -rf ~/Documents subpath (personal dir protection removed)" "$(bash_input 'rm -rf ~/Documents/projects')" +expect_allowed "rm -rf ~/Documents/repo/node_modules (personal dir protection removed)" "$(bash_input 'rm -rf ~/Documents/repo/node_modules')" +expect_allowed "chmod 755" "$(bash_input 'chmod 755 script.sh')" +expect_allowed "chmod 644" "$(bash_input 'chmod 644 file.txt')" +expect_allowed "chown user" "$(bash_input 'chown deploy:deploy app.js')" +expect_allowed "mkdir" "$(bash_input 'mkdir -p /app/logs')" +expect_allowed "cp files" "$(bash_input 'cp -r src/ dist/')" +expect_allowed "mv files" "$(bash_input 'mv old.txt new.txt')" + +# ============================================================================ +section "DOCKER OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "docker rm all containers" "$(bash_input 'docker rm -f $(docker ps -aq)')" +expect_blocked "docker remove all" "$(bash_input 'docker remove $(docker ps -q)')" +expect_blocked "docker rm single container" "$(bash_input 'docker rm container_id')" +expect_blocked "docker container rm" "$(bash_input 'docker container rm mycontainer')" +expect_blocked "docker stop container" "$(bash_input 'docker stop container_id')" +expect_blocked "docker kill container" "$(bash_input 'docker kill container_id')" +expect_blocked "docker system prune" "$(bash_input 'docker system prune')" +expect_blocked "docker system prune -a" "$(bash_input 'docker system prune -a')" +expect_blocked "docker system prune --all" "$(bash_input 'docker system prune --all')" +expect_blocked "docker volume prune" "$(bash_input 'docker volume prune')" +expect_blocked "docker volume rm" "$(bash_input 'docker volume rm myvolume')" +expect_blocked "docker rmi" "$(bash_input 'docker rmi nginx:latest')" +expect_blocked "docker image rm" "$(bash_input 'docker image rm myimage')" +expect_blocked "docker network rm" "$(bash_input 'docker network rm mynetwork')" + +subsection "Should be ALLOWED" +expect_allowed "docker ps" "$(bash_input 'docker ps')" +expect_allowed "docker images" "$(bash_input 'docker images')" +expect_allowed "docker build" "$(bash_input 'docker build -t myapp .')" +expect_allowed "docker run" "$(bash_input 'docker run -d nginx')" +expect_allowed "docker logs" "$(bash_input 'docker logs container_id')" +expect_allowed "docker exec" "$(bash_input 'docker exec -it container_id bash')" +expect_allowed "docker inspect" "$(bash_input 'docker inspect container_id')" +expect_allowed "docker-compose up" "$(bash_input 'docker-compose up -d')" +expect_allowed "docker-compose down" "$(bash_input 'docker-compose down')" + +# ============================================================================ +section "KUBERNETES OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "kubectl delete namespace" "$(bash_input 'kubectl delete namespace production')" +expect_blocked "kubectl delete ns" "$(bash_input 'kubectl delete ns staging')" +expect_blocked "kubectl delete --all" "$(bash_input 'kubectl delete pods --all')" +expect_blocked "kubectl delete in prod" "$(bash_input 'kubectl -n production delete deployment app')" +expect_blocked "kubectl delete --namespace=prod" "$(bash_input 'kubectl --namespace=prod delete svc api')" + +subsection "Should be ALLOWED" +expect_allowed "kubectl get pods" "$(bash_input 'kubectl get pods')" +expect_allowed "kubectl get all" "$(bash_input 'kubectl get all -n staging')" +expect_allowed "kubectl describe" "$(bash_input 'kubectl describe pod nginx')" +expect_allowed "kubectl logs" "$(bash_input 'kubectl logs -f pod/app')" +expect_allowed "kubectl apply" "$(bash_input 'kubectl apply -f deployment.yaml')" +expect_allowed "kubectl delete single pod" "$(bash_input 'kubectl delete pod nginx-123')" +expect_allowed "kubectl delete in dev" "$(bash_input 'kubectl -n development delete pod test')" + +# ============================================================================ +section "CLOUD PROVIDER OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED (AWS S3)" +expect_blocked "aws s3 rm --recursive" "$(bash_input 'aws s3 rm s3://bucket --recursive')" +expect_blocked "aws s3 rb --recursive" "$(bash_input 'aws s3 rb s3://bucket --recursive')" + +subsection "Should be BLOCKED (AWS EC2)" +expect_blocked "aws ec2 terminate" "$(bash_input 'aws ec2 terminate-instances --instance-ids i-123')" +expect_blocked "aws ec2 delete-vpc" "$(bash_input 'aws ec2 delete-vpc --vpc-id vpc-123')" +expect_blocked "aws ec2 delete-subnet" "$(bash_input 'aws ec2 delete-subnet --subnet-id subnet-123')" + +subsection "Should be BLOCKED (AWS RDS)" +expect_blocked "aws rds delete-db-instance" "$(bash_input 'aws rds delete-db-instance --db-instance-identifier mydb')" +expect_blocked "aws rds delete-db-cluster" "$(bash_input 'aws rds delete-db-cluster --db-cluster-identifier mycluster')" +expect_blocked "aws rds delete-db-snapshot" "$(bash_input 'aws rds delete-db-snapshot --db-snapshot-identifier mysnap')" + +subsection "Should be BLOCKED (AWS Lambda)" +expect_blocked "aws lambda delete-function" "$(bash_input 'aws lambda delete-function --function-name myFunc')" +expect_blocked "aws lambda delete-layer-version" "$(bash_input 'aws lambda delete-layer-version --layer-name myLayer --version 1')" + +subsection "Should be BLOCKED (AWS IAM)" +expect_blocked "aws iam delete-user" "$(bash_input 'aws iam delete-user --user-name testuser')" +expect_blocked "aws iam delete-role" "$(bash_input 'aws iam delete-role --role-name testrole')" +expect_blocked "aws iam delete-policy" "$(bash_input 'aws iam delete-policy --policy-arn arn:aws:iam::123:policy/test')" + +subsection "Should be BLOCKED (AWS CloudFormation)" +expect_blocked "aws cloudformation delete-stack" "$(bash_input 'aws cloudformation delete-stack --stack-name mystack')" + +subsection "Should be BLOCKED (Terraform)" +expect_blocked "terraform destroy" "$(bash_input 'terraform destroy')" +expect_blocked "terraform destroy -auto-approve" "$(bash_input 'terraform destroy -auto-approve')" + +subsection "Should be ALLOWED" +expect_allowed "aws s3 ls" "$(bash_input 'aws s3 ls')" +expect_allowed "aws s3 cp" "$(bash_input 'aws s3 cp file.txt s3://bucket/')" +expect_allowed "aws s3 sync" "$(bash_input 'aws s3 sync ./dist s3://bucket')" +expect_allowed "aws ec2 describe" "$(bash_input 'aws ec2 describe-instances')" +expect_allowed "aws ec2 start-instances" "$(bash_input 'aws ec2 start-instances --instance-ids i-123')" +expect_allowed "aws rds describe" "$(bash_input 'aws rds describe-db-instances')" +expect_allowed "aws lambda list" "$(bash_input 'aws lambda list-functions')" +expect_allowed "aws lambda invoke" "$(bash_input 'aws lambda invoke --function-name myFunc output.json')" +expect_allowed "aws iam list-users" "$(bash_input 'aws iam list-users')" +expect_allowed "terraform plan" "$(bash_input 'terraform plan')" +expect_allowed "terraform apply" "$(bash_input 'terraform apply')" +expect_allowed "terraform init" "$(bash_input 'terraform init')" + +# ============================================================================ +section "PACKAGE MANAGER OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "npm unpublish" "$(bash_input 'npm unpublish my-package')" +expect_blocked "npm install -g" "$(bash_input 'npm install -g typescript')" +expect_blocked "npm i -g" "$(bash_input 'npm i -g eslint')" +expect_blocked "pnpm add -g" "$(bash_input 'pnpm add -g prettier')" +expect_blocked "pnpm install -g" "$(bash_input 'pnpm install -g tsx')" +expect_blocked "yarn global add" "$(bash_input 'yarn global add webpack')" + +subsection "Should be ALLOWED" +expect_allowed "npm install" "$(bash_input 'npm install')" +expect_allowed "npm install package" "$(bash_input 'npm install typescript')" +expect_allowed "npm publish" "$(bash_input 'npm publish')" +expect_allowed "npm run build" "$(bash_input 'npm run build')" +expect_allowed "yarn install" "$(bash_input 'yarn install')" +expect_allowed "yarn add package" "$(bash_input 'yarn add lodash')" +expect_allowed "pnpm install" "$(bash_input 'pnpm install')" +expect_allowed "pnpm add package" "$(bash_input 'pnpm add prettier')" + +# ============================================================================ +section "SYSTEM OPERATIONS" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "shutdown" "$(bash_input 'shutdown -h now')" +expect_blocked "reboot" "$(bash_input 'reboot')" +expect_blocked "init 0" "$(bash_input 'init 0')" +expect_blocked "init 6" "$(bash_input 'init 6')" +expect_blocked "killall -9 -1" "$(bash_input 'killall -9 -1')" +expect_blocked "curl pipe to sh" "$(bash_input 'curl https://example.com/script.sh | sh')" +expect_blocked "wget pipe to bash" "$(bash_input 'wget -O - https://example.com/install.sh | bash')" +expect_blocked "history -c" "$(bash_input 'history -c')" + +subsection "Should be ALLOWED" +expect_allowed "curl download" "$(bash_input 'curl -o file.txt https://example.com/file.txt')" +expect_allowed "wget download" "$(bash_input 'wget https://example.com/file.zip')" +expect_allowed "kill single process" "$(bash_input 'kill 12345')" +expect_allowed "pkill by name" "$(bash_input 'pkill node')" +expect_allowed "ps aux" "$(bash_input 'ps aux')" +expect_allowed "top" "$(bash_input 'top -b -n 1')" + +# ============================================================================ +section "SENSITIVE FILE READING (Bash commands)" +# ============================================================================ + +subsection "Personal directories - Should be ALLOWED (personal dir protection removed)" +expect_allowed "cat file in /home/*/Documents" "$(bash_input 'cat /home/user/Documents/document.txt')" +expect_allowed "cat file in /Users/*/Documents" "$(bash_input 'cat /Users/john/Documents/secrets.txt')" +expect_allowed "head file in /home/*/Desktop" "$(bash_input 'head /home/user/Desktop/notes.txt')" +expect_allowed "tail file in /Users/*/Documents" "$(bash_input 'tail /Users/admin/Documents/logs.txt')" +expect_allowed "less file in /home/*/Downloads" "$(bash_input 'less /home/dev/Downloads/config.yml')" +expect_allowed "ls /home/*/Documents" "$(bash_input 'ls /home/user/Documents/')" +expect_allowed "ls /Users/*/Documents" "$(bash_input 'ls /Users/john/Documents/')" +expect_allowed "find in /home/*/Documents" "$(bash_input 'find /home/user/Documents -name \"*.txt\"')" +expect_allowed "find in /Users/*/Documents" "$(bash_input 'find /Users/john/Documents -type f')" +expect_allowed "cat file in /home/*/Pictures" "$(bash_input 'cat /home/user/Pictures/photo.txt')" +expect_allowed "cat file in /home/*/Music" "$(bash_input 'cat /home/user/Music/playlist.txt')" +expect_allowed "cat file in /home/*/Videos" "$(bash_input 'cat /home/user/Videos/list.txt')" +expect_allowed "cat file in ~/Documents (via ~)" "$(bash_input 'cat ~/Documents/document.txt')" + +subsection "Home directories (non-personal) - Should be ALLOWED" +expect_allowed "cat file in /Users non-Documents" "$(bash_input 'cat /Users/john/secrets.txt')" +expect_allowed "tail file in /Users non-Documents" "$(bash_input 'tail /Users/admin/logs.txt')" +expect_allowed "find in /Users non-Documents" "$(bash_input 'find /Users/john/projects -type f')" +expect_allowed "cat file in /home non-personal" "$(bash_input 'cat /home/user/project/config.yml')" +expect_allowed "head file in /home non-personal" "$(bash_input 'head /home/user/.bashrc')" +expect_allowed "ls /home non-personal" "$(bash_input 'ls /home/user/code/')" +expect_allowed "find in /home non-personal" "$(bash_input 'find /home/user/projects -type f')" + +subsection "System auth files - Should be BLOCKED" +expect_blocked "cat /etc/passwd" "$(bash_input 'cat /etc/passwd')" +expect_blocked "cat /etc/shadow" "$(bash_input 'cat /etc/shadow')" +expect_blocked "cat /etc/sudoers" "$(bash_input 'cat /etc/sudoers')" +expect_blocked "head /etc/passwd" "$(bash_input 'head /etc/passwd')" +expect_blocked "grep in /etc/passwd" "$(bash_input 'grep root /etc/passwd')" +expect_blocked "awk /etc/passwd" "$(bash_input 'awk -F: \"{print \\$1}\" /etc/passwd')" + +subsection "SSH files - Should be BLOCKED" +expect_blocked "cat SSH private key" "$(bash_input 'cat ~/.ssh/id_rsa')" +expect_blocked "cat SSH ed25519 key" "$(bash_input 'cat ~/.ssh/id_ed25519')" +expect_blocked "cat SSH config" "$(bash_input 'cat ~/.ssh/config')" +expect_blocked "cat /etc/ssh config" "$(bash_input 'cat /etc/ssh/sshd_config')" + +subsection "Cloud credentials - Should be BLOCKED" +expect_blocked "cat AWS credentials" "$(bash_input 'cat ~/.aws/credentials')" +expect_blocked "cat gcloud config" "$(bash_input 'cat ~/.config/gcloud/credentials.json')" + +subsection "Environment files - Should be BLOCKED" +expect_blocked "cat .env" "$(bash_input 'cat .env')" +expect_blocked "cat .env.local" "$(bash_input 'cat .env.local')" +expect_blocked "grep in .env" "$(bash_input 'grep API_KEY .env')" + +subsection "Private keys - Should be BLOCKED" +expect_blocked "cat .pem file" "$(bash_input 'cat server.pem')" +expect_blocked "cat .key file" "$(bash_input 'cat private.key')" + +subsection "Shell history - Should be BLOCKED" +expect_blocked "cat bash history" "$(bash_input 'cat ~/.bash_history')" +expect_blocked "cat zsh history" "$(bash_input 'cat ~/.zsh_history')" + +# ============================================================================ +section "SENSITIVE FILE READING (Read tool)" +# ============================================================================ + +subsection "Personal directories - Should be ALLOWED (personal dir protection removed)" +expect_allowed "Read file in /home/*/Documents" "$(file_input 'Read' '/home/user/Documents/document.txt')" +expect_allowed "Read file in /Users/*/Documents" "$(file_input 'Read' '/Users/john/Documents/secrets.txt')" +expect_allowed "Read file in /home/*/Desktop" "$(file_input 'Read' '/home/user/Desktop/notes.txt')" +expect_allowed "Read file in /home/*/Downloads" "$(file_input 'Read' '/home/user/Downloads/file.pdf')" +expect_allowed "Read file in ~/Documents (via ~)" "$(file_input 'Read' '~/Documents/document.txt')" + +subsection "Home directories (non-personal) - Should be ALLOWED" +expect_allowed "Read file in /Users non-Documents" "$(file_input 'Read' '/Users/john/secrets.txt')" +expect_allowed "Read file in /home non-personal" "$(file_input 'Read' '/home/user/project/config.yml')" + +subsection "System auth files - Should be BLOCKED" +expect_blocked "Read /etc/passwd" "$(file_input 'Read' '/etc/passwd')" +expect_blocked "Read /etc/shadow" "$(file_input 'Read' '/etc/shadow')" +expect_blocked "Read /etc/sudoers" "$(file_input 'Read' '/etc/sudoers')" + +subsection "SSH files - Should be BLOCKED" +expect_blocked "Read SSH private key" "$(file_input 'Read' '/home/user/.ssh/id_rsa')" +expect_blocked "Read SSH ed25519" "$(file_input 'Read' '/home/user/.ssh/id_ed25519')" +expect_blocked "Read SSH config" "$(file_input 'Read' '/home/user/.ssh/config')" + +subsection "Cloud credentials - Should be BLOCKED" +expect_blocked "Read AWS credentials" "$(file_input 'Read' '/home/user/.aws/credentials')" +expect_blocked "Read .kube/config" "$(file_input 'Read' '/home/user/.kube/config')" +expect_blocked "Read .docker/config" "$(file_input 'Read' '/home/user/.docker/config.json')" + +subsection "Environment files - Should be BLOCKED" +expect_blocked "Read .env" "$(file_input 'Read' '/app/.env')" +expect_blocked "Read .env.local" "$(file_input 'Read' '/app/.env.local')" +expect_blocked "Read .env.production" "$(file_input 'Read' '/app/.env.production')" + +subsection "Git credentials - Should be BLOCKED" +expect_blocked "Read .git-credentials" "$(file_input 'Read' '/home/user/.git-credentials')" +expect_blocked "Read .netrc" "$(file_input 'Read' '/home/user/.netrc')" + +subsection "Package manager configs - Should be BLOCKED" +expect_blocked "Read .npmrc" "$(file_input 'Read' '/home/user/.npmrc')" + +subsection "Private keys - Should be BLOCKED" +expect_blocked "Read .pem file" "$(file_input 'Read' '/app/certs/server.pem')" +expect_blocked "Read .key file" "$(file_input 'Read' '/app/certs/private.key')" + +subsection "Secrets directories - Should be BLOCKED" +expect_blocked "Read from secrets/" "$(file_input 'Read' '/app/secrets/api-key.txt')" +expect_blocked "Read from credentials/" "$(file_input 'Read' '/app/credentials/token.json')" + +subsection "Shell history - Should be BLOCKED" +expect_blocked "Read bash history" "$(file_input 'Read' '/home/user/.bash_history')" +expect_blocked "Read zsh history" "$(file_input 'Read' '/home/user/.zsh_history')" + +# ============================================================================ +section "FILE TOOL OPERATIONS (Edit/Write)" +# ============================================================================ + +subsection "Should be BLOCKED" +expect_blocked "Write to .env" "$(file_input 'Write' '/app/.env')" +expect_blocked "Write to .env.local" "$(file_input 'Write' '/app/.env.local')" +expect_blocked "Write to .env.production" "$(file_input 'Write' '/app/.env.production')" +expect_blocked "Edit .env" "$(file_input 'Edit' '/home/user/project/.env')" +expect_blocked "Write to .aws/credentials" "$(file_input 'Write' '/home/user/.aws/credentials')" +expect_blocked "Write to .aws/config" "$(file_input 'Write' '/home/user/.aws/config')" +expect_blocked "Write to .ssh/id_rsa" "$(file_input 'Write' '/home/user/.ssh/id_rsa')" +expect_blocked "Write to .ssh/authorized_keys" "$(file_input 'Write' '/home/user/.ssh/authorized_keys')" +expect_blocked "Write to .kube/config" "$(file_input 'Write' '/home/user/.kube/config')" +expect_blocked "Write to .git-credentials" "$(file_input 'Write' '/home/user/.git-credentials')" +expect_blocked "Write to .netrc" "$(file_input 'Write' '/home/user/.netrc')" +expect_blocked "Write to .npmrc" "$(file_input 'Write' '/home/user/.npmrc')" +expect_blocked "Write to package-lock.json" "$(file_input 'Write' '/app/package-lock.json')" +expect_blocked "Write to yarn.lock" "$(file_input 'Write' '/app/yarn.lock')" +expect_blocked "Write to pnpm-lock.yaml" "$(file_input 'Write' '/app/pnpm-lock.yaml')" +expect_blocked "Write to .docker/config.json" "$(file_input 'Write' '/home/user/.docker/config.json')" +expect_blocked "Write to /etc/passwd" "$(file_input 'Write' '/etc/passwd')" +expect_blocked "Write to /usr/bin/app" "$(file_input 'Write' '/usr/bin/app')" +expect_blocked "Write to secrets/ dir" "$(file_input 'Write' '/app/secrets/api-key.txt')" +expect_blocked "Write to credentials/ dir" "$(file_input 'Write' '/app/credentials/token.json')" +expect_blocked "Write to private/ dir" "$(file_input 'Write' '/data/private/keys.pem')" + +subsection "Should be ALLOWED" +expect_allowed "Write to src file" "$(file_input 'Write' '/app/src/index.ts')" +expect_allowed "Write to config file" "$(file_input 'Write' '/app/config/settings.json')" +expect_allowed "Edit README" "$(file_input 'Edit' '/app/README.md')" +expect_allowed "Write to test file" "$(file_input 'Write' '/app/tests/app.test.ts')" +expect_allowed "Edit component" "$(file_input 'Edit' '/app/src/components/Button.tsx')" +expect_allowed "Write package.json" "$(file_input 'Write' '/app/package.json')" +expect_allowed "Read project src file" "$(file_input 'Read' '/app/src/index.ts')" +expect_allowed "Read package.json" "$(file_input 'Read' '/app/package.json')" + +# ============================================================================ +section "QUOTED STRING BYPASS (commit messages, echo, etc.)" +# ============================================================================ + +subsection "Sensitive paths inside quotes - Should be ALLOWED" +expect_allowed "git commit with /home path in message" "$(bash_input 'git commit -m "fix: align /home/user/Documents blocking"')" +expect_allowed "git commit with /etc/passwd in message" "$(bash_input 'git commit -m "docs: explain /etc/passwd check"')" +expect_allowed "git commit with .env in message" "$(bash_input 'git commit -m "fix: handle .env.local loading"')" +expect_allowed "git commit with .ssh in message" "$(bash_input 'git commit -m "docs: update .ssh/config instructions"')" +expect_allowed "echo with sensitive path" "$(bash_input 'echo "Reading /home/user/Documents/file.txt"')" +expect_allowed "echo with /etc/passwd" "$(bash_input 'echo "Blocked: cat /etc/passwd"')" +expect_allowed "rm -rf in quoted string" "$(bash_input 'echo "Do not run rm -rf /home"')" + +subsection "Sensitive paths outside quotes - Should still be BLOCKED" +expect_blocked "cat /etc/passwd unquoted" "$(bash_input 'cat /etc/passwd')" +expect_blocked "cat .env unquoted" "$(bash_input 'cat .env')" +expect_blocked "rm -rf system dir unquoted" "$(bash_input 'rm -rf /etc')" + +# ============================================================================ +section "COMMON DEVELOPMENT COMMANDS" +# ============================================================================ + +subsection "Should all be ALLOWED" +expect_allowed "npm test" "$(bash_input 'npm test')" +expect_allowed "npm run dev" "$(bash_input 'npm run dev')" +expect_allowed "npm run build" "$(bash_input 'npm run build')" +expect_allowed "npx jest" "$(bash_input 'npx jest')" +expect_allowed "yarn test" "$(bash_input 'yarn test')" +expect_allowed "pnpm dev" "$(bash_input 'pnpm dev')" +expect_allowed "python script.py" "$(bash_input 'python script.py')" +expect_allowed "python -m pytest" "$(bash_input 'python -m pytest')" +expect_allowed "go build" "$(bash_input 'go build ./...')" +expect_allowed "go test" "$(bash_input 'go test ./...')" +expect_allowed "cargo build" "$(bash_input 'cargo build')" +expect_allowed "cargo test" "$(bash_input 'cargo test')" +expect_allowed "make build" "$(bash_input 'make build')" +expect_allowed "gradle build" "$(bash_input './gradlew build')" +expect_allowed "mvn package" "$(bash_input 'mvn package')" +expect_allowed "ls -la" "$(bash_input 'ls -la')" +expect_allowed "cat file" "$(bash_input 'cat README.md')" +expect_allowed "grep pattern" "$(bash_input 'grep -r TODO src/')" +expect_allowed "find files" "$(bash_input 'find . -name \"*.ts\"')" +expect_allowed "echo" "$(bash_input 'echo \"Hello World\"')" +expect_allowed "env" "$(bash_input 'env')" +expect_allowed "which node" "$(bash_input 'which node')" +expect_allowed "node --version" "$(bash_input 'node --version')" + +# ============================================================================ +# SUMMARY +# ============================================================================ + +echo "" +echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BOLD}${BLUE} TEST SUMMARY${NC}" +echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" + +TOTAL=$((PASSED + FAILED)) +echo -e " ${GREEN}Passed:${NC} $PASSED" +echo -e " ${RED}Failed:${NC} $FAILED" +echo -e " ${BLUE}Total:${NC} $TOTAL" +echo "" + +if [[ $FAILED -eq 0 ]]; then + echo -e " ${GREEN}${BOLD}✓ All tests passed!${NC}" +else + echo -e " ${RED}${BOLD}✗ Some tests failed!${NC}" +fi + +echo "" +exit $FAILED diff --git a/.agents/skills/branch-commit-pr-ticket/SKILL.md b/.agents/skills/branch-commit-pr-ticket/SKILL.md new file mode 100644 index 0000000..da99ad2 --- /dev/null +++ b/.agents/skills/branch-commit-pr-ticket/SKILL.md @@ -0,0 +1,526 @@ +--- +name: branch-commit-pr-ticket +version: 1.3.0 +description: Full automated workflow from local changes to a pull request with Linear ticket creation. Analyzes git diff, creates branch, commits, pushes, creates Linear ticket (French title, sub-issue of a `SUP-XXXX` support issue when one is given), and opens a PR linked to the ticket. +--- + +# Branch, Commit, Linear Ticket and PR Workflow + +Automate the full workflow from local changes to a pull request with Linear ticket creation. + +## Description (optional) + +$ARGUMENTS + +## Support Issue (optional) + +If the description above contains a **`SUP-`** reference, the Linear issue created in Step 11 becomes a **sub-issue of it** (`parentId`). Only the literal `SUP-` prefix triggers this; any other identifier is treated as plain text. + +Rules: + +- **Do not validate the reference first.** No `get_issue` or `list_issues` call — pass the identifier directly to `save_issue` and let the server resolve it. +- **The child keeps the configuration resolved in Step 2.** Team, project, status and labels are the resolved ones; the child does **not** inherit the support issue's team or project. A parent on a different team is expected. +- **The support issue itself is never modified.** No status change, no label, and no PR link — Step 13 links the PR to the created child issue only. +- **`SUP-…` must never appear in the branch name, the PR title, or the PR body.** The GitHub↔Linear integration scans all three and would attach the PR to the support issue. Mentioning it inside the Linear child issue's description is safe (Linear-side backlink only). +- **Strip the reference before inferring anything else.** The remaining description text feeds the commit message, the French title and the PR body, so `SUP-1234` never leaks into them. +- **If the reference cannot be resolved**, never abort: see Step 11. + +## CRITICAL: Fully Automatic Mode + +**This command operates in FULLY AUTOMATIC mode:** + +- NEVER ask the user for any information +- NEVER prompt for confirmation +- NEVER ask questions about type, scope, description, priority, or estimate +- Automatically infer EVERYTHING from git diff, file paths, and code changes +- Proceed directly from analysis to ticket/PR creation + +The command should be a single, seamless operation that requires zero user input. + +## Instructions + +### Step 1: Analyze Changes and Auto-Generate Everything + +**CRITICAL**: NEVER ask the user for information. Automatically infer everything from the git diff and commits. + +1. **Run analysis commands**: + + ```bash + git status + git diff --stat + git diff + git log --oneline -1 # Get last commit message if any + ``` + + **If no changes detected**: If `git status` shows a clean working tree (no staged or unstaged changes, and no untracked files), print a message and EXIT the workflow - do NOT create any branch, commit, ticket, or PR. **Untracked files count as changes** — proceed with the workflow if any exist (they will be staged in Step 5). + +2. **Auto-detect all properties**: + + a. **Type** (chore, fix, feat, refactor, test, docs, style): + - Look at file paths and changes + - `fix`: Bug fixes, error corrections, validation fixes + - `feat`: New components, new features, new functionality + - `chore`: Dependencies, config, tooling, cleanup + - `refactor`: Code restructuring without behavior change + - `style`: Formatting, code style changes + - `test`: Test files (.test., .spec., visual-test) + - `docs`: README, comments, documentation files + + b. **Scope** (from file paths and business domains): + - Find the best topic based on business domains + - For multiple topics: use most significant one + - Extract from directory structure or package names + + c. **Description**: + - Analyze the actual code changes + - Look at function names, component names, variable names + - Identify the main change + - Keep it concise and descriptive + - Focus on WHAT changed, not HOW + + d. **Priority** (default to 3 - Medium, unless the change clearly matches one of these criteria): + - 1 (Urgent): Security fixes, production blockers + - 2 (High): Critical bugs, data loss prevention + - 3 (Medium): Standard changes (default) + - 4 (Low): Minor improvements, nice-to-haves + + e. **Estimate** (auto-calculate using rubric - Fibonacci sequence): + - **1 point (Trivial)**: < 20 lines changed, 1-2 files + - **2 points (Simple)**: 20-50 lines changed, 3 files + - **3 points (Moderate)**: 50-150 lines changed, 4-5 files + - **5 points (Complex)**: 150-300 lines changed, 6-9 files + - **8 points (Very Complex)**: 300-500 lines changed, 10-14 files + - **13 points (Highly Complex)**: 500-1000 lines changed, 15-19 files + - **21 points (Extremely Complex)**: 1000+ lines changed, 20+ files + + f. **Labels** (auto-select based on type — final label list is built in Step 9 using config from Step 2): + - Business Unit label (from config, default: "MOBSUCCESS Group") + - Type label based on change type: + - `fix` → "Bug 🐞" + - `feat` → "Feature ✨" + - `chore`, `refactor`, `style`, `docs`, `test` → "Improvement 🛠️" + - Team label (from config, if present) + - Product label (from config, if present) + +3. **Generate ticket description automatically**: + + ```markdown + ## Problem + + + + ## Solution + + + + ## Changes + + - + + ## Impact + + - + - + ``` + +4. **NEVER ask for confirmation or clarification** - proceed directly to execution + +### Step 2: Resolve Linear Configuration + +Resolve each Linear field using a **4-level priority chain**. For each field, use the first level that provides a value: + +1. **User conversation** — The user explicitly mentioned a preference earlier in this conversation (e.g., "use project X", "put it in team Y") +2. **Project context** — Already loaded in your system prompt from `CLAUDE.md` or `AGENTS.md`. Do NOT read these files — they are already in your context. Look for a `## Linear` section or similar instructions mentioning team, project, status, labels, etc. +3. **`.mobsuccess.yml`** — Read and parse the file at the project root: + ```bash + cat .mobsuccess.yml 2>/dev/null + ``` +4. **Command defaults** — Hardcoded fallback values (see table below) + +#### Fields to resolve + +| Field | Level 2: Context examples | Level 3: `.mobsuccess.yml` keys (first match wins) | Level 4: Default | +| ------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------- | +| **team** | "Team: **Experiences**" | `linear.cli_defaults.feature_team` → `linear.team_identifier` → `linear.main_team_identifier` | `FT Tech Foundation` | +| **project** | "Project: **Workshop Experience 2026**" | `linear.cli_defaults.project[]` (map) → `linear.project` (string) | `FT Tech - RUN` | +| **status** | "Initial status: **To Dev**" | `linear.cli_defaults.status[]` (map) → `linear.default_state` | `QA Accepted` | +| **team_label** | _(if mentioned)_ | `linear.cli_defaults.team_label` → `linear.team_label_identifier` | _(none)_ | +| **business_unit_label** | _(if mentioned)_ | `linear.cli_defaults.business_unit_label` | `MOBSUCCESS Group` | +| **product_label** | _(if mentioned)_ | `linear.cli_defaults.product_label` | _(none)_ | +| **priority_override** | _(if mentioned)_ | `linear.cli_defaults.priority` | _(none — use auto-detected)_ | +| **linear_issue_required** | _(if mentioned)_ | `linear.linear_issue_required` | `true` | + +#### `.mobsuccess.yml` patterns + +Multiple config patterns exist across repos. The resolution order in the table above handles all of them: + +- **Minimal**: `linear.team_identifier` + `linear.team_label_identifier` under `linear:` node +- **cli_defaults with maps**: `linear.cli_defaults.feature_team`, `linear.cli_defaults.project.FTTECH`, `linear.cli_defaults.status.FTTECH`, etc. +- **Cross-team**: `linear.main_team_identifier` + `linear.cli_defaults.feature_team` overriding it + +#### Early exit + +If `linear_issue_required` resolves to `false`, skip all Linear-specific steps (including inferring the issue type label in Step 8, and Steps 9–11 and 13). When creating the PR, do not include any Linear ticket reference (no ticket ID in the title or body). + +#### Validation + +After resolving all fields, verify the team and project exist in Linear: + +1. Call `get_team` with the resolved **team**. If not found, warn and fall back to default `"FT Tech Foundation"`. +2. Call `get_project` with the resolved **project**. If not found, warn and fall back to default `"FT Tech - RUN"`. + +**Note**: If the context already provides UUIDs (e.g., `ID: 930b37e3-...`), you may use them directly without calling `get_team`/`get_project`. + +Store all resolved and validated values for use in subsequent steps. + +### Step 3: Determine Branch Name + +- Generate the target branch name from the inferred type and description: + - Format: `type/short-description` (see Reference: **Branch naming types**) + - Example: `feat/add-user-profile`, `fix/auth-redirect` +- If already on a branch whose name is valid, use it +- If on `main`/`master`, create the generated valid branch +- If on any other branch whose name is invalid, including temporary worktree branches such as `worktree-feat+...`, `codex/...`, or branches containing `+`: + - Do **not** reuse that branch for the PR + - Rename the current branch to the generated valid branch if it has no upstream yet + - Otherwise create a new generated valid branch from the current HEAD + - Preserve the current working tree and staged changes + +**IMPORTANT**: Branch name format is `/`, NOT `-` or a worktree/tool prefix. + +Validate branch names with this rule before pushing or opening the PR: + +```text +^(chore|fix|feat|refactor|style|test|docs)/[a-z0-9]+(-[a-z0-9]+)*$ +``` + +If the current branch fails this validation, fix the branch name in Step 4 before continuing. + +### Step 4: Create Branch (if needed) + +```bash +CURRENT_BRANCH=$(git branch --show-current) +VALID_BRANCH_REGEX='^(chore|fix|feat|refactor|style|test|docs)/[a-z0-9]+(-[a-z0-9]+)*$' + +if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then + git checkout -b +elif [ -z "$CURRENT_BRANCH" ]; then + git checkout -b +elif ! printf '%s' "$CURRENT_BRANCH" | grep -Eq "$VALID_BRANCH_REGEX"; then + if ! git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then + git branch -m + else + git checkout -b + fi +fi +``` + +### Step 5: Safety-Check Untracked Files + +Before staging anything, scan for untracked files that should not be committed: + +```bash +git ls-files --others --exclude-standard +``` + +Flag and **STOP the workflow** (do not stage, commit, push, or create a ticket) if any untracked file matches a suspicious pattern: + +- Secrets / credentials: `.env`, `.env.*`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*secret*`, `*credential*`, `*password*`, `*token*` +- Build / cache artefacts: `*.pyc`, `__pycache__/`, `node_modules/`, `dist/`, `build/`, `.cache/` +- Editor / OS noise: `.DS_Store`, `Thumbs.db`, `*.swp`, `*.swo`, `*~` +- Debug / temp: `*.log`, `*.tmp`, `*.bak`, `debug.*`, `test-output*` + +When stopping, tell the user **which files matched** and suggest either adding them to `.gitignore` or reviewing them before proceeding. + +If no suspicious files are found, continue to Step 6. + +### Step 6: Stage and Commit + +- Stage all relevant changes **including untracked files** with `git add -A` (or `git add .`) so new files are never forgotten. Files in `.gitignore` are excluded automatically. +- Generate a conventional commit with Co-authored-by trailer — see Reference: **Commit format with Co-authored-by** +- Create the commit (use `git commit -m "..."` or `-F -` with heredoc for multi-line) + +### Step 7: Push to Remote + +```bash +git push -u origin +``` + +### Step 8: Infer Issue Type Label + +Based on branch/change type: + +- `feat/` → Type = "Feature ✨" +- `fix/` → Type = "Bug 🐞" +- `chore/`, `refactor/`, `style/`, `docs/`, `test/` → Type = "Improvement 🛠️" + +### Step 9: Gather Linear Data + +Use Linear MCP to find required UUIDs: + +1. **Team**: Find team using resolved **team** from Step 2 +2. **Project**: Find project using resolved **project** from Step 2. If multiple projects match, select the one with the exact name match. +3. **Labels**: Find these labels by name using `list_issue_labels`: + - **IMPORTANT** — This API returns PAGINATED results. Iterate through ALL pages (pass `cursor` when `hasNextPage` is true) until you have UUIDs for every required label. Do NOT stop after the first page. + - Business Unit: resolved **business_unit_label** from Step 2 (default: "MOBSUCCESS Group") + - Type: "Feature ✨", "Improvement 🛠️" or "Bug 🐞" (based on Step 8) + - Team label: resolved **team_label** from Step 2 (only if present — skip if empty/none) + - Product label: resolved **product_label** from Step 2 (only if present — skip if empty/none) +4. **Status**: Find resolved **status** from Step 2 (default: "QA Accepted") via `list_issue_statuses` with team UUID +5. **Current User**: Get authenticated user for assignee +6. **MCP Tool Schema**: Before calling `save_issue`, check the tool descriptor/schema for the exact parameter names (e.g. `projectId`, `labelIds`, `stateId`). Use the names from the schema. + +### Step 10: Generate Linear Title and Description + +**Title**: Actionable verb in infinitive form, max 17 words, in French + +- Example: "Ajouter un endpoint pour récupérer les utilisateurs actifs" +- Example: "Corriger le bug d'authentification sur la page login" + +**Description**: + +```markdown +## Problem + + + +## Solution + + + +## Changes + +- : +- : + +## Impact + +- +- +``` + +### Step 11: Create Linear Issue + +Create an issue via Linear MCP with all UUIDs retrieved in Step 9: + +``` +title: +description: +team: (use UUID) +status: (use UUID) +assignee: me +priority: +estimate: +project: (use UUID) +labels: [ + (use UUID), + (use UUID), + (use UUID — only if present from Step 2), + (use UUID — only if present from Step 2) +] +parentId: +``` + +**Unresolvable parent**: if the call fails because of `parentId` (unknown or inaccessible support issue), the issue was not created. Re-send the same `save_issue` **without** `parentId`, then warn loudly in the final output. A bad support reference must never abort a workflow whose commit is already pushed. + +**IMPORTANT**: The `estimate` parameter is mandatory and must be provided using the Fibonacci rubric (1, 2, 3, 5, 8, 13, 21). + +**Post-create validation**: After creating the issue, call `get_issue` to verify it has the correct project and labels. If missing, call `save_issue` with the issue `id` to add `projectId` and `labelIds`. If updates still don't persist, suggest the user verify the Linear MCP is enabled (see Common Issues: "Linear updates don't persist"). + +The issue identifier (e.g., `FTTECH-123`) will be used in the next step. + +### Step 12: Create Pull Request + +Use GitHub CLI to create the PR **with the Linear ticket ID already in the title**: + +```bash +DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name') +gh pr create --base "$DEFAULT_BRANCH" --title "type(scope): description [ISSUE_ID]" --body "..." +``` + +**PR Title:** `type(scope): description [FTTECH-XXX]` - Max 150 characters + +- Include the Linear issue identifier from Step 11 +- Example: `feat(user): add profile endpoint [FTTECH-123]` +- Always the **created** issue, never the support issue: no `SUP-…` string in the title or the body, otherwise the integration attaches this PR to the support issue as well + +**PR Body:** Follow the repository's PR template if it exists, otherwise use: + +```markdown +## What Does It Do? + + + +## Context + + + +## Test Plan + +- [ ] +``` + +### Step 13: Link PR to Linear Ticket and Ensure Status + +**13a. Add PR link to ticket** + +Get the PR URL: + +```bash +gh pr view --json url --jq '.url' +``` + +Then call `save_issue` with the issue `id` and `links: [{ url: "", title: "PR #" }]` to attach the PR to the Linear ticket. + +**13b. MANDATORY status verification loop — DO NOT SKIP** + +The workflow is **not complete** until this loop is executed. The GitHub-Linear integration often switches the ticket to "Development In Progress"; the resolved status (default: "QA Accepted") must be restored. + +1. **Wait 1–2 seconds** after adding the PR link (to let the integration apply its changes) +2. **Call `get_issue`** with the issue identifier (e.g. `FTTECH-123`) to read the current status +3. **If status ≠ target** (resolved status from Step 2): call `save_issue` with `state: ""` (e.g. `"QA Accepted"`) +4. **Repeat** steps 2–3 up to **5 times** (wait 1–2 seconds between each check) +5. **If status still reverts** after 5 retries: tell the user: "Status remains 'Development In Progress' — please set to 'QA Accepted' manually in Linear, or adjust the GitHub-Linear integration rules." Also suggest checking the Linear MCP is enabled (see Common Issues: "Linear updates don't persist") + +**CRITICAL**: Skipping this loop causes PRs to stay unmergeable. Always execute it when a Linear ticket was created. + +## Output + +When complete, display: + +``` +## Workflow Complete + +### Git +- **Branch**: `` +- **Commit**: - + +### Pull Request +- **Title**: +- **URL**: + +### Linear Ticket +- **Status**: +- **Issue**: - +- **URL**: <linear_url> +- **Project**: <resolved project> +- **Parent**: <SUP-XXXX> (omit this line when no support issue was given) +- **Priority**: <priority> | **Estimate**: <estimate> +``` + +If the parent could not be resolved, replace the `Parent` line with: + +``` +- ⚠️ **Parent**: `SUP-XXXX` could not be resolved — ticket created without a parent, link it manually in Linear +``` + +## Common Issues & Solutions + +### Issue: "Branch name is invalid" + +**Solution**: Branch must be `<type>/<description>`, NOT include ticket number. + +- correct: `chore/remove-exports` +- wrong: `fttech-123-remove-exports` + +### Issue: "Issue does not have a Teams label" + +**Solution**: Ensure these labels are added: + +- Business Unit label (from config, default "MOBSUCCESS Group") +- Type label: "Improvement 🛠️", "Feature ✨", or "Bug 🐞" +- Team label (if resolved from context or config) +- Product label (if resolved from context or config) + +### Issue: "PR not mergeable" + +**Solution**: Linear ticket status must match the resolved status (default: "QA Accepted") + +### Issue: "Status keeps changing back" + +**Solution**: The GitHub-Linear integration may override status. Use the retry loop with delays to ensure status stays at the resolved target value. + +### Issue: "Support issue `SUP-XXXX` not found" + +**Solution**: Re-create the issue without `parentId` and warn the user in the output. Never abort the workflow, and never validate the reference with an extra `get_issue` call beforehand. + +### Issue: "The PR got attached to the support issue" + +**Solution**: A `SUP-…` string leaked into the branch name, PR title or PR body — the GitHub↔Linear integration scans all three. Remove it there; the reference belongs in `parentId` (and optionally the child issue's description), nowhere else. + +### Issue: "Linear updates don't persist" (project, labels, status unchanged after save_issue) + +**Solution**: The Linear MCP may be disabled or disconnected. Ask the user to verify it is enabled in their environment. + +## Defaults Summary + +All fields follow the **4-level priority chain**: User conversation > CLAUDE.md/AGENTS.md context > `.mobsuccess.yml` > Command defaults. + +| Setting | Command default (Level 4) | +| --------------------- | ----------------------------------------------------------- | +| Team | `FT Tech Foundation` | +| Project | `FT Tech - RUN` | +| Status | `QA Accepted` | +| Team Label | _(none)_ | +| Business Unit | `MOBSUCCESS Group` | +| Product Label | _(none)_ | +| Priority | Medium (3) — auto-detected | +| Linear Issue Required | `true` | +| Estimate | Auto-calculated (Fibonacci: 1, 2, 3, 5, 8, 13, 21) | +| Type | Improvement 🛠️ (or Bug 🐞 if `fix/`, Feature ✨ if `feat/`) | + +## Reference + +### Commit format with Co-authored-by (required) + +Always append the Co-authored-by trailer so the AI appears as co-author on GitHub. **Use your platform's identity** — this is a placeholder per platform, not a fixed string to copy. + +- **Cursor**: use `Cursor <cursoragent@cursor.com>` (example below) +- **Claude/Codex/Copilot/others**: check your platform's docs + +```bash +git commit -m "fix(athena): hide reason and URL from unauthorized clients + +Co-authored-by: $PLATFORM_COAUTHOR" +``` + +Replace $PLATFORM_COAUTHOR with your platform's identity. +For longer messages, use a file or heredoc to avoid shell escaping issues. + +### Branch naming types + +- `chore/` - Maintenance, cleanup, dependencies +- `fix/` - Bug fixes +- `feat/` - New features +- `refactor/` - Code refactoring +- `style/` - Formatting changes +- `test/` - Adding/updating tests +- `docs/` - Documentation changes + +### Priority levels + +| Level | Name | Use for | +| ----- | ------ | ----------------------------------- | +| 1 | Urgent | Security fixes, production blockers | +| 2 | High | Critical bugs, data loss prevention | +| 3 | Medium | Standard changes (default) | +| 4 | Low | Minor improvements, nice-to-haves | + +### Estimate rubric (Fibonacci sequence) + +| Points | Lines Changed | Files | Complexity | +| ------ | ------------- | ----- | ------------------------------------- | +| 1 | < 20 | 1-2 | Trivial | +| 2 | 20-50 | 3 | Simple | +| 3 | 50-150 | 4-5 | Moderate | +| 5 | 150-300 | 6-9 | Complex | +| 8 | 300-500 | 10-14 | Very Complex | +| 13 | 500-1000 | 15-19 | Highly Complex | +| 21 | 1000+ | 20+ | Extremely Complex (split recommended) | + +### Required labels + +1. **Business Unit**: From `linear.cli_defaults.business_unit_label` or fallback `"MOBSUCCESS Group"` (always required) +2. **Type**: One of "Improvement 🛠️" (chore, refactor, style, docs, test), "Feature ✨" (feat), "Bug 🐞" (fix) +3. **Team**: From `linear.cli_defaults.team_label` or `linear.team_label_identifier` (optional, included only if configured) +4. **Product**: From `linear.cli_defaults.product_label` (optional, included only if configured) diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..2edab9b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash|Shell|Edit|Write|Read|mcp__.*", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.agents/hooks/security-guard/security-guard.sh", + "timeout": 5 + } + ] + } + ] + } +} diff --git a/.claude/skills/branch-commit-pr-ticket b/.claude/skills/branch-commit-pr-ticket new file mode 120000 index 0000000..c789038 --- /dev/null +++ b/.claude/skills/branch-commit-pr-ticket @@ -0,0 +1 @@ +../../.agents/skills/branch-commit-pr-ticket \ No newline at end of file diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 47f5c53..0678348 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16.16.0 + node-version: 24 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -35,7 +35,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16.16.0 + node-version: 24 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -59,7 +59,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -75,7 +75,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16.16.0 + node-version: 24 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -83,7 +83,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -137,7 +137,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16.16.0 + node-version: 24 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -145,7 +145,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -207,7 +207,7 @@ jobs: name: package - uses: actions/setup-node@v3 with: - node-version: 16.16.0 + node-version: 24 registry-url: https://npm.pkg.github.com/ scope: "@mobsuccess-devops" - run: echo "registry=https://npm.pkg.github.com/@mobsuccess-devops" >> .npmrc diff --git a/action.yml b/action.yml index 66128f4..92f8d4f 100644 --- a/action.yml +++ b/action.yml @@ -4,5 +4,5 @@ branding: icon: "chevron-right" color: "gray-dark" runs: - using: "node20" + using: "node24" main: "index.js" diff --git a/ai-lock.json b/ai-lock.json new file mode 100644 index 0000000..b267264 --- /dev/null +++ b/ai-lock.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "skills": { + "branch-commit-pr-ticket": { + "source": "mobsuccess-devops/skills-hub", + "sourceUrl": "https://github.com/mobsuccess-devops/skills-hub.git", + "skillPath": "skills/branch-commit-pr-ticket", + "skillFolderHash": "a2940631713f1692a1dd722e2586ca824716b1c2", + "agents": [ + "claude-code" + ], + "trusted": true, + "installedAt": "2026-09-01T08:16:40.071Z", + "updatedAt": "2026-09-01T08:16:40.071Z" + } + }, + "mcpServers": {}, + "hooks": { + "security-guard": { + "source": "mobsuccess-devops/skills-hub", + "sourceUrl": "https://github.com/mobsuccess-devops/skills-hub.git", + "folderPath": "hooks/security-guard", + "folderHash": "88855ec1878d5287fa97b2bb600300bd6fc53bb4", + "agents": [ + "claude-code" + ], + "trusted": true, + "handlers": { + "claude-code": { + "PreToolUse": [ + { + "matcher": "Bash|Shell|Edit|Write|Read|mcp__.*", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.agents/hooks/security-guard/security-guard.sh", + "timeout": 5 + } + ] + } + ] + } + }, + "installedAt": "2026-09-01T08:16:40.087Z", + "updatedAt": "2026-09-01T08:16:40.087Z" + } + } +} \ No newline at end of file From 259555193fb96e98bba0e70b6379a8246a9969fe Mon Sep 17 00:00:00 2001 From: Mathis Pinsault <pinsaultm@gmail.com> Date: Tue, 1 Sep 2026 10:24:57 +0200 Subject: [PATCH 2/5] Discard changes to .github/workflows/npm.yml --- .github/workflows/npm.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 0678348..47f5c53 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 24 + node-version: 16.16.0 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -35,7 +35,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 24 + node-version: 16.16.0 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -59,7 +59,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -75,7 +75,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 24 + node-version: 16.16.0 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -83,7 +83,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -137,7 +137,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 24 + node-version: 16.16.0 - name: Cache Node Modules id: cache uses: actions/cache@v3 @@ -145,7 +145,7 @@ jobs: cache-name: cache-node-modules with: path: ./node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-24 + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}-node-16.16.0 - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.MS_READ_PACKAGES_GITHUB_PAT }}" >> ~/.npmrc name: Configure Credentials For GitHub Packages - run: npm ci @@ -207,7 +207,7 @@ jobs: name: package - uses: actions/setup-node@v3 with: - node-version: 24 + node-version: 16.16.0 registry-url: https://npm.pkg.github.com/ scope: "@mobsuccess-devops" - run: echo "registry=https://npm.pkg.github.com/@mobsuccess-devops" >> .npmrc From 0bebceef76eb6287628a7b49901b98454074b88a Mon Sep 17 00:00:00 2001 From: Mathis Pinsault <pinsaultm@gmail.com> Date: Tue, 1 Sep 2026 10:49:24 +0200 Subject: [PATCH 3/5] fix prettier --- ai-lock.json | 10 +++------- lib/checks/lockVersion.js | 4 ++-- lib/checks/preReleases.js | 2 +- lib/utils.js | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/ai-lock.json b/ai-lock.json index b267264..ee48038 100644 --- a/ai-lock.json +++ b/ai-lock.json @@ -6,9 +6,7 @@ "sourceUrl": "https://github.com/mobsuccess-devops/skills-hub.git", "skillPath": "skills/branch-commit-pr-ticket", "skillFolderHash": "a2940631713f1692a1dd722e2586ca824716b1c2", - "agents": [ - "claude-code" - ], + "agents": ["claude-code"], "trusted": true, "installedAt": "2026-09-01T08:16:40.071Z", "updatedAt": "2026-09-01T08:16:40.071Z" @@ -21,9 +19,7 @@ "sourceUrl": "https://github.com/mobsuccess-devops/skills-hub.git", "folderPath": "hooks/security-guard", "folderHash": "88855ec1878d5287fa97b2bb600300bd6fc53bb4", - "agents": [ - "claude-code" - ], + "agents": ["claude-code"], "trusted": true, "handlers": { "claude-code": { @@ -45,4 +41,4 @@ "updatedAt": "2026-09-01T08:16:40.087Z" } } -} \ No newline at end of file +} diff --git a/lib/checks/lockVersion.js b/lib/checks/lockVersion.js index 9e86aff..b306ce6 100644 --- a/lib/checks/lockVersion.js +++ b/lib/checks/lockVersion.js @@ -5,12 +5,12 @@ const expectedLockfileVersion = 3; function checkLockVersion(lockFile) { if (!lockFile) { return new Error( - "No package-lock.json file, ignoring lockfileVersion check" + "No package-lock.json file, ignoring lockfileVersion check", ); } if (lockFile.lockfileVersion !== expectedLockfileVersion) { return new FatalError( - `Unexpected lockfileVersion, found “${lockFile.lockfileVersion}”, expected “${expectedLockfileVersion}”` + `Unexpected lockfileVersion, found “${lockFile.lockfileVersion}”, expected “${expectedLockfileVersion}”`, ); } } diff --git a/lib/checks/preReleases.js b/lib/checks/preReleases.js index f0107eb..f06c1c1 100644 --- a/lib/checks/preReleases.js +++ b/lib/checks/preReleases.js @@ -14,7 +14,7 @@ function checkForPreRelease(pkgJson = {}) { /-?pr-backend-(\d+)(\.(\d+))*$/.test(version)) // matches -pr-backend-1 or -pr-backend-1.0 or -pr-backend-1.0.0 ) { return new FatalError( - `Unexpected pre-release dependency found: ${name}@${version}\n` + `Unexpected pre-release dependency found: ${name}@${version}\n`, ); } } diff --git a/lib/utils.js b/lib/utils.js index 63caa9c..17611e0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -16,7 +16,7 @@ function getAllPackages() { const result = spawnSync( "find", [".", "-name", "package.json", "-not", "-path", "./node_modules/*"], - { stdio: ["ignore", "pipe", "ignore"] } + { stdio: ["ignore", "pipe", "ignore"] }, ); if (result.status !== 0) { throw new Error(`find command failed with status ${result.status}`); From 78fc4c6e705f81f5cebfa32104627a0d599eeed7 Mon Sep 17 00:00:00 2001 From: Mathis Pinsault <pinsaultm@gmail.com> Date: Tue, 1 Sep 2026 13:47:14 +0200 Subject: [PATCH 4/5] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42c642a..2e917ef 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@types/jest": "^29.2.4", "eslint": "^8.39.0", "jest": "^29.3.1", - "prettier": "^2.8.8" + "prettier": "^3.9.6" }, "publishConfig": { "registry": "https://npm.pkg.github.com" From 86a37621f1ab637c7b1ddb4fcea7b12e999d0c3a Mon Sep 17 00:00:00 2001 From: Mathis Pinsault <pinsaultm@gmail.com> Date: Tue, 1 Sep 2026 13:48:34 +0200 Subject: [PATCH 5/5] Update package-lock.json --- package-lock.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae687c1..987762e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@types/jest": "^29.2.4", "eslint": "^8.39.0", "jest": "^29.3.1", - "prettier": "^2.8.8" + "prettier": "^3.9.6" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3737,15 +3737,16 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, + "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1"