mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* chore: add reviewer-feedback guardrails (CLAUDE.md, pre-commit hook, skill) Analysis of ~50 PRs from the past week identified 10 recurring themes in Copilot and Gemini code review comments. This change addresses them at development time through three layers: 1. CLAUDE.md additions (7 new rules): - Transaction safety for multi-step DB operations - UTF-8 string safety (no byte-index slicing) - Case-insensitive comparisons for paths/media types - Decorator/wrapper trait method delegation - Sensitive data redaction in logs/SSE - tempfile crate for test temporary files - Trust boundaries for worker container data 2. Pre-commit hook (scripts/pre-commit-safety.sh): Mechanical checks for unsafe byte slicing, case-sensitive extension comparisons, hardcoded /tmp paths, unredacted tool parameter logging, and non-transactional DB operations. Installed via dev-setup.sh alongside existing commit-msg hook. 3. Review checklist skill (skills/review-checklist/SKILL.md): Activates on "review"/"merge" keywords. Covers the judgment-based items that can't be linted: transaction safety, SSRF validation, approval checks, decorator delegation, test quality, and doc accuracy. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review feedback on pre-commit-safety.sh - Cache diff output in variable to avoid ~10 redundant git diff calls (Gemini) - Add early exit when no .rs files are changed (Gemini) - Fix header comment: list all 5 checks, not just 4 (Copilot) - Fix check 2 comment: only mentions file extensions, not media types (Copilot) - Add resolve_base_ref() with fallback candidates instead of hardcoded origin/main for standalone mode (Copilot) - TX check: use -W (function context) to reduce false positives, honor // safety: suppression, print triggering lines (Copilot) [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
137 lines
5.7 KiB
Bash
Executable File
137 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Pre-commit safety checks for common issues caught by AI code reviewers.
|
|
#
|
|
# Can be run standalone: bash scripts/pre-commit-safety.sh
|
|
# Or installed as a git pre-commit hook via dev-setup.sh.
|
|
#
|
|
# Checks staged .rs files for:
|
|
# 1. Unsafe UTF-8 byte slicing (panics on multi-byte chars)
|
|
# 2. Case-sensitive file extension comparisons
|
|
# 3. Hardcoded /tmp paths in tests (flaky in parallel runs)
|
|
# 4. Tool parameters logged without redaction (secret leaks)
|
|
# 5. Multi-step DB operations without transaction wrapping
|
|
#
|
|
# Suppress individual lines with an inline "// safety: <reason>" comment.
|
|
|
|
set -euo pipefail
|
|
|
|
# Determine a suitable base ref for standalone diffs.
|
|
resolve_base_ref() {
|
|
local candidates=(
|
|
"@{upstream}"
|
|
"origin/HEAD"
|
|
"origin/main"
|
|
"origin/master"
|
|
"main"
|
|
"master"
|
|
)
|
|
|
|
for ref in "${candidates[@]}"; do
|
|
if git rev-parse --verify --quiet "$ref" >/dev/null 2>&1; then
|
|
echo "$ref"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
echo "pre-commit-safety: could not determine a base Git ref for diff (tried: ${candidates[*]})." >&2
|
|
echo "pre-commit-safety: ensure your repository has an upstream or a local main/master branch." >&2
|
|
exit 1
|
|
}
|
|
|
|
# Support both pre-commit hook (staged files) and standalone (all changed vs base)
|
|
if git diff --cached --quiet 2>/dev/null; then
|
|
# No staged changes -- compare working tree against a resolved base ref
|
|
BASE_REF="$(resolve_base_ref)"
|
|
DIFF_OUTPUT=$(git diff "$BASE_REF" -- '*.rs' 2>/dev/null || true)
|
|
else
|
|
DIFF_OUTPUT=$(git diff --cached -U0 -- '*.rs' 2>/dev/null || true)
|
|
fi
|
|
|
|
# Early exit if there are no relevant .rs changes
|
|
if [ -z "$DIFF_OUTPUT" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
WARNINGS=0
|
|
|
|
warn() {
|
|
if [ "$WARNINGS" -eq 0 ]; then
|
|
echo ""
|
|
echo "=== Pre-commit Safety Checks ==="
|
|
echo ""
|
|
fi
|
|
WARNINGS=$((WARNINGS + 1))
|
|
echo " [$1] $2"
|
|
}
|
|
|
|
# 1. Unsafe UTF-8 byte slicing: &s[..N] or &s[..some_var] on strings
|
|
# Safe patterns: is_char_boundary, char_indices, // safety:
|
|
if echo "$DIFF_OUTPUT" | grep -nE '^\+' | grep -E '\[\.\..*\]' | grep -vE 'is_char_boundary|char_indices|// safety:|as_bytes|Vec<|&\[u8\]|\[u8\]|bytes\(\)|&bytes' | head -3 | grep -q .; then
|
|
warn "UTF8" "Possible unsafe byte-index string slicing. Use is_char_boundary() or char_indices()."
|
|
echo "$DIFF_OUTPUT" | grep -nE '^\+' | grep -E '\[\.\..*\]' | grep -vE 'is_char_boundary|char_indices|// safety:|as_bytes|Vec<|&\[u8\]|\[u8\]|bytes\(\)|&bytes' | head -3 | sed 's/^/ /'
|
|
fi
|
|
|
|
# 2. Case-sensitive file extension checks
|
|
# Match: .ends_with(".png") without prior to_lowercase
|
|
if echo "$DIFF_OUTPUT" | grep -nE '^\+.*ends_with\("\.([pP][nN][gG]|[jJ][pP][eE]?[gG]|[gG][iI][fF]|[wW][eE][bB][pP]|[mM][dD])"\)' | grep -vE 'to_lowercase|to_ascii_lowercase|// safety:' | head -3 | grep -q .; then
|
|
warn "CASE" "Case-sensitive file extension comparison. Normalize to lowercase first."
|
|
echo "$DIFF_OUTPUT" | grep -nE '^\+.*ends_with\("\.([pP][nN][gG]|[jJ][pP][eE]?[gG]|[gG][iI][fF]|[wW][eE][bB][pP]|[mM][dD])"\)' | grep -vE 'to_lowercase|to_ascii_lowercase|// safety:' | head -3 | sed 's/^/ /'
|
|
fi
|
|
|
|
# 3. Hardcoded /tmp paths in test files
|
|
if echo "$DIFF_OUTPUT" | grep -nE '^\+.*"/tmp/' | grep -vE 'tempfile|tempdir|// safety:' | head -3 | grep -q .; then
|
|
warn "TMPDIR" "Hardcoded /tmp path. Use tempfile::tempdir() for parallel-safe tests."
|
|
echo "$DIFF_OUTPUT" | grep -nE '^\+.*"/tmp/' | grep -vE 'tempfile|tempdir|// safety:' | head -3 | sed 's/^/ /'
|
|
fi
|
|
|
|
# 4. Logging tool parameters without redaction
|
|
if echo "$DIFF_OUTPUT" | grep -nE '^\+.*tracing::(info|debug|warn|error).*param' | grep -vE 'redact|// safety:' | head -3 | grep -q .; then
|
|
warn "REDACT" "Logging tool parameters without redaction. Use redact_params() first."
|
|
echo "$DIFF_OUTPUT" | grep -nE '^\+.*tracing::(info|debug|warn|error).*param' | grep -vE 'redact|// safety:' | head -3 | sed 's/^/ /'
|
|
fi
|
|
|
|
# 5. Multi-step DB operations without transaction
|
|
# Uses -W (function context) to reduce false positives from existing transactions.
|
|
# Suppressible with "// safety:" in the hunk.
|
|
DIFF_W_OUTPUT=$(git diff --cached -W -- '*.rs' 2>/dev/null || git diff "$(resolve_base_ref)" -W -- '*.rs' 2>/dev/null || true)
|
|
if [ -n "$DIFF_W_OUTPUT" ]; then
|
|
HUNK_COUNT=$(echo "$DIFF_W_OUTPUT" | awk '
|
|
/^@@/ {
|
|
if (count >= 2 && !has_tx && !has_safety) found++
|
|
count=0; has_tx=0; has_safety=0
|
|
}
|
|
/^\+.*\.(execute|query)\(/ { count++ }
|
|
/^\+.*(transaction|\.tx\.|\.begin\()/ { has_tx=1 }
|
|
/ .*(transaction|\.tx\.|\.begin\()/ { has_tx=1 }
|
|
/\/\/ safety:/ { has_safety=1 }
|
|
END {
|
|
if (count >= 2 && !has_tx && !has_safety) found++
|
|
print found+0
|
|
}
|
|
')
|
|
if [ "$HUNK_COUNT" -gt 0 ]; then
|
|
warn "TX" "Multiple DB operations in same function without transaction. Wrap in a transaction for atomicity."
|
|
echo "$DIFF_W_OUTPUT" | awk '
|
|
/^@@/ {
|
|
if (count >= 2 && !has_tx && !has_safety) { print buf }
|
|
buf=""; count=0; has_tx=0; has_safety=0
|
|
}
|
|
/^\+.*\.(execute|query)\(/ { count++ }
|
|
/^\+.*(transaction|\.tx\.|\.begin\()/ { has_tx=1 }
|
|
/ .*(transaction|\.tx\.|\.begin\()/ { has_tx=1 }
|
|
/\/\/ safety:/ { has_safety=1 }
|
|
{ buf = buf "\n" $0 }
|
|
END {
|
|
if (count >= 2 && !has_tx && !has_safety) { print buf }
|
|
}
|
|
' | grep -E '^\+.*\.(execute|query)\(' | head -4 | sed 's/^/ /'
|
|
fi
|
|
fi
|
|
|
|
if [ "$WARNINGS" -gt 0 ]; then
|
|
echo ""
|
|
echo "Found $WARNINGS potential issue(s). Fix them or add '// safety: <reason>' to suppress."
|
|
echo ""
|
|
exit 1
|
|
fi
|