# Replying To Pr Review Threads

> Use when a `discussion_rNNN` URL anchor appears, when replying to a specific PR review comment from a bot or AI reviewer (CodeRabbit, cubic, Claude, Copilot) or a human reviewer, reading all reviews on a PR with their replies as a tree, or resolving review threads from the CLI. Also covers where AI reviewers put out-of-diff findings and the mandatory `@handle` tag when replying. Triggers on "reply to coderabbit", "reply to cubic", "reply to the bot", "answer the bot review", "address bot reviews", "answer AI review comments", "answer this review comment", "read all reviews on PR", "show review threads", "resolve this thread".

- Skill: `carlos-algms/replying-to-pr-review-threads` (Agent Skill)
- Install (CLI): `npx skillmds@latest add carlos-algms/replying-to-pr-review-threads`
- Raw SKILL.md: https://api.skillmd.com/api/skills/carlos-algms/replying-to-pr-review-threads/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: carlos-algms (https://skillmd.com/u/carlos-algms)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/carlos-algms/replying-to-pr-review-threads

---


# PR review threads

A PR has three independent comment surfaces. Cover all three to "read all
reviews".

| Stream                | What it is                                         | How to read               |
| --------------------- | -------------------------------------------------- | ------------------------- |
| PR conversation       | Top-level issue comments                           | `gh pr view N --comments` |
| Review summaries      | Body of each "Approve / Request changes / Comment" | `gh pr view N --comments` |
| Inline review threads | Diff comments + replies, grouped by file/line      | GraphQL `reviewThreads`   |

`gh pr comment` writes only to stream 1. There is no native `gh` subcommand for
reading nested inline threads or replying to one (cli/cli#12273). Use `gh api`.

URL anchor: `pull/N#discussion_rNNN` -> `NNN` is the comment's REST `id` and
GraphQL `databaseId`.

## Reading all reviews on a PR

```bash
# Streams 1 + 2
gh pr view N --comments

# Stream 3: inline threads as a tree
gh api graphql -f query='
  query($owner:String!,$repo:String!,$num:Int!,$cursor:String){
    repository(owner:$owner,name:$repo){
      pullRequest(number:$num){
        reviewThreads(first:100,after:$cursor){
          pageInfo{ hasNextPage endCursor }
          nodes{
            id isResolved isOutdated path line
            comments(first:100){
              pageInfo{ hasNextPage endCursor }
              nodes{ databaseId author{ login } createdAt body url }
            }
          }
        }
      }
    }
  }' -f owner=OWNER -f repo=REPO -F num=N
```

`-F num=N` (typed Int), not `-f`.

Repeat with `-f cursor=END_CURSOR` until `reviewThreads.pageInfo.hasNextPage` is
false. For any thread whose `comments.pageInfo.hasNextPage` is true, paginate
its comments separately:

```bash
gh api graphql -f query='
  query($thread:ID!,$cursor:String){
    node(id:$thread){
      ... on PullRequestReviewThread{
        comments(first:100,after:$cursor){
          pageInfo{ hasNextPage endCursor }
          nodes{ databaseId author{ login } createdAt body url }
        }
      }
    }
  }' -f thread=PRRT_xxxx -f cursor=END_CURSOR
```

Repeat until `comments.pageInfo.hasNextPage` is false. Omit `cursor` on each
first request.

Shape:

- Each `reviewThreads.nodes[]` is one diff-anchored thread.
- `comments.nodes[0]` is the original; `[1..]` are replies in order.
- `id` (`PRRT_*`) is the GraphQL thread ID, needed for resolve.
- `databaseId` is the REST id (the `NNN` in `discussion_rNNN`), needed to reply
  or to find a thread by URL.

Filter examples (`--jq`):

```bash
# Only unresolved threads
... --jq '.data.repository.pullRequest.reviewThreads.nodes
  | map(select(.isResolved|not))'

# Only CodeRabbit threads
... --jq '.data.repository.pullRequest.reviewThreads.nodes
  | map(select(.comments.nodes[0].author.login == "coderabbitai"))'

# Find the GraphQL thread.id given a discussion_rNNN
... --jq --argjson id NNN '.data.repository.pullRequest.reviewThreads.nodes[]
  | select(.comments.nodes[].databaseId == $id) | .id'
```

## Replying to a thread

```bash
gh api repos/OWNER/REPO/pulls/comments/NNN \
  --jq '{id, path, body: (.body|.[0:200])}'

gh api repos/OWNER/REPO/pulls/N/comments/NNN/replies \
  -X POST \
  -f body="$(cat <<'EOF'
Reply body. Markdown OK.
EOF
)" --jq '.html_url'
```

### Replying to an AI reviewer (CodeRabbit, cubic, Claude, etc.)

AI reviewers persist your reply as a learning for future reviews (CodeRabbit
shows an "✏️ Learnings added" block confirming it). Write the reply so the
stored learning is correct AND generalizable.

#### Decide whether to reply

Do not reply to every finding.

- Applied inline finding: do not reply or resolve it. The reviewer can inspect
  newer commits and resolve the thread automatically
- Out-of-diff finding: reply in a top-level comment and tag the reviewer
- Rejected finding: reply in its thread and tag the reviewer. Reject only when
  the claim is invalid or the fix is not worth its cost

Do not post acknowledgements, "fixed" replies, commit references, or summaries
of applied findings.

#### Always tag the bot

Start every reply to an AI reviewer with its handle, on both surfaces (inline
thread reply and top-level comment). The tag is the bot's GitHub login, prefixed
`@`.

| Reviewer   | Handle          |
| ---------- | --------------- |
| CodeRabbit | `@coderabbitai` |
| cubic      | `@cubic-dev-ai` |
| Claude     | `@claude`       |
| Copilot    | `@copilot`      |

Unknown bot: read its login from the thread (`comments.nodes[0].author.login`)
and tag that.

A tag in a thread reply is harmless; a missing tag on a top-level comment means
the reply silently goes nowhere. Tag always - do not reason about which surface
needs it.

#### Reply style

- Terse, low word count. Facts + rationale only. No pleasantries, no closing, no
  restating the finding.
- Lead with the rejection ("Skipping. YAGNI." / "Wrong - X is never null
  here.").
- One or two lines is the target. Prose paragraphs are wrong.
- Give the verifiable evidence: command run, files inspected, why the rule
  applies or doesn't.
- Phrase the rationale in terms that generalize beyond this file/line. "For sync
  helpers in this codebase, ..." beats "for `with_modifiable`, ...". The
  reviewer copies your wording into the learning - narrow phrasing produces a
  one-off rule.
- If the AI reviewer should change behavior next time, say so directly. ("Grep
  callers before flagging defensive guards on sync helpers.")
- Do not include code blocks or diffs unless correcting the suggestion. The
  thread already has the suggested code.

Example:

```text
@coderabbitai Skipping. All 4 callers of `parse_config` already validate the
path upstream (`rg 'parse_config\(' src/`). Defensive guards on internal sync
helpers in this codebase are dead code.
```

## Out-of-diff findings live in the review body

CodeRabbit anchors what it can to a diff line (stream 3) and puts the rest in
the review summary body (stream 2, `gh pr view N --comments`):

- "Outside diff range" comments - findings on code the diff did not touch
- Nitpicks and duplicate findings, inside collapsed `<details>` blocks
- The actionable-comments count, which covers streams 2 AND 3

Reading only `reviewThreads` therefore misses real findings, and the ones it
misses skew severe: a finding that could not anchor to a changed line is often
one about code the change broke elsewhere.

- Read stream 2 to its last line. Collapsed `<details>` content is in the body
  text `gh` returns; do not stop at the first visible section.
- Reconcile the count: the body states N actionable comments. Fewer found across
  both streams means the read was incomplete, not that the rest do not exist.
- An out-of-diff finding has no thread, so it has no `databaseId` and cannot be
  replied to or resolved as a thread. Answer it in a top-level comment, tagged
  with the bot's handle.

## Resolving a thread

REST has no resolve endpoint. Mutation needs the GraphQL thread `id` (`PRRT_*`),
not the REST comment id - look it up via the `reviewThreads` query above.

Do not manually resolve AI-review threads after applying a finding. Use this
mutation only when the user explicitly requests manual resolution.

```bash
gh api graphql -f query='
  mutation($id:ID!){
    resolveReviewThread(input:{threadId:$id}){ thread{ isResolved } }
  }
' -f id=PRRT_xxxx
```

## Common mistakes

| Mistake                                                       | Symptom                                  | Fix                                              |
| ------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------ |
| Used `repos/.../pulls/{n}/comments/{id}` to fetch one comment | 404 Not Found                            | Drop the `/{n}`: `repos/.../pulls/comments/{id}` |
| Used `POST /pulls/{n}/comments` with `in_reply_to`            | 422 "in_reply_to is not a permitted key" | Use `/pulls/{n}/comments/{id}/replies`           |
| Passed numeric ID with `-f`                                   | 422 "not a number"                       | Use `-F`                                         |
| Resolving with the REST comment id                            | GraphQL: invalid ID                      | Look up `thread.id` (`PRRT_*`) via reviewThreads |

