#!/usr/bin/env bash # audit_environment.sh — Audit shell environment and paths on macOS # Output: JSON lines to stdout, one per finding set -euo pipefail json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; } json_finding() { local severity="$1" finding action details finding=$(json_escape "$2") action=$(json_escape "$3") details=$(json_escape "$4") printf '{"module":"environment","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \ "$severity" "$finding" "$action" "$details" } # --- PATH analysis --- IFS=':' read -ra path_dirs <<< "$PATH" total_dirs=${#path_dirs[@]} # Duplicate detection (compatible with bash 3) duplicates=() for i in "${!path_dirs[@]}"; do dir="${path_dirs[$i]}" is_dup=false for j in "${!path_dirs[@]}"; do if [ "$j" -lt "$i" ] && [ "${path_dirs[$j]}" = "$dir" ]; then is_dup=true break fi done if $is_dup; then duplicates+=("$dir") fi done if [ ${#duplicates[@]} -gt 0 ]; then dup_list=$(printf '%s, ' "${duplicates[@]}" | sed 's/, $//') json_finding "warning" "${#duplicates[@]} duplicate PATH entries" "deduplicate" "$dup_list" else json_finding "info" "No duplicate PATH entries" "none" "${total_dirs} directories in PATH" fi # Non-existent directories missing=() for dir in "${path_dirs[@]}"; do if [ ! -d "$dir" ]; then missing+=("$dir") fi done if [ ${#missing[@]} -gt 0 ]; then missing_list=$(printf '%s, ' "${missing[@]}" | sed 's/, $//') json_finding "warning" "${#missing[@]} non-existent directories in PATH" "remove" "$missing_list" fi # --- Broken symlinks --- broken_symlinks=() for check_dir in /usr/local/bin "$HOME/.local/bin"; do if [ -d "$check_dir" ]; then while IFS= read -r link; do broken_symlinks+=("$link") done < <(find "$check_dir" -maxdepth 1 -type l ! -exec test -e {} \; -print 2>/dev/null || true) fi done if [ ${#broken_symlinks[@]} -gt 0 ]; then broken_list=$(printf '%s, ' "${broken_symlinks[@]}" | sed 's/, $//') json_finding "warning" "${#broken_symlinks[@]} broken symlinks found" "remove" "$broken_list" else json_finding "info" "No broken symlinks in bin directories" "none" "" fi # --- Shell config analysis --- shell_configs=() for cfg in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bash_profile" "$HOME/.bashrc"; do if [ -f "$cfg" ]; then shell_configs+=("$(basename "$cfg")") fi done json_finding "info" "Shell configs found: ${shell_configs[*]}" "none" "" # Check for PATH modifications across multiple files path_mods=0 for cfg in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bash_profile" "$HOME/.bashrc"; do if [ -f "$cfg" ]; then count=$(grep -c -E 'export PATH|PATH=' "$cfg" 2>/dev/null || echo 0) count=$(echo "$count" | tr -d '[:space:]') path_mods=$((path_mods + count)) fi done if [ "$path_mods" -gt 5 ]; then json_finding "warning" "PATH modified ${path_mods} times across shell configs" "consolidate" "Consider consolidating PATH modifications" fi # --- Environment variable checks --- if [ -z "${ANTHROPIC_API_KEY:-}" ]; then json_finding "info" "ANTHROPIC_API_KEY not set" "none" "Set if using Claude API" fi if [ -n "${JAVA_HOME:-}" ] && [ ! -d "$JAVA_HOME" ]; then json_finding "warning" "JAVA_HOME points to non-existent directory" "fix" "$JAVA_HOME" fi # --- Shell startup time --- if command -v zsh &>/dev/null; then startup_time=$( { time zsh -i -c exit; } 2>&1 | grep real | awk '{print $2}' || echo "unknown") # Parse the time - handle both formats like "0m0.123s" and "0.123" if echo "$startup_time" | grep -q 'm'; then seconds=$(echo "$startup_time" | sed 's/.*m//' | sed 's/s//') else seconds="$startup_time" fi if command -v python3 &>/dev/null; then is_slow=$(python3 -c "print('slow' if float('${seconds}') > 0.5 else 'ok')" 2>/dev/null || echo "ok") else is_slow="ok" fi if [ "$is_slow" = "slow" ]; then json_finding "warning" "Shell startup is slow (${startup_time})" "optimize" "Consider profiling with zsh -xv" else json_finding "info" "Shell startup time: ${startup_time}" "none" "" fi fi