Skip to content

Instantly share code, notes, and snippets.

@sammcj
Created February 6, 2026 20:42
Show Gist options
  • Select an option

  • Save sammcj/e351784d112f694116c97dcfe683e157 to your computer and use it in GitHub Desktop.

Select an option

Save sammcj/e351784d112f694116c97dcfe683e157 to your computer and use it in GitHub Desktop.
minimal openclaw skill-audit hook
#!/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 "${@}"
@sammcj
Copy link
Author

sammcj commented Feb 6, 2026

#!/usr/bin/env bash
# ~/.openclaw/workspace/tools/skill-audit-wrapper.sh
# skill-audit-wrapper.sh - Source this file to add skill auditing to your shell
# Add to ~/.bashrc or ~/.zshrc: source ~/.openclaw/workspace/tools/skill-audit-wrapper.sh

# Configuration
readonly SKILL_AUDIT_SCRIPT="${HOME}/.openclaw/workspace/tools/skill-audit.sh"
readonly SKILL_AUDIT_ENABLED="${SKILL_AUDIT_ENABLED:-1}"

# Colors
if [[ -t 1 ]]; then
  _SKILL_AUDIT_RED='\033[0;31m'
  _SKILL_AUDIT_YELLOW='\033[1;33m'
  _SKILL_AUDIT_GREEN='\033[0;32m'
  _SKILL_AUDIT_CYAN='\033[0;36m'
  _SKILL_AUDIT_NC='\033[0m'
else
  _SKILL_AUDIT_RED=''
  _SKILL_AUDIT_YELLOW=''
  _SKILL_AUDIT_GREEN=''
  _SKILL_AUDIT_CYAN=''
  _SKILL_AUDIT_NC=''
fi

# Check if audit script exists
if [[ ! -f "${SKILL_AUDIT_SCRIPT}" ]]; then
  echo "Warning: Skill audit script not found at ${SKILL_AUDIT_SCRIPT}" >&2
  return 1
fi

# Wrapper function for clawhub
clawhub() {
  local cmd="${1:-}"

  # Only intercept 'install' command when audit is enabled
  if [[ "${cmd}" == "install" ]] && [[ "${SKILL_AUDIT_ENABLED}" == "1" ]]; then
    shift  # Remove 'install'

    if [[ $# -eq 0 ]]; then
      command clawhub install
      return $?
    fi

    local skill_name="${1}"
    shift  # Remove skill name

    echo -e "${_SKILL_AUDIT_CYAN}🔒 Running pre-install security audit for: ${skill_name}${_SKILL_AUDIT_NC}"
    echo

    # Create temp directory for audit
    local tmpdir
    tmpdir=$(mktemp -d)

    # Cleanup function
    _skill_audit_cleanup() {
      rm -rf "${tmpdir}"
    }
    trap _skill_audit_cleanup EXIT

    local audit_target=""
    local audit_result=0

    # Determine how to fetch the skill
    if [[ "${skill_name}" =~ ^https?:// ]] || [[ -d "${skill_name}" ]] || [[ -f "${skill_name}" ]]; then
      # URL or local path - audit directly
      audit_target="${skill_name}"
    else
      # Registry skill - need to fetch first
      echo "Fetching skill '${skill_name}' for audit..."

      # Try to download using clawhub's internal mechanism
      # We'll install to temp, audit, then install for real if clean
      if command clawhub install "${skill_name}" --dir "${tmpdir}/skill" 2>/dev/null; then
        audit_target="${tmpdir}/skill"
      else
        echo -e "${_SKILL_AUDIT_YELLOW}Warning: Could not fetch skill for audit. Proceeding with installation...${_SKILL_AUDIT_NC}"
        audit_result=0
      fi
    fi

    # Run audit if we have a target
    if [[ -n "${audit_target}" ]]; then
      "${SKILL_AUDIT_SCRIPT}" "${audit_target}"
      audit_result=$?
    fi

    # Cleanup temp files
    _skill_audit_cleanup
    trap - EXIT

    echo

    # Handle audit results
    case ${audit_result} in
      0)
        # Proceed silently if clean
        command clawhub install "${skill_name}" "${@}"
        ;;
        command clawhub install "${skill_name}" "${@}"
        ;;
      1)
        echo -e "${_SKILL_AUDIT_RED}✗ Security audit FAILED - critical suspicious patterns detected${_SKILL_AUDIT_NC}"
        echo -e "${_SKILL_AUDIT_RED}Installation aborted to protect your system.${_SKILL_AUDIT_NC}"
        echo
        echo "If you're certain this skill is safe, you can bypass the audit:"
        echo "  SKILL_AUDIT_ENABLED=0 clawhub install ${skill_name}"
        echo
        return 1
        ;;
      2)
        echo -e "${_SKILL_AUDIT_YELLOW}⚠ Security audit found warnings that require review${_SKILL_AUDIT_NC}"
        echo
        read -rp "Do you want to proceed with installation anyway? [y/N] " response
        if [[ "${response}" =~ ^[Yy]$ ]]; then
          echo -e "${_SKILL_AUDIT_CYAN}Proceeding with installation...${_SKILL_AUDIT_NC}"
          echo
          command clawhub install "${skill_name}" "${@}"
        else
          echo "Installation cancelled."
          return 0
        fi
        ;;
      *)
        echo "Warning: Audit script returned unknown exit code: ${audit_result}" >&2
        echo "Proceeding with installation..."
        command clawhub install "${skill_name}" "${@}"
        ;;
    esac

    return $?
  else
    # Pass through to real clawhub
    command clawhub "${@}"
    return $?
  fi
}

# Also create alias for clawhub install with audit
alias clawhub-audit-on='SKILL_AUDIT_ENABLED=1'
alias clawhub-audit-off='SKILL_AUDIT_ENABLED=0'

# Silent by default - only show message in verbose mode
if [[ "${SKILL_AUDIT_VERBOSE:-}" == "1" ]] && [[ -t 1 ]]; then
  echo "Skill audit wrapper loaded. 'clawhub install' will now audit skills before installation."
  echo "To disable: SKILL_AUDIT_ENABLED=0 clawhub install <skill>"
  echo "To permanently disable: unset SKILL_AUDIT_ENABLED in your shell config"
fi

@sammcj
Copy link
Author

sammcj commented Feb 6, 2026

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