Created
February 6, 2026 20:42
-
-
Save sammcj/e351784d112f694116c97dcfe683e157 to your computer and use it in GitHub Desktop.
minimal openclaw skill-audit hook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ${HOME}/.openclaw/workspace/tools/skill-audit.sh | |
| set -euo pipefail | |
| # skill-audit.sh - Audit OpenClaw skills for suspicious patterns before installation | |
| # Usage: skill-audit.sh <skill-path-or-url> | |
| # Exit codes: 0 = clean, 1 = suspicious patterns found, 2 = error | |
| readonly SCRIPT_NAME="${0##*/}" | |
| # Color output (disable if not tty) | |
| if [[ -t 1 ]]; then | |
| readonly RED='\033[0;31m' | |
| readonly YELLOW='\033[1;33m' | |
| readonly GREEN='\033[0;32m' | |
| readonly NC='\033[0m' # No Color | |
| else | |
| readonly RED='' | |
| readonly YELLOW='' | |
| readonly GREEN='' | |
| readonly NC='' | |
| fi | |
| die() { | |
| echo -e "${RED}Error: ${1}${NC}" >&2 | |
| exit 2 | |
| } | |
| warn() { | |
| echo -e "${YELLOW}Warning: ${1}${NC}" >&2 | |
| } | |
| info() { | |
| echo -e "${GREEN}Info: ${1}${NC}" | |
| } | |
| # Check dependencies | |
| command -v grep >/dev/null 2>&1 || die "grep is required" | |
| # Patterns that indicate potential malware/suspicious behavior | |
| # These are regex patterns for grep -E | |
| readonly SUSPICIOUS_PATTERNS=( | |
| # Quarantine removal (macOS Gatekeeper bypass) | |
| 'xattr.*-d.*com\.apple\.quarantine' | |
| 'xattr.*--remove.*quarantine' | |
| # Encoded/obfuscated payloads | |
| 'base64.*\|.*(bash|sh|zsh)' | |
| 'base64.*-D.*\|' | |
| 'eval\s*\$\(' | |
| 'eval\s*\`' | |
| '\beval\s+"[^"]*\$' | |
| # Staged downloads/executions | |
| 'curl.*\|.*(bash|sh|zsh)' | |
| 'wget.*\|.*(bash|sh|zsh)' | |
| 'curl.*-o.*&&.*chmod.*\+x' | |
| 'wget.*-O.*&&.*chmod.*\+x' | |
| # Suspicious domains (common malicious patterns) | |
| '\.tk/' | |
| '\.ml/' | |
| '\.ga/' | |
| '\.cf/' | |
| 'pastebin\.com' | |
| 'gist\.github\.com.*raw' | |
| 'bit\.ly' | |
| 'tinyurl' | |
| 't\.co' | |
| # Obfuscation techniques | |
| '\$\{IFS\}' | |
| 'printf.*\\x[0-9a-f]' | |
| '\\x[0-9a-f]{2,}' | |
| '\$\{.*##\}' | |
| # Suspicious file operations | |
| '>/dev/(null|tcp)' | |
| 'bash.*-c.*\$\(' | |
| 'sh.*-c.*\$\(' | |
| # Self-modification | |
| 'chmod.*\+s' | |
| 'sudo.*curl' | |
| 'sudo.*wget' | |
| ) | |
| # Patterns that warrant extra scrutiny but aren't necessarily malicious | |
| readonly WARNING_PATTERNS=( | |
| # External script downloads | |
| 'curl.*\.sh' | |
| 'wget.*\.sh' | |
| 'curl.*install' | |
| 'wget.*install' | |
| # Clipboard manipulation (ClickFix style) | |
| 'pbcopy' | |
| 'xclip' | |
| 'clipboard' | |
| # Password/key handling | |
| 'password' | |
| 'api[_-]?key' | |
| 'secret[_-]?key' | |
| 'private[_-]?key' | |
| # Network listeners | |
| 'nc\s+-l' | |
| 'netcat.*-l' | |
| 'ncat.*-l' | |
| # Code compilation (could hide malicious code) | |
| 'gcc\s+' | |
| 'clang\s+' | |
| 'make\s+' | |
| 'cmake' | |
| ) | |
| # Check if input is a URL or local path | |
| check_input_type() { | |
| local input="${1}" | |
| if [[ "${input}" =~ ^https?:// ]]; then | |
| echo "url" | |
| elif [[ -d "${input}" ]]; then | |
| echo "directory" | |
| elif [[ -f "${input}" ]]; then | |
| echo "file" | |
| else | |
| echo "unknown" | |
| fi | |
| } | |
| # Download skill from URL to temp directory | |
| download_skill() { | |
| local url="${1}" | |
| local tmpdir="${2}" | |
| command -v curl >/dev/null 2>&1 || die "curl is required to download from URL" | |
| info "Downloading skill from ${url}..." | |
| # Try to download as tarball/archive first (common for GitHub releases) | |
| if [[ "${url}" =~ \.(tar\.gz|tgz|tar\.bz2|zip)$ ]]; then | |
| local archive="${tmpdir}/skill-archive" | |
| if ! curl -fsSL "${url}" -o "${archive}" 2>/dev/null; then | |
| die "Failed to download archive from ${url}" | |
| fi | |
| # Extract based on extension | |
| if [[ "${url}" =~ \.zip$ ]]; then | |
| command -v unzip >/dev/null 2>&1 || die "unzip is required for .zip files" | |
| unzip -q "${archive}" -d "${tmpdir}/extracted" | |
| else | |
| tar -xf "${archive}" -C "${tmpdir}/extracted" --strip-components=1 2>/dev/null || \ | |
| tar -xf "${archive}" -C "${tmpdir}/extracted" 2>/dev/null || \ | |
| die "Failed to extract archive" | |
| fi | |
| echo "${tmpdir}/extracted" | |
| else | |
| # Try as git repo | |
| if command -v git >/dev/null 2>&1; then | |
| if git clone --depth 1 "${url}" "${tmpdir}/cloned" 2>/dev/null; then | |
| echo "${tmpdir}/cloned" | |
| return 0 | |
| fi | |
| fi | |
| # Fallback: try to download raw files | |
| die "Cannot download skill from ${url} - not a recognized archive or git repo" | |
| fi | |
| } | |
| # Check a single file for suspicious patterns | |
| check_file() { | |
| local file="${1}" | |
| local found_suspicious=0 | |
| local found_warnings=0 | |
| # Skip binary files | |
| if file "${file}" 2>/dev/null | grep -q 'binary\|executable'; then | |
| warn "Skipping binary file: ${file}" | |
| return 0 | |
| fi | |
| # Skip if not a text file | |
| if ! grep -Iq . "${file}" 2>/dev/null; then | |
| return 0 | |
| fi | |
| # Check suspicious patterns | |
| for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do | |
| if grep -Ei "${pattern}" "${file}" >/dev/null 2>&1; then | |
| echo -e "${RED}[CRITICAL]${NC} Suspicious pattern found in ${file}:" | |
| grep -Eni "${pattern}" "${file}" | head -5 | while read -r line; do | |
| echo " ${line}" | |
| done | |
| found_suspicious=1 | |
| fi | |
| done | |
| # Check warning patterns | |
| for pattern in "${WARNING_PATTERNS[@]}"; do | |
| if grep -Ei "${pattern}" "${file}" >/dev/null 2>&1; then | |
| echo -e "${YELLOW}[WARNING]${NC} Potentially suspicious pattern in ${file}:" | |
| grep -Eni "${pattern}" "${file}" | head -3 | while read -r line; do | |
| echo " ${line}" | |
| done | |
| found_warnings=1 | |
| fi | |
| done | |
| if (( found_suspicious )); then | |
| return 1 | |
| elif (( found_warnings )); then | |
| return 2 | |
| fi | |
| return 0 | |
| } | |
| # Main audit function | |
| audit_skill() { | |
| local path="${1}" | |
| local total_suspicious=0 | |
| local total_warnings=0 | |
| local files_checked=0 | |
| info "Auditing skill at: ${path}" | |
| info "Patterns checked: ${#SUSPICIOUS_PATTERNS[@]} suspicious, ${#WARNING_PATTERNS[@]} warning" | |
| echo | |
| # If it's a single file | |
| if [[ -f "${path}" ]]; then | |
| files_checked=1 | |
| check_file "${path}" | |
| case $? in | |
| 1) total_suspicious=1 ;; | |
| 2) total_warnings=1 ;; | |
| esac | |
| elif [[ -d "${path}" ]]; then | |
| # Find all relevant files | |
| while IFS= read -r -d '' file; do | |
| (( files_checked++ )) || true | |
| check_file "${file}" | |
| case $? in | |
| 1) (( total_suspicious++ )) || true ;; | |
| 2) (( total_warnings++ )) || true ;; | |
| esac | |
| done < <(find "${path}" -type f \( \ | |
| -name "*.md" -o \ | |
| -name "*.sh" -o \ | |
| -name "*.bash" -o \ | |
| -name "*.zsh" -o \ | |
| -name "*.js" -o \ | |
| -name "*.ts" -o \ | |
| -name "*.json" -o \ | |
| -name "*.yaml" -o \ | |
| -name "*.yml" -o \ | |
| -name "*.py" -o \ | |
| -name "*.rb" -o \ | |
| -name "Makefile" -o \ | |
| -name "makefile" -o \ | |
| -name "SKILL*" \ | |
| \) -print0 2>/dev/null) | |
| fi | |
| echo | |
| info "Audit complete: ${files_checked} files checked" | |
| if (( total_suspicious > 0 )); then | |
| echo -e "${RED}CRITICAL: Found ${total_suspicious} files with suspicious patterns${NC}" | |
| echo -e "${RED}Recommendation: DO NOT INSTALL this skill${NC}" | |
| return 1 | |
| elif (( total_warnings > 0 )); then | |
| echo -e "${YELLOW}WARNING: Found ${total_warnings} files with patterns requiring review${NC}" | |
| echo -e "${YELLOW}Recommendation: Review carefully before installing${NC}" | |
| return 2 | |
| else | |
| echo "Audit complete. No known suspicious patterns found." | |
| echo "Note: This is not a guarantee of safety - review the skill manually if untrusted." | |
| return 0 | |
| fi | |
| } | |
| # Print usage | |
| usage() { | |
| cat <<EOF | |
| Usage: ${SCRIPT_NAME} <skill-path-or-url> | |
| Audit an OpenClaw skill for suspicious patterns before installation. | |
| Arguments: | |
| skill-path-or-url Local path to skill directory/file, or URL to download | |
| Exit codes: | |
| 0 Skill appears clean | |
| 1 Suspicious patterns found (do not install) | |
| 2 Warning patterns found (review recommended) | |
| 3 Usage error | |
| Examples: | |
| ${SCRIPT_NAME} ./my-skill | |
| ${SCRIPT_NAME} https://github.com/user/skill-repo | |
| ${SCRIPT_NAME} ./SKILL.md | |
| EOF | |
| } | |
| # Main | |
| main() { | |
| if [[ $# -eq 0 ]]; then | |
| usage | |
| exit 3 | |
| fi | |
| local input="${1}" | |
| local audit_path="" | |
| local cleanup_needed=false | |
| local tmpdir="" | |
| # Handle different input types | |
| local input_type | |
| input_type=$(check_input_type "${input}") | |
| case "${input_type}" in | |
| url) | |
| tmpdir=$(mktemp -d) | |
| trap 'rm -rf "${tmpdir}"' EXIT | |
| audit_path=$(download_skill "${input}" "${tmpdir}") | |
| ;; | |
| directory|file) | |
| audit_path="${input}" | |
| ;; | |
| *) | |
| die "Cannot determine input type: ${input}" | |
| ;; | |
| esac | |
| # Run the audit | |
| audit_skill "${audit_path}" | |
| } | |
| main "${@}" | |
| root@debian-vm:~# vim ${HOME}/.openclaw/workspace/tools/skill-audit.sh | |
| root@debian-vm:~# vim ${HOME}/.openclaw/workspace/tools/skill-audit.sh | |
| root@debian-vm:~# cat ${HOME}/.openclaw/workspace/tools/skill-audit.sh | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # skill-audit.sh - Audit OpenClaw skills for suspicious patterns before installation | |
| # Usage: skill-audit.sh <skill-path-or-url> | |
| # Exit codes: 0 = clean, 1 = suspicious patterns found, 2 = error | |
| readonly SCRIPT_NAME="${0##*/}" | |
| # Color output (disable if not tty) | |
| if [[ -t 1 ]]; then | |
| readonly RED='\033[0;31m' | |
| readonly YELLOW='\033[1;33m' | |
| readonly GREEN='\033[0;32m' | |
| readonly NC='\033[0m' # No Color | |
| else | |
| readonly RED='' | |
| readonly YELLOW='' | |
| readonly GREEN='' | |
| readonly NC='' | |
| fi | |
| die() { | |
| echo -e "${RED}Error: ${1}${NC}" >&2 | |
| exit 2 | |
| } | |
| warn() { | |
| echo -e "${YELLOW}Warning: ${1}${NC}" >&2 | |
| } | |
| info() { | |
| echo -e "${GREEN}Info: ${1}${NC}" | |
| } | |
| # Check dependencies | |
| command -v grep >/dev/null 2>&1 || die "grep is required" | |
| # Patterns that indicate potential malware/suspicious behavior | |
| # These are regex patterns for grep -E | |
| readonly SUSPICIOUS_PATTERNS=( | |
| # Quarantine removal (macOS Gatekeeper bypass) | |
| 'xattr.*-d.*com\.apple\.quarantine' | |
| 'xattr.*--remove.*quarantine' | |
| # Encoded/obfuscated payloads | |
| 'base64.*\|.*(bash|sh|zsh)' | |
| 'base64.*-D.*\|' | |
| 'eval\s*\$\(' | |
| 'eval\s*\`' | |
| '\beval\s+"[^"]*\$' | |
| # Staged downloads/executions | |
| 'curl.*\|.*(bash|sh|zsh)' | |
| 'wget.*\|.*(bash|sh|zsh)' | |
| 'curl.*-o.*&&.*chmod.*\+x' | |
| 'wget.*-O.*&&.*chmod.*\+x' | |
| # Suspicious domains (common malicious patterns) | |
| '\.tk/' | |
| '\.ml/' | |
| '\.ga/' | |
| '\.cf/' | |
| 'pastebin\.com' | |
| 'gist\.github\.com.*raw' | |
| 'bit\.ly' | |
| 'tinyurl' | |
| 't\.co' | |
| # Obfuscation techniques | |
| '\$\{IFS\}' | |
| 'printf.*\\x[0-9a-f]' | |
| '\\x[0-9a-f]{2,}' | |
| '\$\{.*##\}' | |
| # Suspicious file operations | |
| '>/dev/(null|tcp)' | |
| 'bash.*-c.*\$\(' | |
| 'sh.*-c.*\$\(' | |
| # Self-modification | |
| 'chmod.*\+s' | |
| 'sudo.*curl' | |
| 'sudo.*wget' | |
| ) | |
| # Patterns that warrant extra scrutiny but aren't necessarily malicious | |
| readonly WARNING_PATTERNS=( | |
| # External script downloads | |
| 'curl.*\.sh' | |
| 'wget.*\.sh' | |
| 'curl.*install' | |
| 'wget.*install' | |
| # Clipboard manipulation (ClickFix style) | |
| 'pbcopy' | |
| 'xclip' | |
| 'clipboard' | |
| # Password/key handling | |
| 'password' | |
| 'api[_-]?key' | |
| 'secret[_-]?key' | |
| 'private[_-]?key' | |
| # Network listeners | |
| 'nc\s+-l' | |
| 'netcat.*-l' | |
| 'ncat.*-l' | |
| # Code compilation (could hide malicious code) | |
| 'gcc\s+' | |
| 'clang\s+' | |
| 'make\s+' | |
| 'cmake' | |
| ) | |
| # Check if input is a URL or local path | |
| check_input_type() { | |
| local input="${1}" | |
| if [[ "${input}" =~ ^https?:// ]]; then | |
| echo "url" | |
| elif [[ -d "${input}" ]]; then | |
| echo "directory" | |
| elif [[ -f "${input}" ]]; then | |
| echo "file" | |
| else | |
| echo "unknown" | |
| fi | |
| } | |
| # Download skill from URL to temp directory | |
| download_skill() { | |
| local url="${1}" | |
| local tmpdir="${2}" | |
| command -v curl >/dev/null 2>&1 || die "curl is required to download from URL" | |
| info "Downloading skill from ${url}..." | |
| # Try to download as tarball/archive first (common for GitHub releases) | |
| if [[ "${url}" =~ \.(tar\.gz|tgz|tar\.bz2|zip)$ ]]; then | |
| local archive="${tmpdir}/skill-archive" | |
| if ! curl -fsSL "${url}" -o "${archive}" 2>/dev/null; then | |
| die "Failed to download archive from ${url}" | |
| fi | |
| # Extract based on extension | |
| if [[ "${url}" =~ \.zip$ ]]; then | |
| command -v unzip >/dev/null 2>&1 || die "unzip is required for .zip files" | |
| unzip -q "${archive}" -d "${tmpdir}/extracted" | |
| else | |
| tar -xf "${archive}" -C "${tmpdir}/extracted" --strip-components=1 2>/dev/null || \ | |
| tar -xf "${archive}" -C "${tmpdir}/extracted" 2>/dev/null || \ | |
| die "Failed to extract archive" | |
| fi | |
| echo "${tmpdir}/extracted" | |
| else | |
| # Try as git repo | |
| if command -v git >/dev/null 2>&1; then | |
| if git clone --depth 1 "${url}" "${tmpdir}/cloned" 2>/dev/null; then | |
| echo "${tmpdir}/cloned" | |
| return 0 | |
| fi | |
| fi | |
| # Fallback: try to download raw files | |
| die "Cannot download skill from ${url} - not a recognized archive or git repo" | |
| fi | |
| } | |
| # Check a single file for suspicious patterns | |
| check_file() { | |
| local file="${1}" | |
| local found_suspicious=0 | |
| local found_warnings=0 | |
| # Skip binary files | |
| if file "${file}" 2>/dev/null | grep -q 'binary\|executable'; then | |
| warn "Skipping binary file: ${file}" | |
| return 0 | |
| fi | |
| # Skip if not a text file | |
| if ! grep -Iq . "${file}" 2>/dev/null; then | |
| return 0 | |
| fi | |
| # Check suspicious patterns | |
| for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do | |
| if grep -Ei "${pattern}" "${file}" >/dev/null 2>&1; then | |
| echo -e "${RED}[CRITICAL]${NC} Suspicious pattern found in ${file}:" | |
| grep -Eni "${pattern}" "${file}" | head -5 | while read -r line; do | |
| echo " ${line}" | |
| done | |
| found_suspicious=1 | |
| fi | |
| done | |
| # Check warning patterns | |
| for pattern in "${WARNING_PATTERNS[@]}"; do | |
| if grep -Ei "${pattern}" "${file}" >/dev/null 2>&1; then | |
| echo -e "${YELLOW}[WARNING]${NC} Potentially suspicious pattern in ${file}:" | |
| grep -Eni "${pattern}" "${file}" | head -3 | while read -r line; do | |
| echo " ${line}" | |
| done | |
| found_warnings=1 | |
| fi | |
| done | |
| if (( found_suspicious )); then | |
| return 1 | |
| elif (( found_warnings )); then | |
| return 2 | |
| fi | |
| return 0 | |
| } | |
| # Main audit function | |
| audit_skill() { | |
| local path="${1}" | |
| local total_suspicious=0 | |
| local total_warnings=0 | |
| local files_checked=0 | |
| info "Auditing skill at: ${path}" | |
| info "Patterns checked: ${#SUSPICIOUS_PATTERNS[@]} suspicious, ${#WARNING_PATTERNS[@]} warning" | |
| echo | |
| # If it's a single file | |
| if [[ -f "${path}" ]]; then | |
| files_checked=1 | |
| check_file "${path}" | |
| case $? in | |
| 1) total_suspicious=1 ;; | |
| 2) total_warnings=1 ;; | |
| esac | |
| elif [[ -d "${path}" ]]; then | |
| # Find all relevant files | |
| while IFS= read -r -d '' file; do | |
| (( files_checked++ )) || true | |
| check_file "${file}" | |
| case $? in | |
| 1) (( total_suspicious++ )) || true ;; | |
| 2) (( total_warnings++ )) || true ;; | |
| esac | |
| done < <(find "${path}" -type f \( \ | |
| -name "*.md" -o \ | |
| -name "*.sh" -o \ | |
| -name "*.bash" -o \ | |
| -name "*.zsh" -o \ | |
| -name "*.js" -o \ | |
| -name "*.ts" -o \ | |
| -name "*.json" -o \ | |
| -name "*.yaml" -o \ | |
| -name "*.yml" -o \ | |
| -name "*.py" -o \ | |
| -name "*.rb" -o \ | |
| -name "Makefile" -o \ | |
| -name "makefile" -o \ | |
| -name "SKILL*" \ | |
| \) -print0 2>/dev/null) | |
| fi | |
| echo | |
| info "Audit complete: ${files_checked} files checked" | |
| if (( total_suspicious > 0 )); then | |
| echo -e "${RED}CRITICAL: Found ${total_suspicious} files with suspicious patterns${NC}" | |
| echo -e "${RED}Recommendation: DO NOT INSTALL this skill${NC}" | |
| return 1 | |
| elif (( total_warnings > 0 )); then | |
| echo -e "${YELLOW}WARNING: Found ${total_warnings} files with patterns requiring review${NC}" | |
| echo -e "${YELLOW}Recommendation: Review carefully before installing${NC}" | |
| return 2 | |
| else | |
| # Silent on clean scan - user should still review manually | |
| return 0 | |
| fi | |
| } | |
| # Print usage | |
| usage() { | |
| cat <<EOF | |
| Usage: ${SCRIPT_NAME} <skill-path-or-url> | |
| Audit an OpenClaw skill for suspicious patterns before installation. | |
| Arguments: | |
| skill-path-or-url Local path to skill directory/file, or URL to download | |
| Exit codes: | |
| 0 Skill appears clean | |
| 1 Suspicious patterns found (do not install) | |
| 2 Warning patterns found (review recommended) | |
| 3 Usage error | |
| Examples: | |
| ${SCRIPT_NAME} ./my-skill | |
| ${SCRIPT_NAME} https://github.com/user/skill-repo | |
| ${SCRIPT_NAME} ./SKILL.md | |
| EOF | |
| } | |
| # Main | |
| main() { | |
| if [[ $# -eq 0 ]]; then | |
| usage | |
| exit 3 | |
| fi | |
| local input="${1}" | |
| local audit_path="" | |
| local cleanup_needed=false | |
| local tmpdir="" | |
| # Handle different input types | |
| local input_type | |
| input_type=$(check_input_type "${input}") | |
| case "${input_type}" in | |
| url) | |
| tmpdir=$(mktemp -d) | |
| trap 'rm -rf "${tmpdir}"' EXIT | |
| audit_path=$(download_skill "${input}" "${tmpdir}") | |
| ;; | |
| directory|file) | |
| audit_path="${input}" | |
| ;; | |
| *) | |
| die "Cannot determine input type: ${input}" | |
| ;; | |
| esac | |
| # Run the audit | |
| audit_skill "${audit_path}" | |
| } | |
| main "${@}" |
Author
sammcj
commented
Feb 6, 2026
Author
In ~/.bashrc:
# OpenClaw Skill Security Audit - automatically audit skills before installation
source "$HOME/.openclaw/workspace/tools/skill-audit-wrapper.sh"
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment