From e5a08ebb45c6a18aa4f66bd252d58fcc73291ccd Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sat, 20 Jun 2026 21:53:24 +0000 Subject: [PATCH] fix(hooks): preserve exec bit on re-run and fix pipefail on empty grep Two bugs in setup-hooks.sh: 1. awk+mv to replace a marker block created a 644 temp file, losing the exec bit. chmod +x after every write_block call fixes this. Regression test added to idempotency block. 2. set -euo pipefail in the deployed pre-commit hook caused grep to exit 1 when no files of a given type were staged, aborting the hook. Changed all filter pipes to process substitution with || true so no-match is handled gracefully. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01TP4EGbBg3XMcyF28Lx78XJ --- scripts/setup-hooks.sh | 17 +++++++++-------- tests/test-setup-hooks.sh | 5 +++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh index 3bca889..77695a3 100755 --- a/scripts/setup-hooks.sh +++ b/scripts/setup-hooks.sh @@ -44,6 +44,7 @@ write_block() { fi printf '\n%s\n%s\n%s\n' "$MARKER" "$block_content" "$END_MARKER" >> "$hook_file" + chmod +x "$hook_file" } ensure_hook() { @@ -85,40 +86,40 @@ staged=$(git diff --cached --name-only --diff-filter=ACM) # shellcheck on staged .sh files if command -v shellcheck &>/dev/null; then - echo "$staged" | grep '\.sh$' | while IFS= read -r f; do + while IFS= read -r f; do [[ -f "$f" ]] && shellcheck "$f" - done + done < <(echo "$staged" | grep '\.sh$' || true) else echo "Warning: shellcheck not installed — shell script linting skipped" >&2 fi # jq validation on staged .json files if command -v jq &>/dev/null; then - echo "$staged" | grep '\.json$' | while IFS= read -r f; do + while IFS= read -r f; do [[ -f "$f" ]] && jq . "$f" > /dev/null - done + done < <(echo "$staged" | grep '\.json$' || true) else echo "Warning: jq not installed — JSON validation skipped" >&2 fi # yq validation on staged .yaml/.yml files if command -v yq &>/dev/null; then - echo "$staged" | grep -E '\.(yaml|yml)$' | while IFS= read -r f; do + while IFS= read -r f; do [[ -f "$f" ]] && yq eval '.' "$f" > /dev/null - done + done < <(echo "$staged" | grep -E '\.(yaml|yml)$' || true) else echo "Warning: yq not installed — YAML validation skipped" >&2 fi # SKILL.md frontmatter: must have name: and description: -echo "$staged" | grep 'SKILL\.md$' | while IFS= read -r f; do +while IFS= read -r f; do if [[ -f "$f" ]]; then if ! grep -q '^name:' "$f" || ! grep -q '^description' "$f"; then echo "ERROR: $f is missing required frontmatter fields (name: and description:)" >&2 exit 1 fi fi -done +done < <(echo "$staged" | grep 'SKILL\.md$' || true) BLOCK } diff --git a/tests/test-setup-hooks.sh b/tests/test-setup-hooks.sh index e740428..f0c0db3 100644 --- a/tests/test-setup-hooks.sh +++ b/tests/test-setup-hooks.sh @@ -158,6 +158,11 @@ for hook_file in "$REPO5/.git/hooks/commit-msg" "$REPO5/.git/hooks/pre-commit" " else fail "idempotent: $hook_name marker appears $count times — block duplicated" fi + if [[ -x "$hook_file" ]]; then + pass "idempotent: $hook_name remains executable after 3 runs" + else + fail "idempotent: $hook_name lost executable bit after repeated runs" + fi done # --- 7. Hooks contain graceful degradation paths for missing tools ---