From 7dce6ed648d2f94113ef8f6d00ab4e0da55b04ec Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 14:52:45 +0200 Subject: [PATCH 1/4] Scope gh permissions: read-only auto-allow, writes require confirmation Replace broad Bash(gh pr:*) and Bash(gh api:*) allow rules with granular read-only allows (view, list, diff, checks, status, search) and an explicit ask list for write operations (create, edit, comment, close, ready, merge, POST API calls). --- .claude/settings.json | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index af23312..68500e5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -19,11 +19,36 @@ "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:*)", + "Bash(gh api * --method GET *)", + "Bash(gh api * --jq *)" ], "deny": [ "Read(./.env*)" + ], + "ask": [ + "Bash(gh pr create:*)", + "Bash(gh pr edit:*)", + "Bash(gh pr comment:*)", + "Bash(gh pr close:*)", + "Bash(gh pr ready:*)", + "Bash(gh pr merge:*)", + "Bash(gh api * -X POST *)", + "Bash(gh api * --method POST *)", + "Bash(gh api * -f *)" ] }, "hooks": { From a4e45c0b76046c969063bb45423314808903d863 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 14:54:25 +0200 Subject: [PATCH 2/4] Drop gh api permission rules - flexible arg order defeats prefix matching --- .claude/settings.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 68500e5..e92fb8d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -32,9 +32,7 @@ "Bash(gh release view:*)", "Bash(gh release list:*)", "Bash(gh repo view:*)", - "Bash(gh search:*)", - "Bash(gh api * --method GET *)", - "Bash(gh api * --jq *)" + "Bash(gh search:*)" ], "deny": [ "Read(./.env*)" @@ -45,10 +43,7 @@ "Bash(gh pr comment:*)", "Bash(gh pr close:*)", "Bash(gh pr ready:*)", - "Bash(gh pr merge:*)", - "Bash(gh api * -X POST *)", - "Bash(gh api * --method POST *)", - "Bash(gh api * -f *)" + "Bash(gh pr merge:*)" ] }, "hooks": { From 20c98675638356d165bbd2c961b0ac3de08e39f3 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 15:00:17 +0200 Subject: [PATCH 3/4] Remove explicit ask rules - unmatched gh commands prompt by default --- .claude/settings.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index e92fb8d..6111672 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -36,14 +36,6 @@ ], "deny": [ "Read(./.env*)" - ], - "ask": [ - "Bash(gh pr create:*)", - "Bash(gh pr edit:*)", - "Bash(gh pr comment:*)", - "Bash(gh pr close:*)", - "Bash(gh pr ready:*)", - "Bash(gh pr merge:*)" ] }, "hooks": { From 91c695d2fb27cfc44cd6bf2a22db13d59cd4c411 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 15:07:18 +0200 Subject: [PATCH 4/4] Apply review feedback from chartmogul-node PR to hooks - Use session_id from hook input instead of generating a custom one - Respect TMPDIR instead of hardcoding /tmp - Skip rspec when no files were edited --- .claude/hooks/post-edit-lint.sh | 2 +- .claude/hooks/post-stop-validate.sh | 20 ++++++++++++-------- .claude/hooks/session-start-setup.sh | 7 ++++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.claude/hooks/post-edit-lint.sh b/.claude/hooks/post-edit-lint.sh index 08b38b1..300d9ed 100755 --- a/.claude/hooks/post-edit-lint.sh +++ b/.claude/hooks/post-edit-lint.sh @@ -6,7 +6,7 @@ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Only track .rb files, skip vendored/generated paths if [[ "$FILE" == *.rb ]] && [[ "$FILE" != */vendor/* ]] && [[ "$FILE" != */coverage/* ]]; then - TRACKER="/tmp/claude-edited-rb-files-${CLAUDE_HOOK_SESSION_ID:-default}" + TRACKER="${TMPDIR:-/tmp}/claude-edited-rb-files-${CLAUDE_HOOK_SESSION_ID:-default}" echo "$FILE" >> "$TRACKER" sort -u "$TRACKER" -o "$TRACKER" fi diff --git a/.claude/hooks/post-stop-validate.sh b/.claude/hooks/post-stop-validate.sh index adea946..b80f8e5 100755 --- a/.claude/hooks/post-stop-validate.sh +++ b/.claude/hooks/post-stop-validate.sh @@ -1,9 +1,10 @@ #!/bin/bash cd "$CLAUDE_PROJECT_DIR" || exit 0 -TRACKER="/tmp/claude-edited-rb-files-${CLAUDE_HOOK_SESSION_ID:-default}" +TRACKER="${TMPDIR:-/tmp}/claude-edited-rb-files-${CLAUDE_HOOK_SESSION_ID:-default}" ctx="" +had_edits=false # Batch rubocop autocorrect on tracked files (if any were edited) if [[ -f "$TRACKER" ]]; then @@ -11,6 +12,7 @@ if [[ -f "$TRACKER" ]]; then rm -f "$TRACKER" if [[ -n "$files" ]]; then + had_edits=true rubocop_out=$(echo "$files" | xargs bundle exec rubocop -a 2>&1) || true offenses=$(echo "$rubocop_out" | grep -E "^.+:[0-9]+:[0-9]+:" | head -20) if [[ -n "$offenses" ]]; then @@ -19,13 +21,15 @@ if [[ -f "$TRACKER" ]]; then fi fi -# Always run rspec - edits to specs or lib code both matter (~1s) -rspec_out=$(bundle exec rspec 2>&1) -rspec_exit=$? -if [[ $rspec_exit -ne 0 ]]; then - summary=$(echo "$rspec_out" | grep -E "[0-9]+ examples?, [0-9]+ failures?" | tail -1) - failed=$(echo "$rspec_out" | grep -E "^rspec .+" | head -10) - ctx+="rspec failed ($summary):\n$failed\n" +# Only run rspec if files were edited (~1s) +if [[ "$had_edits" == true ]]; then + rspec_out=$(bundle exec rspec 2>&1) + rspec_exit=$? + if [[ $rspec_exit -ne 0 ]]; then + summary=$(echo "$rspec_out" | grep -E "[0-9]+ examples?, [0-9]+ failures?" | tail -1) + failed=$(echo "$rspec_out" | grep -E "^rspec .+" | head -10) + ctx+="rspec failed ($summary):\n$failed\n" + fi fi if [[ -n "$ctx" ]]; then diff --git a/.claude/hooks/session-start-setup.sh b/.claude/hooks/session-start-setup.sh index 122a877..5301889 100755 --- a/.claude/hooks/session-start-setup.sh +++ b/.claude/hooks/session-start-setup.sh @@ -1,10 +1,11 @@ #!/bin/bash cd "$CLAUDE_PROJECT_DIR" || exit 0 -# Generate a stable session ID and persist via CLAUDE_ENV_FILE +# Read 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)-$$" -if [[ -n "$CLAUDE_ENV_FILE" ]]; then +INPUT=$(cat) +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') +if [[ -n "$SESSION_ID" ]] && [[ -n "$CLAUDE_ENV_FILE" ]]; then echo "export CLAUDE_HOOK_SESSION_ID='$SESSION_ID'" >> "$CLAUDE_ENV_FILE" fi