# 為不屬於我的儲存庫寫一個 Claude Code 審查 skill — 自我審查、PR 內文與差異 hunk

> 把 Claude Code 的發布 skill 移植到一個我無法合併也無法打標籤的儲存庫：什麼被砍掉、為什麼 git diff HEAD 讓審查能在提交前跑、忽略規則該放在哪裡。

- Source: https://oharu121.com/zh-tw/blog/claude-code-review-skill-pr-body-diff-hunks/
- Published: 2026-08-22T08:54:53+09:00
- Tags: Claude Code, 開發工具, 自動化

---
**重點摘要**

- 發布流程與審查流程的差別，剛好就是那些需要別人儲存庫寫入權限的步驟。
- 當你不能合併時，交付物就不再是一個標籤，而是一份審查者會讀的文件。
- `git diff HEAD` 同時涵蓋已暫存與未暫存的變更，所以自我審查可以在提交前執行，它找到的問題也能折進同一次提交裡。
- Biome 的 `check:fix` 會寫入工作目錄，這迫使驗證必須排在提交之前而不是之後。
- 本機的忽略規則屬於 `.git/info/exclude`。`.gitignore` 是被追蹤的檔案，在一個不屬於你的儲存庫裡改它，等於在自己的 pull request 裡塞進一段差異。

## 引言

我在這個部落格上有一個 `/release` skill，它能在我不碰鍵盤的情況下跑完整套發布：開一個 GitHub issue、切分支、驗證、審查差異、開 pull request、squash 合併、對 `main` 打標籤，然後發布 release notes。上週我想在公司的 monorepo 上伸手拿同一套東西，卻在第一步就停住了。那個儲存庫屬於組織，不屬於我。我不會合併自己的工作，不會打標籤，發布節奏也是別人的決定。

有用的發現是，**砍完之後留下來的並不是一個比較小的發布 skill**。它是另一種產物，因為它產出的東西不再是一次合併，而是一份同事在批准任何事情之前會讀的交接文件。本文將整理哪些步驟被拿掉、什麼取代了它們，以及在所有權消失之後才浮現的三個設計決定。

## 發布 skill 假設自己擁有這個儲存庫

我部落格的發布 skill 會跑完橫跨四個階段的十七個編號步驟。攤開來看，其中多數與其說是工程，不如說是所有權的主張。

| 步驟 | 它需要什麼 |
| --- | --- |
| 建立 GitHub issue、指定里程碑 | issue 的寫入權限，以及一套屬於我的里程碑慣例 |
| 更新 `package.json` 的版本號 | 決定版本號的權限 |
| 把 `CHANGELOG.md` 的 `[Unreleased]` 升級 | 決定變更紀錄格式的權限 |
| squash 合併 pull request | 預設分支的合併權限 |
| 對 `main` 打標籤並推送標籤 | ref 的寫入權限 |
| 建立 GitHub release | 發布的權限 |
| 附上 released-in 留言並關閉 issue | 再一次的 issue 寫入權限 |

**十七個步驟裡有七個，在還沒考慮任何程式碼之前就消失了。** 剩下的十個才是真正在做事的：讀取目前狀態、驗證、審查、開分支、提交、推送，以及回報。

發布 skill 花了整整一節去辯護的標籤順序規則，是個好例子。它存在的原因是 squash 合併會把差異當成 `main` 上的一個新提交重播，並丟掉原本的分支，所以在合併前建立的標籤會指向一個不是 `main` 祖先的提交，`git describe` 再也看不到它。那確實是個細微的陷阱，**而它對一個我從不打標籤的儲存庫完全無關。** 發布 skill 那些辛苦換來的知識大多是這個形狀：**正確、學費昂貴，而且範圍僅限於我掌控的儲存庫。**

*Figure — ReleaseVsReview: 被劃掉的那幾列就是論點本身。左欄把發布 skill 的十七個步驟歸納成九列，其中四列需要不屬於我的寫入權限；右欄下半部空著，就是移除它們之後剩下的東西。*

## 砍掉那些需要別人寫入權限的步驟

留下來的東西分成六個階段：讀取目前狀態、自我審查差異、確認遠端有沒有動過、驗證，接著是分支與提交與推送，最後撰寫審查用文件。**最後這個階段在發布 skill 裡根本不存在。**

在我自己的儲存庫上，一次成功執行的產出是一個標籤和一份已發布的 release。在團隊的儲存庫上，**產出是兩份 Markdown 文件**：一份 pull request 內文，以及一份逐個 hunk 解釋差異的說明。我給了代理人兩份來自過去 pull request 的真實範例，並要它把那兩份當成格式規格，而不是自己發明一套範本，因為那些文件已經有一個通過審查考驗的形狀。依閱讀順序共五節：問題與追溯到的根本原因、一張列出實際跑了什麼的驗證表、依變更而非依檔案整理的工作內容、這次變更的代價，以及補充說明。

**skill 在 `git push` 之後就停下來。** 它從 `git remote get-url origin` 組出比較用的 URL 並交出去，而且從不呼叫 `gh`。pull request 由我自己開，這不是我繞過的限制，而是在一個「開 pull request 既是技術行為也是社交行為」的儲存庫裡，正確的行為。

## 「沒有提交要怎麼審查？」

當代理人提議在提交前先跑自我審查時，我反對了。那看起來像是範疇上的錯誤：審查讀的是差異，差異是提交產生的，所以在提交前審查聽起來就是在審查空氣。

答案是 **`git diff HEAD` 會把已暫存與未暫存的變更一起拿來，跟上一個提交比對。** 沒有什麼好等的。要讀哪一道指令，只取決於手上正在進行的是什麼。

*Figure — DiffSources: 要讀哪一道指令只取決於手上正在進行的是什麼。第一列裡完全沒有提交，這正是審查能跑在提交之前的原因。*

這件事的份量比初看時更重。如果審查跑在提交之後，它產出的每一項意見都只能落成第二個提交，分支上就會多出一個「處理審查意見」的提交，而人類審查者得先讀它再在腦中把它丟掉。**讓審查先跑，意味著那些修正與原本的工作再也分不出來**，因為它們本來就是同一件事。審查者讀到的是一份完整的變更，而不是一份變更加上它的勘誤表。

我對機制的理解是錯的，但那個反對仍然值得提出，因為它迫使這個順序被論證，而不是被預設。

## `check:fix` 會寫入工作目錄，所以它跑在提交之前

這個 monorepo 用的是 Biome，而儲存庫標準化採用的指令碼是 `check:fix`，也就是 `biome check --write`。**它不回報問題。它改寫檔案。**

**光是這一件事就把階段順序定死了。** 驗證不能排在提交之後，否則格式化的變更會被留在工作目錄裡，然後需要第二個提交去收拾。它也不能換成唯讀的 `check`，因為這個儲存庫自己的慣例就是把格式套用下去，而不是抱怨它。於是順序被強制決定：審查、套用修正、驗證、提交、推送、撰寫文件。

*Figure — PhaseOrder: 限制在階段 4。因為它是寫入而不是回報，它不能排在階段 5 之後，其餘順序都由此推導出來。*

代理人提議偵測一次變更碰到了哪些工作區，而不是每次都跑完整套，我接受了。這個對應是機械式的，從變更的路徑對到套件的過濾條件，只有一個例外是它主張的，而我認為那是對的：**共用的 `common` 套件底下的變更會升級成驗證全部。** 三個應用程式都吃這個套件的建置產物，所以一個被改動的型別在 `common` 裡編譯得乾乾淨淨，卻在使用端的 `tsc` 壞掉。偵測範圍太窄，就會對一個根本建置不起來的變更回報綠燈。

有個細節第一次看到會很嚇人，值得寫下來：client 與 admin 的建置輸出到已經被忽略的目錄，server 的建置也是。**跑完整套驗證之後 `git status` 仍然是乾淨的。** 如果建置產物出現在那裡，那代表某個路徑出了問題，而不是代表建置大聲地成功了。

## 忽略規則放在 `.git/info/exclude`，不是 `.gitignore`

那兩份生成的文件總得放在某個地方，而最直覺的答案是儲存庫裡的一個暫存目錄加上 `.gitignore` 的一行。那個答案錯了，理由跟那些發布步驟錯掉的理由一樣。

**`.gitignore` 是被追蹤的檔案。** 把一個私人的暫存目錄加進去，意味著我的 pull request 裡多了一段審查者得讀的差異、一行被提交進一個不屬於我的儲存庫，以及把我的本機慣例強加給每一個複製這個儲存庫的人。這些都不是我要的。我要的只是一個放草稿的地方。

**`.git/info/exclude` 有相同的語法與相同的效果，而且它是每份複本各自持有、不被追蹤的：**

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

驗證方式跟任何其他忽略規則一樣，而 `git check-ignore` 會告訴你這條規則是哪個檔案提供的：

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

同一個檔案最後也收留了那個 skill 本身。**在這個儲存庫裡，`.claude/` 不是個人目錄** ，它已經追蹤著共用的代理人與斜線指令，所以在它底下開一個新資料夾，會被讀成某個團隊應該接手的東西。一個把單一個人偏好編碼進去的工作流程 skill 不是那種東西，至少在有人開口要它之前不是：

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

三個項目，沒有任何一個是別人得審查的行。

**代價是這條規則帶不走。** 別人的複本上沒有它，我自己重新複製一份之後也不會有。對一個放著草稿、而那些草稿注定要被貼進網頁表單然後遺忘的暫存目錄來說，那是正確的取捨，而且跟不打標籤是同一個判斷：當儲存庫不是我的，預設就是不在裡面留下痕跡。

## 一份指名這個程式碼庫的檢查清單，找得到通用清單找不到的東西

**一個只被交代「找 bug」的審查代理人，會產出一串不是 bug 的東西。** 代理人是從儲存庫本身導出這份檢查清單的，而其中真正站得住腳的面向，正是從外面猜不出來的那些。

### 那個不留痕跡的區塊

最強的例子是 server 裡的一個內容區塊 reducer。它走訪一串區塊，並透過一張回呼表分派每一個，而在那張表裡 **除了 text 的處理器之外，每一個處理器都是選用的**，每個分支的守門條件都是「是這個區塊型別，而且有對應的回呼存在」。

*Figure — SilentDrop: 沒有 else 分支。缺少回呼會讓判斷式為假，於是那個區塊被略過的路徑，跟什麼都不略過時走的是同一條。*

因此，一個從未為某種區塊型別定義處理器的供應商整合，會無聲地丟掉那個區塊。**不會拋出例外。不會留下記錄。** 內容就是沒有送達模型，而失敗會在稍後以一段畸形的對話、或是一個看不出原因的 API 驗證錯誤浮現。

這個形狀在這個程式碼庫裡已經造成過兩次上線的 bug，所以「新的區塊型別出現，卻沒有在每個供應商裡加上對應的處理器」值得列為高嚴重度的發現。**通用的審查器永遠不會去找它**，因為程式碼裡沒有任何一處看起來像是錯誤。

### 哪個檔案註冊了路由，決定了誰能到達它

後端把路由註冊在各個前綴底下，而其中只有兩個帶著驗證用的中介軟體。一個會碰到物件儲存或資料庫的處理器，在那兩個前綴底下是安全的，放在其他任何地方就是公開可達的，這使得 **「哪個檔案註冊了這個路由」是一個資安問題，而不是一個整理問題。**

這個儲存庫自己的路由進入點上，已經有一段註解寫著完全一樣的話。把一段既有的註解編碼成審查面向不花什麼成本，而且抓到的正是那個真正會出事的錯誤。

### 這份檢查清單拒絕回報什麼

同樣有用的是它排除掉的那串發現，因為每一項都已經由下游的某個東西保證了：

| 不回報 | 已經由誰處理 |
| --- | --- |
| 格式、import 順序、引號風格 | 兩個階段之後改寫它們的 `check:fix` |
| 未使用的 import、應該是 `const` 的 `let` | 同一階段的 Biome lint 規則 |
| 型別錯誤 | 每個工作區建置裡的 `tsc` 步驟 |

一份回報這些東西的審查，是在把讀者的注意力花在根本到不了 pull request 的發現上，並把那些真正需要人類判斷的發現埋在它們底下。

## 一個語言設定原來是兩個

我要求所有生成的內容都用同事讀的語言。代理人在答應之前先查了儲存庫的歷史，然後帶著一個矛盾回來：**日誌裡的每一個提交都是英文，都是 Conventional Commits 的形式，包括其他所有人的。** 用另一種語言寫的提交訊息，會是那裡唯一的異類。

所以這個切分是依讀者而不是依偏好。審查用的文件跟著審查者走，因為那是一個人讀一次然後做出決定的東西。提交訊息維持英文，因為它們加入的是一份比這次審查活得更久、而且屬於所有人的共用日誌。**我一直把「語言」當成一個設定，但它其實是兩個**，而代理人之所以注意到，是因為它讀了日誌而不是讀我的指示。

這是整件事裡我自己到不了的部分，而誰做了什麼值得寫清楚：範圍、格式規格，以及每一個最終決定都是我的，而那個我沒看見的矛盾，是代理人該找出來的。

## 那份裝不下自己範例的範本

有一個失敗值得記下來，因為修法不是最明顯的那一種。

那兩份文件範本裡含有實作範例，而那些範例本身就是程式碼圍籬。一個 ` ```markdown ` 區塊裡包著一個 ` ```diff ` 區塊時，會在內層的圍籬處被終止。代理人的第一個嘗試是在每個內層圍籬前面加一個零寬空格，它渲染出來看不見，又能阻止解析器比對成功。這確實有效，代價是在一份別人會讀的檔案裡撒滿看不見的字元。

要把它們再拿掉的過程並不順利：

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

**那道指令回傳 0，而且什麼都沒改。** 少了 `-CSD`，Perl 會把檔案當成位元組而不是 UTF-8 來讀，所以 `\x{200b}` 永遠比不上檔案裡實際存在的那三個位元組。線索是：在一次連錯誤都沒回報的替換之後，`grep -c` 仍然數到四個。

```text
remaining: 4
```

兩個修正，都很小。`perl -CSD` 讓替換看到的是字元而不是位元組。而外層圍籬改成 `~~~`，Markdown 接受它作為替代的圍籬記號，於是內層那些反引號圍籬完全不需要跳脫，檔案裡也不再有任何看不見的字元。**第二個修正意味著第一個從來就不需要**，這正是這類問題慣常的形狀。

## 總結

把一個發布 skill 移植到一個不屬於我的儲存庫，結果是一場帶著一項加法的減法。十七個步驟裡有七個被拿掉，因為它們主張的是所有權：issue、里程碑、版本號、變更紀錄的權限、合併、標籤、發布。取代它們的是一個撰寫文件的階段，因為現在的最後一步是一位同事讀 pull request 的內文，而不是一次合併上的綠色勾勾。

有三個決定是在所有權消失之後才看得見的。審查跑在提交之前，因為 `git diff HEAD` 讓它成為可能，而把修正折進原本的提交，能讓審查者只需要讀一份變更。驗證也跑在提交之前，因為這個儲存庫的格式化工具是寫入而不是回報。至於暫存目錄則是透過 `.git/info/exclude` 而不是 `.gitignore` 被忽略，因為一個被追蹤的忽略檔，等於是我在別人的儲存庫裡又多改了一樣東西。

如果有一個可以一般化的形式，那會是：**一個工作流程 skill 編碼的是一組權限，其程度不亞於它編碼的一組步驟**，而能在搬家之後活下來的步驟，是那些從一開始就與權限無關的。

## 參考連結

- [gitignore 文件，包含每份複本各自持有的 `$GIT_DIR/info/exclude` 及其優先順序](https://git-scm.com/docs/gitignore)
- [Claude Code Agent Skills，說明 SKILL.md 的 frontmatter 與隨附資源](https://docs.claude.com/en/docs/claude-code/skills)
- [Biome `check` 指令參考，其中記載 `--write` 會套用安全的修正](https://biomejs.dev/reference/cli/#biome-check)
- [Conventional Commits 1.0.0，這個儲存庫的日誌所遵循的格式](https://www.conventionalcommits.org/en/v1.0.0/)
- [perlrun 的 `-C` 選項，控制 Perl 是否把輸入視為 UTF-8](https://perldoc.perl.org/perlrun#-C-[number/list])
