From fe9560f98daa561df035c04ce30b34991bfb00b3 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Fri, 17 Apr 2026 10:27:23 +0200 Subject: [PATCH] Refine AI hook pipeline and tighten gh permissions - session-start.sh: use session_id from hook input instead of generating a custom one, with fallback to timestamp-pid - post-edit-record.sh, stop-format.sh: use ${TMPDIR:-/tmp} instead of hardcoded /tmp to respect contributor's environment - stop-format.sh: skip formatting and tests entirely when no files were edited this turn (no wasted Docker containers on read-only turns) - settings.json: replace broad Bash(gh pr:*) and Bash(gh api:*) with granular read-only allows (view, list, diff, checks, status, search); write operations (create, edit, comment, merge) prompt by default --- .claude/hooks/post-edit-record.sh | 2 +- .claude/hooks/session-start.sh | 8 +++-- .claude/hooks/stop-format.sh | 49 ++++++++++++++++--------------- .claude/settings.json | 16 ++++++++-- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/.claude/hooks/post-edit-record.sh b/.claude/hooks/post-edit-record.sh index 4baf288..8f9bae1 100755 --- a/.claude/hooks/post-edit-record.sh +++ b/.claude/hooks/post-edit-record.sh @@ -8,7 +8,7 @@ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Only track .php files, skip vendored/generated paths if [[ "$FILE" == *.php ]] && [[ "$FILE" != vendor/* ]] && [[ "$FILE" != */vendor/* ]] && [[ "$FILE" != coverage/* ]] && [[ "$FILE" != */coverage/* ]]; then - TRACKER="/tmp/claude-edited-php-files-${CLAUDE_HOOK_SESSION_ID:-default}" + TRACKER="${TMPDIR:-/tmp}/claude-edited-php-files-${CLAUDE_HOOK_SESSION_ID:-default}" echo "$FILE" >> "$TRACKER" sort -u "$TRACKER" -o "$TRACKER" fi diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 122753b..00a6815 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -3,9 +3,13 @@ # No network calls, no installs, no docker run - just detect and report. cd "$CLAUDE_PROJECT_DIR" || exit 0 -# Generate a stable session ID and persist via CLAUDE_ENV_FILE +# Use session_id from hook input and persist via CLAUDE_ENV_FILE # so the edit tracker and stop hook share the same file path -SESSION_ID="$(date +%s)-$$" +INPUT=$(cat) +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') +if [[ -z "$SESSION_ID" ]]; then + SESSION_ID="$(date +%s)-$$" +fi if [[ -n "$CLAUDE_ENV_FILE" ]]; then echo "export CLAUDE_HOOK_SESSION_ID='$SESSION_ID'" >> "$CLAUDE_ENV_FILE" fi diff --git a/.claude/hooks/stop-format.sh b/.claude/hooks/stop-format.sh index 55b1bf2..561ce76 100755 --- a/.claude/hooks/stop-format.sh +++ b/.claude/hooks/stop-format.sh @@ -1,36 +1,37 @@ #!/bin/bash # Stop hook: batch-format PHP files touched this turn, run test suite, report issues. -# Tests are fast (~2s) so we include them every turn - failures are reported as context. +# Tests are fast (~2s) so we include them when files were edited. +# Failures are reported as context, not blocking. cd "$CLAUDE_PROJECT_DIR" || exit 0 -TRACKER="/tmp/claude-edited-php-files-${CLAUDE_HOOK_SESSION_ID:-default}" +TRACKER="${TMPDIR:-/tmp}/claude-edited-php-files-${CLAUDE_HOOK_SESSION_ID:-default}" + +# Skip entirely if no files were edited this turn +[[ -f "$TRACKER" ]] || exit 0 + +files=$(cat "$TRACKER") +rm -f "$TRACKER" +[[ -n "$files" ]] || exit 0 ctx="" -# Batch php-cs-fixer autocorrect on tracked files (if any were edited) -if [[ -f "$TRACKER" ]]; then - files=$(cat "$TRACKER") - rm -f "$TRACKER" - - if [[ -n "$files" ]]; then - if docker image inspect chartmogulphp82 &>/dev/null 2>&1; then - file_args=$(echo "$files" | tr '\n' ' ') - docker run --rm -w /src -v "$(pwd):/src" -v "$HOME/.composer/cache:/root/.composer/cache" chartmogulphp82 \ - bash -c "vendor/bin/php-cs-fixer fix --no-interaction --quiet $file_args" 2>/dev/null || true - lint_out=$(docker run --rm -w /src -v "$(pwd):/src" -v "$HOME/.composer/cache:/root/.composer/cache" chartmogulphp82 \ - bash -c "vendor/bin/php-cs-fixer fix --dry-run --no-interaction $file_args 2>&1") - elif [[ -x vendor/bin/php-cs-fixer ]]; then - echo "$files" | xargs vendor/bin/php-cs-fixer fix --no-interaction --quiet 2>/dev/null || true - lint_out=$(echo "$files" | xargs vendor/bin/php-cs-fixer fix --dry-run --no-interaction 2>&1) - fi - offenses=$(echo "$lint_out" | grep -E "^\s+\d+\)" | head -20) - if [[ -n "$offenses" ]]; then - ctx+="php-cs-fixer offenses remaining after autocorrect:\n$offenses\n" - fi - fi +# Batch php-cs-fixer autocorrect on tracked files +if docker image inspect chartmogulphp82 &>/dev/null 2>&1; then + file_args=$(echo "$files" | tr '\n' ' ') + docker run --rm -w /src -v "$(pwd):/src" -v "$HOME/.composer/cache:/root/.composer/cache" chartmogulphp82 \ + bash -c "vendor/bin/php-cs-fixer fix --no-interaction --quiet $file_args" 2>/dev/null || true + lint_out=$(docker run --rm -w /src -v "$(pwd):/src" -v "$HOME/.composer/cache:/root/.composer/cache" chartmogulphp82 \ + bash -c "vendor/bin/php-cs-fixer fix --dry-run --no-interaction $file_args 2>&1") +elif [[ -x vendor/bin/php-cs-fixer ]]; then + echo "$files" | xargs vendor/bin/php-cs-fixer fix --no-interaction --quiet 2>/dev/null || true + lint_out=$(echo "$files" | xargs vendor/bin/php-cs-fixer fix --dry-run --no-interaction 2>&1) +fi +offenses=$(echo "$lint_out" | grep -E "^\s+\d+\)" | head -20) +if [[ -n "$offenses" ]]; then + ctx+="php-cs-fixer offenses remaining after autocorrect:\n$offenses\n" fi -# Always run tests - edits to tests or src both matter (~2s) +# Run tests - only when files were edited this turn (~2s) if docker image inspect chartmogulphp82 &>/dev/null 2>&1; then test_out=$(docker run --rm -w /src -v "$(pwd):/src" -v "$HOME/.composer/cache:/root/.composer/cache" chartmogulphp82 \ phpunit 2>&1) diff --git a/.claude/settings.json b/.claude/settings.json index 578e0e3..e2a0333 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -22,8 +22,20 @@ "Bash(git pull:*)", "Bash(git cherry-pick:*)", "Bash(git reset:*)", - "Bash(gh pr:*)", - "Bash(gh api:*)" + "Bash(gh pr view:*)", + "Bash(gh pr list:*)", + "Bash(gh pr diff:*)", + "Bash(gh pr checks:*)", + "Bash(gh pr status:*)", + "Bash(gh run view:*)", + "Bash(gh run list:*)", + "Bash(gh run watch:*)", + "Bash(gh issue view:*)", + "Bash(gh issue list:*)", + "Bash(gh release view:*)", + "Bash(gh release list:*)", + "Bash(gh repo view:*)", + "Bash(gh search:*)" ], "deny": [ "Read(./.env*)"