You are performing a deep code review on a GitLab merge request. Follow a strict two-phase workflow: first analyze and present findings locally, then post only user-approved comments to GitLab.
Large MRs are reviewed in chunks for depth. Every finding includes a fix prompt for Claude Code.
GitLab access — embedded CLI
All GitLab interaction goes through the bundled CLI scripts/gitlab.mjs that ships inside this skill's own directory. Invoke it via Bash as node <skill-dir>/scripts/gitlab.mjs <command> [flags], where <skill-dir> is wherever this skill is installed:
- Project-level install (run from the repo root):
node .claude/skills/review-gitlab/scripts/gitlab.mjs … - User-level install:
node ~/.claude/skills/review-gitlab/scripts/gitlab.mjs …
Below, gitlab.mjs <command> is shorthand for that full node <skill-dir>/scripts/gitlab.mjs <command> invocation. The CLI reads MR_MCP_GITLAB_TOKEN (and optional MR_MCP_GITLAB_HOST, default https://gitlab.com) from the environment. Each command prints JSON to stdout; on error it prints {"error": "..."} to stderr and exits non-zero.
There is no GitLab MCP server. Do not call any mcp__gitlab-mr__* or mcp__gitlab-mr-edit__* tool — use the CLI commands below instead.
| Old MCP tool | CLI command | Required flags |
|---|---|---|
get_projects |
get_projects |
— (optional --verbose) |
list_open_merge_requests |
list_open_mrs |
--project-id N |
get_merge_request_details |
get_mr |
--project-id N --mr IID |
get_merge_request_diff |
get_mr_diff |
--project-id N --mr IID |
get_merge_request_comments |
get_mr_comments |
--project-id N --mr IID |
add_merge_request_comment |
add_mr_comment |
--project-id N --mr IID --body-file F |
add_merge_request_diff_comment |
add_diff_comment |
--project-id N --mr IID --base-sha S --start-sha S --head-sha S --file P --line L --body-file F |
get_issue_details |
get_issue |
--project-id N --issue IID |
set_merge_request_description |
set_mr_description |
--project-id N --mr IID --body-file F |
set_merge_request_title |
set_mr_title |
--project-id N --mr IID --title "…" |
create_merge_request_comment (Notes API, plain note) |
create_note |
--project-id N --mr IID --body-file F |
edit_merge_request_comment |
edit_note |
--project-id N --mr IID --note-id ID --body-file F |
delete_merge_request_comment |
delete_note |
--project-id N --mr IID --note-id ID |
Passing comment bodies: comment/scorecard/description text is large markdown (backticks, newlines, code fences) and is fragile on the command line. For every write command, Write the body to a temp file first (e.g. /tmp/claude-review-<short>.md) and pass it with --body-file <path>. A short --body "text" flag exists for trivial one-liners, and --title-file mirrors --body-file for set_mr_title. Add --verbose to any read command to get the full unfiltered GitLab payload.
Input
The user provided: $ARGUMENTS
This can be one of:
- A full GitLab MR URL:
https://gitlab.com/group/project/-/merge_requests/42 - A path with MR IID:
group/project !42 - Just an MR IID if the project is obvious from context:
!42
Phase 0 — Setup
Step 1: Parse the input
Extract the project path (e.g. group/project) and MR IID (e.g. 42) from the input.
If given a full URL, parse it:
https://gitlab.com/group/subgroup/project/-/merge_requests/42→ project path =group/subgroup/project, IID =42
Step 2: Find the project ID
Run gitlab.mjs get_projects to list projects. Match the project path (path_with_namespace) to find the numeric id (the --project-id).
If $ARGUMENTS contains only an MR IID (like !42), ask the user for the project path.
Step 3: Fetch MR data (3 parallel calls)
Run these in parallel (independent Bash calls in one message):
gitlab.mjs get_mr --project-id N --mr IID— metadata +diff_refs(containingbase_sha,start_sha,head_shaneeded for Phase 2)gitlab.mjs get_mr_diff --project-id N --mr IID— the full code diffgitlab.mjs get_mr_comments --project-id N --mr IID— existing review comments (for deduplication)
Phase 1 — Analyze (READ-ONLY, no GitLab writes)
CRITICAL: Do NOT run any write command during Phase 1. No add_mr_comment, no create_note, no add_diff_comment, no edit_note, no delete_note, no set_mr_*. Phase 1 is strictly read-only.
Step 4: Triage & chunk plan
Count total changed files and estimate total diff character length. Classify the MR:
| Size | Threshold | Strategy |
|---|---|---|
| Small | <15 files OR <50K chars | Single-pass — review all diffs together |
| Medium | 15-50 files OR 50K-150K chars | 3-5 chunks |
| Large | 50+ files OR 150K+ chars | 5-10 chunks, ~15-25 files each |
Grouping heuristic (apply in priority order):
- Feature cohorts — files that work together (e.g. a block's
block.ts+Component.tsx+index.ts) - Directory —
payload/blocks/**,components/**,app/[locale]/**,packages/** - Config/infra —
package.json, CI configs, migrations, docker files → one chunk - Generated/lock files —
payload-types.ts,pnpm-lock.yaml,*.gen.ts→ skip entirely (note in Skipped Files section)
Print the chunk plan before starting reviews:
Review plan: [Single-pass | N chunks]
- Chunk 1: [Label] ([N] files) — file1.ts, file2.ts, ...
- Chunk 2: [Label] ([N] files) — ...
- Skipped: [N] files (generated/lock)
Step 5: Index existing comments
From the get_mr_comments results, build a mental index of all existing review comments:
note_id(theidfield of each note — needed for edit/delete operations)- File path + line number (for diff comments)
- Finding substance / what it's about
- Whether resolved or still open
- Author name
Rules during review:
- If a finding is already raised by an existing comment → skip it and list in "Previously Raised" section
- If an existing comment points out an issue that appears to be fixed in the current diff → note it as "appears fixed"
- Also check for a comment containing
<!-- CLAUDE_REVIEW_SCORECARD_v1 -->— if found, note itsnote_idand date for the scorecard update strategy
Augment previous Claude Code review comments that lack fix prompts:
- Look for diff comments containing the
<!-- CLAUDE_REVIEW_FINDING -->marker (posted by a previous run of this skill) that do not already contain a💡 **Fix prompt:**block - If the issue is still relevant (not fixed in the current diff), generate a fix prompt for it
- List these in a dedicated "Augmented Comments" section in the draft
- These will be edited into the original comment (appending the fix prompt block) using the
note_id - Note: comments posted before this marker was introduced won't be detected — that's expected
Step 6: Chunked deep review
For each chunk (or single pass for small MRs):
- Print progress header:
"Reviewing chunk 2/5: Design system components..." - Extract that chunk's diffs from the diff data (filter by file paths belonging to this chunk)
- For each non-trivial changed file: use the Read tool to read the full local file for broader context around the changes
- Analyze diff + local context for:
- Bugs: Logic errors, off-by-one, null/undefined risks, race conditions
- Security: Injection vulnerabilities, auth issues, data exposure, OWASP top 10
- Performance: N+1 queries, unnecessary re-renders, missing memoization, large payloads
- Style: Violations of project conventions (see CLAUDE.md/AGENTS.md — no native HTML tags, no inline styles, PandaCSS only, strict imports, etc.)
- Logic: Incorrect business logic, missing edge cases, incomplete error handling
- Types: TypeScript type safety issues, overly broad types, missing type guards
- Record findings with: file, line, severity, category, description, fix prompt
- Do NOT present findings between chunks — accumulate all findings, present once at the end
Step 7: Calculate confidence score
Start at 7/10, then adjust across five dimensions:
1. Coverage
| Adj | Condition |
|---|---|
| +1 | All changed files read with full local context |
| 0 | Most files read, a few skipped (generated/lock) |
| -1 | Chunked review with context gaps between chunks |
| -2 | Many files skipped or only diffs read (no local files) |
2. Code complexity
| Adj | Condition |
|---|---|
| +1 | Straightforward — declarative, config, UI components, translations |
| 0 | Standard — clear control flow, well-structured feature work |
| -1 | Non-trivial — algorithms, state machines, complex data transformations |
| -2 | Highly complex — dynamic code gen, deeply nested async, intricate caching |
3. Security findings
| Adj | Condition |
|---|---|
| +1 | Security-sensitive areas reviewed, no issues found |
| 0 | No security-sensitive code in the MR |
| -1 | Minor security concerns found |
| -2 | Critical security issues found |
4. Test coverage signal
| Adj | Condition |
|---|---|
| +1 | MR includes tests covering the new/changed logic |
| 0 | Existing tests likely cover the changes |
| -1 | No tests and changes affect business logic or data flow |
5. Domain familiarity
| Adj | Condition |
|---|---|
| +1 | Well-documented areas with clear patterns (e.g. Payload blocks) |
| 0 | Standard project areas |
| -1 | Unfamiliar domain, third-party integrations, undocumented APIs |
Final score = clamp(7 + sum of adjustments, 1, 10)
Present the breakdown as the criteria table shown in the scorecard format below (one row per dimension with points and explanation).
Step 8: Present the draft review
Merge all chunk findings, deduplicate, sort by severity (critical first), and present the full draft.
Severity emojis:
- 🔴 Critical — bugs, security vulnerabilities, data loss risks
- 🟠 Warning — performance issues, potential bugs, logic concerns
- 🟡 Suggestion — style issues, minor improvements, best practices
- 🟢 Nit — cosmetic, optional, purely stylistic
Use this exact format:
## MR Review Draft: group/project !IID
**Title:** [MR title]
**Author:** [MR author]
**Branch:** [source_branch] → [target_branch]
**Review method:** [Single-pass | Chunked (N chunks)]
**Files reviewed:** X/Y (Z skipped)
---
### Review Scorecard (will be posted as MR comment)
<!-- CLAUDE_REVIEW_SCORECARD_v1 -->
## 🔍 Review Scorecard
# Confidence Score: [N]/10
| Criteria | Points | Explanation |
|----------|--------|-------------|
| Base | 7 | — |
| Read coverage | [+1/0/-1/-2] | [One-line reason] |
| Code complexity | [+1/0/-1/-2] | [One-line reason] |
| Security | [+1/0/-1/-2] | [One-line reason] |
| Test coverage | [+1/0/-1/-2] | [One-line reason] |
| Familiarity | [+1/0/-1] | [One-line reason] |
**Reviewed:** [X]/[Y] files ([Z] skipped: [reason])
**Method:** [Single-pass review | Chunked deep review (N chunks)]
### Summary
[2-4 sentence assessment. Is it ready to merge? What are the main themes?]
### Findings Breakdown
| Severity | Count | Key Areas |
|----------|-------|-----------|
| Critical | N | [areas or —] |
| Warning | N | [areas or —] |
| Suggestion | N | [areas or —] |
| Nit | N | [areas or —] |
### Verdict
[APPROVE / APPROVE WITH SUGGESTIONS / REQUEST CHANGES / NEEDS DISCUSSION]
[One sentence explaining the verdict]
---
*Automated review by Claude Code — [N] inline comments posted, [M] existing comments edited with fix prompts*
---
### Previously Raised (will NOT be re-posted)
- `src/foo.tsx:12` — Missing null check (by @author, still open)
- `src/bar.tsx:45` — Unused import (by @author, appears fixed)
*[Or: "No existing review comments found."]*
---
### Augmented Comments (fix prompts for previous Claude Code review comments)
These will be edited into previous `/review-gitlab` comments that lacked a fix prompt:
A1. ✏️ Edit previous review on `[file_path]:[line_number]` (note_id: [id]) — "[original comment summary]"
💡 **Fix prompt:** "[Imperative fix description referencing exact file paths,
line numbers, current code, and expected replacement.]"
A2. ✏️ Edit previous review on `[file_path]:[line_number]` (note_id: [id]) — "[original comment summary]"
💡 **Fix prompt:** "[...]"
*[Or: "No existing comments need augmentation."]*
---
### New Findings
1. 🔴 **Bug** — `[file_path]:[line_number]`
> [Clear description of the issue]
💡 **Fix prompt:** "[Imperative description of what to change, referencing exact
file paths, line numbers, current code, and expected replacement. Specific enough
to execute without further searching.]"
2. 🟠 **Performance** — `[file_path]:[line_number]`
> [Clear description of the issue]
💡 **Fix prompt:** "[...]"
3. 🟡 **Convention** — `[file_path]:[line_number]`
> [Clear description of the issue]
💡 **Fix prompt:** "[...]"
[...more findings sorted by severity...]
---
### Skipped Files
- `apps/nextjs/payload-types.ts` — Generated
- `pnpm-lock.yaml` — Lock file
[Or: "No files skipped."]
Then ask:
**What would you like to post to GitLab?**
- `all` — scorecard + all findings + all augmented comments
- `none` — discard everything
- `scorecard only` — just the scorecard comment
- `1, 3, 5` — scorecard + these numbered findings only
- `all except 2` — scorecard + all findings except these
- `augments only` — scorecard + augmented comment edits only (no new findings)
- `A1, A2` — specific augmented comments only
- `edit 3` — edit an existing posted finding's comment (by note_id from Step 5)
- `delete 3` — delete an existing posted finding's comment (by note_id from Step 5)
- `dry-run <selection>` — preview what would be posted without posting (e.g. `dry-run all`, `dry-run 1, 3`)
- Or edit any finding/prompt before posting
If there are no issues found, still present the scorecard with a positive assessment and ask if the user wants to post it.
Phase 2 — Post (only after user approval)
Wait for the user to respond with their selection. Then:
Step 9: Post approved comments
Dry-run mode: If the user's selection starts with dry-run, do NOT run any write command. Instead, parse the selection after the dry-run prefix (e.g. dry-run all → treat as all, dry-run 1, 3 → treat as 1, 3) and for each action that would be taken, print a summary block:
## Dry Run — Actions Preview
The following actions would be taken (nothing was posted):
1. 📝 **Create scorecard comment** (plain note)
- Command: `gitlab.mjs create_note`
- Body preview: [first 200 chars of scorecard]...
2. 💬 **Post diff comment** on `src/foo.tsx:42`
- Command: `gitlab.mjs add_diff_comment`
- Severity: 🔴 Critical — [category]
- Body preview: [first 150 chars]...
3. ✏️ **Edit existing comment** (note_id: 12345) — append fix prompt
- Command: `gitlab.mjs edit_note`
- Appending: 💡 Fix prompt for [description]
4. ✏️ **Update existing scorecard** (note_id: 67890)
- Command: `gitlab.mjs edit_note`
- Body preview: [first 200 chars]...
---
Total: N new comments, M edits, D deletes
Ready to post? Reply with your selection (without `dry-run`) to execute.
Then stop and wait for the user's next response. Do NOT proceed to post.
Normal posting mode: Based on the user's selection. For every write, Write the comment body to a temp file (e.g. /tmp/claude-review-<n>.md) and pass --body-file:
Scorecard comment: Post the scorecard (everything between
<!-- CLAUDE_REVIEW_SCORECARD_v1 -->and the closing---) as a general MR comment.Scorecard update strategy:
- If an existing comment with
<!-- CLAUDE_REVIEW_SCORECARD_v1 -->was found in Step 5, edit it in place:gitlab.mjs edit_note --project-id N --mr IID --note-id <recorded id> --body-file <file>. Append\n\n> ℹ️ Updated from previous scorecardat the end of the new scorecard body. - If no existing scorecard found, post a new one:
gitlab.mjs create_note --project-id N --mr IID --body-file <file>(Notes API — creates a plain comment, not a resolvable thread).
- If an existing comment with
Line-level findings: For each approved finding, post it as a diff comment:
gitlab.mjs add_diff_comment --project-id N --mr IID --base-sha <base> --start-sha <start> --head-sha <head> --file <path> --line <line> --body-file <file>where:--base-sha/--start-sha/--head-sha: fromdiff_refs(fetched in Step 3)--file: the file path as shown in the diff--line: the line number (maps tonew_linein the GitLab API — it targets the new version of the file)- body file content:
<!-- CLAUDE_REVIEW_FINDING -->\n**[Severity emoji] [Category]**: [description]\n\n💡 **Fix prompt:**\n\``\n[fix prompt text]\n````
Run the diff-comment commands in parallel where possible (independent Bash calls in one message).
Augmented comments: For each approved augmented comment (A1, A2, etc.), edit the original comment in place:
gitlab.mjs edit_note --project-id N --mr IID --note-id <original id> --body-file <file>. Set the body to the original comment body with the fix prompt block appended:- Append:
\n\n💡 **Fix prompt:**\n\``\n[fix prompt text]\n```\n\n*— Fix prompt added by Claude Code review*`
Edit these in parallel where possible.
- Append:
If the user edited any comment text, use the edited version when posting.
Edit/delete existing comments: If the user chose
edit Nordelete N:edit N— Ask the user for the new text (or let them provide it inline), then rungitlab.mjs edit_note --project-id N --mr IID --note-id <id of finding #N> --body-file <file>.delete N— Confirm with the user, then rungitlab.mjs delete_note --project-id N --mr IID --note-id <id of finding #N>.
Step 10: Report results
After posting, report:
## Review Posted ✓
Posted to [MR URL]:
- Scorecard comment [posted | edited in place] (confidence: N/10, verdict: [VERDICT])
- [N] inline comments on [files list]
- [M] existing comments edited with fix prompts
- [D] comments deleted (if any)
[Any errors that occurred during posting]
If any comment fails to post (e.g. invalid line number for a deleted file), report the error and continue posting the rest.
Fix Prompt Guidelines
When writing fix prompts for findings, follow these rules:
- Reference exact file paths and line numbers from the diff
- Describe the current code and the expected replacement clearly
- Group related fixes across files into a single prompt when they share the same root cause
- Use imperative voice: "Replace...", "Remove...", "Add...", "Refactor..."
- Be specific enough to execute without further codebase searching
- Optimize for Claude Opus/Sonnet — explicit, no ambiguity, no pronouns for code references
- Include the import path if a new import is needed