Skip to content

Instantly share code, notes, and snippets.

@xadn
Created February 25, 2026 07:59
Show Gist options
  • Select an option

  • Save xadn/4a62b18cecba6f37c2234b8716c4b953 to your computer and use it in GitHub Desktop.

Select an option

Save xadn/4a62b18cecba6f37c2234b8716c4b953 to your computer and use it in GitHub Desktop.
Claude Code hook: force read-before-edit on PRs

Force Claude Code to read before editing PRs

Claude Code likes to gh pr edit PRs without reading the current state first — overwriting descriptions, losing context, or making changes based on stale assumptions. This hook pair forces a "read before write" pattern: any gh pr edit is blocked unless gh pr view was called first for that PR in the same session.

How it works

Two hooks — a PostToolUse hook records when Claude views a PR, and a PreToolUse hook blocks edits unless a view was recorded. The view is consumed after each edit, so consecutive edits each require a fresh read.

gh pr view 123 ...   →  records "123" to a temp file
gh pr edit 123 ...   →  checks file, allows, then removes "123"
gh pr edit 123 ...   →  blocked — must view again first

State is per-session (temp file keyed by $SESSION_ID), so it resets naturally.

Setup

1. Create the hooks

~/.claude/hooks/check-pr-viewed.sh (PreToolUse — blocks edits)

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')

# Only care about gh pr edit commands
if echo "$COMMAND" | grep -qE '\bgh\s+pr\s+edit\b'; then
  PR_NUM=$(echo "$COMMAND" | grep -oE '\bedit\s+([0-9]+)' | grep -oE '[0-9]+')
  STATE_FILE="/tmp/claude-pr-viewed-${SESSION_ID}.txt"

  if [ -n "$PR_NUM" ]; then
    if ! grep -qx "$PR_NUM" "$STATE_FILE" 2>/dev/null; then
      echo "Blocked: run 'gh pr view $PR_NUM --json body -q .body' first to read the current PR state before editing." >&2
      exit 2
    fi
    # Consume the view — next edit requires a fresh view
    sed -i '' "/^${PR_NUM}$/d" "$STATE_FILE"
  fi
fi
exit 0

~/.claude/hooks/record-pr-view.sh (PostToolUse — records views)

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')

# Only care about gh pr view commands
if echo "$COMMAND" | grep -qE '\bgh\s+pr\s+view\b'; then
  PR_NUM=$(echo "$COMMAND" | grep -oE '\bview\s+([0-9]+)' | grep -oE '[0-9]+')
  if [ -n "$PR_NUM" ]; then
    echo "$PR_NUM" >> "/tmp/claude-pr-viewed-${SESSION_ID}.txt"
  fi
fi
exit 0

Make both executable:

chmod +x ~/.claude/hooks/check-pr-viewed.sh ~/.claude/hooks/record-pr-view.sh

2. Add to settings

In ~/.claude/settings.json, add to the hooks object:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/check-pr-viewed.sh",
            "timeout": 5,
            "statusMessage": "Checking PR view status..."
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/record-pr-view.sh",
            "timeout": 5,
            "statusMessage": "Tracking PR views..."
          }
        ]
      }
    ]
  }
}

3. Note on sed -i

The sed -i '' syntax is macOS. On Linux, use sed -i (no empty string argument).

What Claude sees when blocked

PreToolUse:Bash hook error: [~/.claude/hooks/check-pr-viewed.sh]:
Blocked: run 'gh pr view 123 --json body -q .body' first to read the current PR state before editing.

Claude then reads the PR and retries — exactly the behavior you want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment