Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions .claude/hooks/pre-pr-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,52 @@ if [[ "$COMMAND" != *"gh pr create"* ]]; then
exit 0
fi

# Determine base branch (default: main)
BASE="main"
if echo "$COMMAND" | grep -qoP '(?<=--base\s)\S+'; then
BASE=$(echo "$COMMAND" | grep -oP '(?<=--base\s)\S+')
# Parse the command as one line, with backslash continuations joined. Quoted strings (the --title and --body
# values, including a heredoc inside "$(...)") and a bare heredoc are dropped before reading flags, so text inside
# them cannot match. --base/--head values are unquoted first so they survive.
ONE_LINE=$(printf '%s\n' "$COMMAND" | sed -e ':a' -e '/\\$/N' -e 's/\\\n//' -e 'ta' | tr '\n' ' ')
FLAGS=$(printf '%s\n' "$ONE_LINE" | sed -E "s/((--base|-B|--head|-H)[[:space:]=]+)\"([^\"]*)\"/\1\3/g; s/((--base|-B|--head|-H)[[:space:]=]+)'([^']*)'/\1\3/g; s/\"[^\"]*\"//g; s/'[^']*'//g; s/<<.*//")

# Only a command that runs gh pr create, not one that mentions it (a commit message, a grep)
RUNS_PR_CREATE='(^|[;&|(])[[:space:]]*gh[[:space:]]+pr[[:space:]]+create'
if ! [[ "$FLAGS" =~ $RUNS_PR_CREATE ]]; then
exit 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heredoc erases later create command

Medium Severity

Flattening the full command and then cutting FLAGS at << also deletes a later gh pr create. RUNS_PR_CREATE then fails and the hook exits 0, so a heredoc-then-create sequence never gets a freshness check.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a137690. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create-command regex misses valid invocations

Medium Severity

RUNS_PR_CREATE only accepts gh at the start of FLAGS or after [;&|(]. An env assignment, a full path to gh, or a newline-separated command (flattened to spaces) leaves gh after other tokens, so the hook skips the check.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a137690. Configure here.

fi

# Check the repo the command runs in: the target of the last `cd` before gh pr create, else the session's cwd.
# A relative target is resolved against the session's cwd.
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
BEFORE="; ${ONE_LINE%%gh pr create*}"
REPO_DIR=$(printf '%s\n' "$BEFORE" | sed -nE "s/.*[;&|][[:space:]]*cd[[:space:]]+(\"([^\"]+)\"|'([^']+)'|([^;&|[:space:]]+)).*/\2\3\4/p")
REPO_DIR="${REPO_DIR/#\~/$HOME}"
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
if [[ -n "$REPO_DIR" && "$REPO_DIR" != /* && -n "$CWD" ]]; then
REPO_DIR="$CWD/$REPO_DIR"
fi
REPO_DIR="${REPO_DIR:-${CWD:-.}}"

# Determine base branch (default: main) and the PR's head branch (default: the checked-out one). gh uses the last
# value of a repeated flag, and --head may be owner:branch. grep -oE rather than grep -P, which macOS grep lacks.
BASE=$(printf '%s\n' "$FLAGS" | grep -oE -- '(^|[[:space:]])(--base|-B)[[:space:]=]+[^[:space:];&|)]+' | tail -n1 | sed -E 's/^[[:space:]]*(--base|-B)[[:space:]=]+//')
BASE="${BASE:-main}"
HEAD_BRANCH=$(printf '%s\n' "$FLAGS" | grep -oE -- '(^|[[:space:]])(--head|-H)[[:space:]=]+[^[:space:];&|)]+' | tail -n1 | sed -E 's/^[[:space:]]*(--head|-H)[[:space:]=]+//')
HEAD_BRANCH="${HEAD_BRANCH#*:}"
HEAD_BRANCH="${HEAD_BRANCH:-$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null)}"

# A value that looks like an option would be passed to git as one, so skip the check instead
if [[ "$BASE" == -* || "$HEAD_BRANCH" == -* ]]; then
exit 0
fi

# Fetch latest base and check freshness
git fetch origin "$BASE" --quiet 2>/dev/null || true
BEHIND=$(git rev-list --count HEAD.."origin/$BASE" 2>/dev/null || echo "?")
git -C "$REPO_DIR" fetch origin "$BASE" --quiet 2>/dev/null || true
BEHIND=$(git -C "$REPO_DIR" rev-list --count "$HEAD_BRANCH".."origin/$BASE" 2>/dev/null || echo "?")

if [[ "$BEHIND" != "0" && "$BEHIND" != "?" ]]; then
cat >&2 << EOF

===================================================================
PR BLOCKED: Branch is $BEHIND commit(s) behind $BASE
PR BLOCKED: Branch $HEAD_BRANCH is $BEHIND commit(s) behind $BASE
(checked in $REPO_DIR)
===================================================================

Your PR will show "out of date with base branch" on GitHub.
Expand All @@ -45,13 +76,12 @@ EOF
exit 2
fi

# Check we're not on main/master
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
# Check we're not opening a PR from main/master
if [[ "$HEAD_BRANCH" == "main" || "$HEAD_BRANCH" == "master" ]]; then
cat >&2 << EOF

===================================================================
PR BLOCKED: Cannot create PR from $BRANCH
PR BLOCKED: Cannot create PR from $HEAD_BRANCH
===================================================================

Create a feature branch first:
Expand Down
Loading