# A Claude Code review skill for a repo I don't own — self-review, PR body, and diff hunks

> Porting a Claude Code release skill to a repo I cannot merge or tag: what gets cut, why git diff HEAD lets review run before the commit, where ignore rules go.

- Source: https://oharu121.com/blog/claude-code-review-skill-pr-body-diff-hunks/
- Published: 2026-08-22T08:54:53+09:00
- Tags: Claude Code, Developer Tooling, Automation

---
**Key takeaways**

- A release flow and a review flow differ by exactly the steps that need write access to somebody else's repository.
- When you cannot merge, the deliverable stops being a tag and becomes a document the reviewer reads.
- `git diff HEAD` covers staged and unstaged work, so a self-review can run before the commit and its findings fold into that same commit.
- Biome's `check:fix` writes to the working tree, which forces validation ahead of the commit rather than after it.
- Local ignore rules belong in `.git/info/exclude`. `.gitignore` is a tracked file, so editing it in a repo you don't own puts a diff in your own pull request.

## Introduction

I have a `/release` skill on this blog that runs an entire release without me touching the keyboard: it opens a GitHub issue, cuts a branch, validates, reviews the diff, opens a pull request, squash merges it, tags `main`, and publishes the release notes. Last week I went to reach for the same thing on a work monorepo and stopped at the first step. That repository belongs to the organisation, not to me. I do not merge my own work there, I do not cut tags, and the release cadence is somebody else's call.

The useful finding was that **what survives the cut is not a smaller release skill**. It is a different artifact, because the thing it produces stops being a merge and becomes a handoff document that a colleague reads before approving anything. This article walks through which steps came out, what replaced them, and the three design decisions that only became visible once ownership was gone.

## The release skill assumes it owns the repo

My blog's release skill runs seventeen numbered steps across four phases. Written out, most of them turn out to be assertions of ownership rather than engineering:

| Step | What it needs |
| --- | --- |
| Create a GitHub issue, assign a milestone | Issue write access, and a milestone convention that is mine |
| Bump the version in `package.json` | Authority over the version number |
| Promote `[Unreleased]` in `CHANGELOG.md` | Authority over the changelog format |
| Squash merge the pull request | Merge rights on the default branch |
| Tag `main` and push the tag | Write access to refs |
| Create the GitHub release | Release authority |
| Close the issue with a released-in comment | Issue write access again |

**Seven of the seventeen steps are gone before any code is considered.** The remaining ten are the ones that were doing real work: read the current state, validate, review, branch, commit, push, and report.

The tag ordering rule that the release skill spends a whole section defending is a good illustration. It exists because squash merge replays a diff as a new commit on `main` and discards the branch, so a tag created before the merge points at a commit that is not an ancestor of `main` and `git describe` can never see it again. That is a genuinely subtle trap, and **it is completely irrelevant to a repository where I never tag anything.** Most of the release skill's hard-won knowledge is like this: **correct, expensive to learn, and scoped to a repository I control.**

*Figure — ReleaseVsReview: The struck rows are the argument. The left column groups the seventeen release steps into nine rows; the four struck ones need write access that is not mine, and the empty lower half of the right column is what removing them leaves.*

## Cutting the steps that need somebody else's write access

What is left runs in six phases: read the state, self-review the diff, check whether the remote moved, validate, then branch and commit and push, then write the review documents. **The last phase is the one that did not exist in the release skill at all.**

On my own repository the output of a successful run is a tag and a published release. On a team repository **the output is two markdown documents**: a pull request body and a hunk-by-hunk explanation of the diff. I gave the agent two real examples from earlier pull requests and told it to treat them as the format specification rather than inventing a template, because those documents already had a shape that survived review. Five sections, in reading order: the problem with its root cause traced, a verification table of what actually ran, the work organised by change rather than by file, what the change costs, and notes.

**The skill stops after `git push`.** It composes the compare URL from `git remote get-url origin` and hands it over, and it never calls `gh`. I raise the pull request myself, which is not a limitation I worked around but the correct behaviour for a repository where opening a pull request is a social act as much as a technical one.

## "How can you review without commit?"

When the agent offered to run the self-review before the commit, I pushed back. It seemed like a category error: review reads a diff, a diff is what a commit produces, so reviewing before committing sounded like reviewing nothing.

The answer is that **`git diff HEAD` covers staged and unstaged changes together, against the last commit.** There is nothing to wait for. Which command to read depends only on what is in flight:

*Figure — DiffSources: Which command to read depends only on what is in flight. The first row has no commit in it at all, which is why the review can run ahead of one.*

That matters more than it first appears. If the review runs after the commit, every finding it produces has to land as a second commit, and the branch grows an "address review feedback" commit that a human reviewer then has to read and mentally discard. **Running the review first means the fixes are indistinguishable from the original work**, because they are part of it. The reviewer reads one coherent change instead of a change plus its errata.

I was wrong about the mechanism and the objection was still worth making, because it forced the ordering to be justified rather than assumed.

## `check:fix` writes to the tree, so it runs before the commit

The monorepo uses Biome, and the script the repository standardises on is `check:fix`, which is `biome check --write`. **It does not report problems. It rewrites files.**

**That single fact fixes the phase order.** Validation cannot sit after the commit, because it would leave formatting changes stranded in the working tree that then need a second commit to collect. It cannot be swapped for the read-only `check` either, since the repository's own convention is that formatting is applied rather than complained about. So the order is forced: review, apply fixes, validate, commit, push, write the documents.

*Figure — PhaseOrder: Phase 4 is the constraint. Because it writes rather than reports, it cannot sit after phase 5, and the rest of the order follows from that.*

The agent proposed detecting which workspaces a change touched rather than running the full suite every time, and I took it. The mapping is mechanical, from changed path to package filter, with one exception it argued for and which I think is right: **a change under the shared `common` package escalates to validating everything.** The three applications each consume that package's built output, so a modified type compiles cleanly inside `common` and breaks at the consumer's `tsc`. Narrow detection would report green on a change that does not build.

One detail worth stating because it looks alarming the first time: the client and admin builds emit into directories that are already ignored, and the server build does too. **A full validation run leaves `git status` clean.** Build output showing up there would mean something was wrong with a path, not that the build succeeded loudly.

## The ignore rules go in `.git/info/exclude`, not `.gitignore`

The two generated documents have to live somewhere, and the obvious answer is a scratch directory in the repository with an entry in `.gitignore`. That answer is wrong for the same reason the release steps were wrong.

**`.gitignore` is a tracked file.** Adding a personal scratch directory to it means a diff in my pull request that reviewers have to read, a line committed to a repository I do not own, and my local convention imposed on everyone who clones it. None of that is what I wanted. I wanted a place to put a draft.

**`.git/info/exclude` has the same syntax and the same effect, and it is per clone and untracked:**

```bash
echo '.ignore/' >> .git/info/exclude
```

Verifying it works the same way as any other ignore rule, and `git check-ignore` will tell you which file supplied the rule:

```text
$ git check-ignore -v .ignore/pr-body.md
.git/info/exclude:8:.ignore/	.ignore/pr-body.md
```

The same file ended up holding the skill as well. **`.claude/` is not a personal directory in this repository** — it already tracks shared agents and slash commands, so a new folder under it reads as something the team is meant to pick up. A workflow skill encoding one person's preferences is not that, at least not before anyone has asked for it:

```text
$ tail -3 .git/info/exclude
scripts/
.ignore/
.claude/skills/review
```

Three entries, and not one of them a line anybody else has to review.

**The trade is that the rule does not travel.** Nobody else's clone has it, and neither will mine after a fresh clone. For a scratch directory holding drafts that are meant to be pasted into a web form and then forgotten, that is the correct trade, and it is the same judgement as not tagging: when the repository is not mine, the default is to leave no trace in it.

## A checklist that names the codebase finds what a generic one does not

**A review agent given "look for bugs" produces a list of things that are not bugs.** The agent derived the checklist from the repository itself, and the dimensions that earn their place are the ones that could not have been guessed from outside.

### The block that leaves no trace

The strongest example is a content-block reducer in the server. It walks a list of blocks and dispatches each one through a callback map in which **every handler except the text handler is optional**, with each branch guarded as "this block type, and a callback exists for it".

*Figure — SilentDrop: There is no else branch. An absent callback makes the guard falsy, so the block is skipped by the same code path that skips nothing at all.*

A provider integration that never defined a handler for some block type therefore drops that block silently. **Nothing throws. Nothing logs.** The content simply does not reach the model, and the failure surfaces later as a malformed conversation or an API validation error with no obvious cause.

That exact shape has already produced shipped bugs twice in this codebase, so a new block type arriving without a corresponding handler in every provider is worth a high-severity finding. **No general-purpose reviewer would ever look for it**, because there is nothing in the code that looks like a mistake.

### Which file registers a route decides who can reach it

The backend registers routes under prefixes, and only two of them carry authentication middleware. A handler that reaches object storage or the database is safe under those two prefixes and publicly reachable anywhere else, which makes **"which file registers this route" a security question rather than an organisational one.**

The repository's own routing entry point already carries a comment saying exactly that. Encoding an existing comment as a review dimension costs nothing and catches the one mistake that would actually matter.

### What the checklist refuses to report

Just as useful is the list of findings the checklist rules out, because each one is already guaranteed by something downstream:

| Not reported | Already handled by |
| --- | --- |
| Formatting, import order, quote style | `check:fix`, which rewrites them two phases later |
| Unused imports, `let` that should be `const` | Biome's lint rules, same phase |
| Type errors | the `tsc` step inside every workspace build |

A review that reports these is spending the reader's attention on findings that were never going to reach the pull request, and burying the ones that need human judgement underneath them.

## One language setting turned out to be two

I asked for all the generated content in the language my colleagues read. The agent checked the repository's history before agreeing and came back with a conflict: **every commit in the log is English, in Conventional Commits form, including everyone else's.** Commit messages in another language would have been the only ones of their kind.

So the split is by audience rather than by preference. The review documents follow the reviewer, because a person reads them once and decides something. The commit messages stay English, because they join a shared log that outlives the review and belongs to everyone. **I had been treating "language" as a single setting when it is two**, and the agent noticed because it read the log rather than my instruction.

This is the part of the exercise I would not have got to alone, and it is worth being precise about who did what: the scope, the format specification, and every final call were mine, and the conflict I did not see was the agent's to find.

## The template that could not contain its own examples

One failure is worth recording because the fix is not the obvious one.

The two document templates contain worked examples, and those examples are themselves fenced code blocks. A ` ```markdown ` block containing a ` ```diff ` block terminates at the inner fence. The agent's first attempt was to prefix each inner fence with a zero-width space, which renders invisibly and stops the parser from matching. It works, and it leaves invisible characters scattered through a file that other people will read.

Removing them again did not go smoothly:

```bash
perl -i -pe 's/\x{200b}//g' templates/diff-explanation.md
```

**That command exits zero and changes nothing.** Without `-CSD`, Perl reads the file as bytes rather than as UTF-8, so `\x{200b}` never matches the three-byte sequence actually in the file. The tell was that `grep -c` still counted four occurrences after a substitution that had reported no error at all:

```text
remaining: 4
```

Two fixes, both small. `perl -CSD` makes the substitution see characters instead of bytes. And the outer fence became `~~~`, which markdown accepts as an alternative fence marker, so the inner backtick fences need no escaping and the file contains no invisible characters at all. **The second fix means the first was never needed**, which is the usual shape of this kind of problem.

## Summary

Porting a release skill to a repository I do not own turned out to be a subtraction exercise with one addition. Seven of seventeen steps came out because they asserted ownership: issues, milestones, version numbers, changelog authority, merges, tags, releases. What replaced them is a document-writing phase, because a colleague reading a pull request body is now the last step instead of a green check on a merge.

Three decisions only became visible once ownership was gone. Review runs before the commit, because `git diff HEAD` makes it possible and folding fixes into the original commit gives the reviewer one change to read. Validation runs before the commit too, because the repository's formatter writes rather than reports. And the scratch directory is ignored through `.git/info/exclude` rather than `.gitignore`, because a tracked ignore file is one more thing I would be changing in somebody else's repository.

The general form, if there is one: **a workflow skill encodes a set of permissions as much as a set of steps**, and the steps that survive a move are the ones that were never about permission in the first place.

## References

- [gitignore documentation, including the per-clone `$GIT_DIR/info/exclude` file and its precedence order](https://git-scm.com/docs/gitignore)
- [Claude Code Agent Skills, covering SKILL.md frontmatter and bundled resources](https://docs.claude.com/en/docs/claude-code/skills)
- [Biome's `check` command reference, where `--write` is documented as applying safe fixes](https://biomejs.dev/reference/cli/#biome-check)
- [Conventional Commits 1.0.0, the format this repository's log follows](https://www.conventionalcommits.org/en/v1.0.0/)
- [perlrun on the `-C` switch, which controls whether Perl treats input as UTF-8](https://perldoc.perl.org/perlrun#-C-[number/list])
