# 從別的儲存庫執行 Claude Code 的 skill — 符號連結、cd -P，以及每一條路徑都用絕對路徑

> 讓 Claude Code 的 skill 能從任何儲存庫呼叫：用符號連結做發現、用 additionalDirectories 給存取權、入口執行一次 cd -P，並且不使用相對路徑。

- Source: https://oharu121.com/zh-tw/blog/claude-code-skill-from-another-repo-symlink-absolute-paths/
- Published: 2026-08-21T17:31:04+09:00
- Tags: Claude Code, 開發工具, 自動化

---
**重點摘要**

- skill 的目錄可以是一條符號連結，而且這條連結會被正常發現。這就是它不必複製也能從每個儲存庫到達的原因。
- `${CLAUDE_SKILL_DIR}` 展開的是**連結自身**的路徑，不是連結的目標。路徑會以字面方式正規化，所以 `..` 會沿著連結的上層走，永遠到不了真正的儲存庫。
- `cd` 需要 `-P` 才會先以實體方式解析連結。檔案類工具沒有對應的選項，所以傳給它們的每一條路徑都得是絕對路徑。
- `/add-dir` 用一道指令就能完成同樣的事，但它在 VS Code 與 JetBrains 的擴充套件裡並不存在。
- 測試要從真正的遠端工作階段執行，用編號的檢查清單搭配固定的輸出格式。要逐字的證據、一個反向對照，以及明確詢問「有什麼是你做不到的」。

## 引言

我經營一個三語部落格，它的文章流程是一個放在部落格自己儲存庫裡的 Claude Code skill。問題在於，幾乎每一篇文章都是*關於在別處發生的工作*：另一個程式碼庫裡的事件、修好之前試過卻失敗的三件事、以及誰做了哪個決定。那些材料只存在於親身經歷它的那個工作階段裡，過了一週再問，回來的版本就單薄了。

所以撰寫想要從另一個儲存庫開始，而流程仍留在這一邊。我原本的權宜做法是把 skill 的路徑貼進那個工作階段，結果產出的草稿看起來完成了，卻悄悄跳過了流程裡的每一項檢查。

最後的解法由四件事組成，而且沒有一件是我原本去找的：用**符號連結**做發現、用 `permissions.additionalDirectories` 給存取權、在入口執行一次 `cd -P`，以及**任何地方都不寫相對路徑**。走到這一步花了一天四次發布，因為關於路徑如何解析的三個假設分別都是錯的，而且每一個都只能從遠端那側看見。

本文先說明可運作的設定，接著逐一整理每個假設與推翻它的東西。

## 可運作的設定

兩條符號連結與一個設定鍵，只需設定一次。

```bash
mkdir -p ~/.claude/skills
ln -s /path/to/blog-repo/.claude/skills/blog ~/.claude/skills/blog
ln -s /path/to/blog-repo/.claude/skills/svg  ~/.claude/skills/svg
```

```json title="~/.claude/settings.json"
{
  "permissions": {
    "additionalDirectories": ["/path/to/blog-repo"]
  }
}
```

之後 `/blog new <topic>` 在任何儲存庫都能用，不需要每個工作階段再下一次指令。

**這兩半做的是不同的工作，誰也不能取代誰。** 符號連結讓 skill 可以被*發現*：[官方文件](https://code.claude.com/docs/en/skills)說明，personal、project 或 enterprise 任一位置的 skill 名稱項目都可以是符號連結，Claude Code 會從連結的目標讀取 `SKILL.md`。設定鍵那一半給的是對儲存庫的*檔案存取權*，同時讓那個目錄成為**已許可**的目錄，這一點的作用下面會談。

符號連結不是複本，這正是重點。它會解析到儲存庫自己的檔案，所以不會走樣。這件事之所以要緊，是因為 personal 的 skill 會**覆蓋**同名的 project skill。放一份複本進去，連在部落格儲存庫裡都會蓋住真正的流程，然後無聲地過時；用連結，蓋住的是它自己。

第二條連結很容易忘。blog skill 透過 `Skill` 工具把圖表工作交給旁邊的 `svg` skill，而沒有被發現的 skill 是叫不動的。

## 實際用起來是什麼樣子

呼叫本身很平淡，而這正是目標。在某個無關儲存庫的工作階段裡，做到一半、正是文章要寫的那段工作時，輸入：

```text
/blog new astro service worker cache eviction
```

skill 會載入，它的入口步驟印出部落格儲存庫的路徑，接著開始訪談。過程中不會提到呼叫端的儲存庫，也不必手動輸入任何路徑。

值得寫清楚的是**東西最後落在哪裡**，因為這裡有一個現成的錯誤答案。文章資料夾、`.mdx`、圖表元件與圖片，全都寫進*部落格*儲存庫。遠端那個儲存庫不會被動到。握有材料的工作階段留在原地，跨過去的只有檔案。

*Figure — FarNearSplit: 跨過去的只有文章檔案。構成這篇文章的對話紀錄不會移動，所以是撰寫流程過去找它，而不是把材料交接出來。*

接著回到部落格儲存庫：

```text
/blog publish astro-service-worker-cache-eviction
```

**只有發布這一步留在原地**，而且不是為了整潔。它會啟動開發伺服器來量測圖表標籤、跑一次正式建置，並且 grep 建置產物。支配這三件事的規則寫在儲存庫自己的 `CLAUDE.md` 裡，而 `CLAUDE.md` 不會從符號連結、也不會從 `--add-dir` 到達的目錄載入。在遠端工作階段跑這些步驟，等於少了護欄。

## 每個子指令都從一次 cd 開始

`--add-dir` 與符號連結都能讓你到達 skill，但兩者都不會改變工作目錄，那裡仍然是遠端的儲存庫。也就是說 `pnpm check`、`node scripts/prose-check.ts` 以及 skill 裡其他每一道指令，都會對著錯誤的專案執行。

Bash 工具維持單一一個 shell，它的工作目錄會**跨呼叫保持**，所以在每個子指令開頭 `cd` 一次，就足以涵蓋 skill 裡全部三十五個指令區塊：

```bash
cd -P ${CLAUDE_SKILL_DIR}/../../.. && pwd
```

`pwd` 不是裝飾。它讓「我現在在錯的儲存庫」這件事變得看得見而不是無聲，而如果遠端儲存庫剛好也定義了自己的 `check` 指令稿，少了它就會讓檢查對著錯誤的專案通過。

這種保持有一個值得知道的條件，而它決定了整個設計成不成立。以三個目標量測的結果：

| `cd` 的目標 | 是否已許可 | 下一次呼叫的 `pwd` |
| --- | --- | --- |
| 專案根目錄底下的目錄 | 是 | 保持住了 |
| 列在 `additionalDirectories` 的 `/tmp` | 是 | **保持住了** |
| 沒有列入的儲存庫 | 否 | `Shell cwd was reset to …` |

**工作目錄只有在該目錄屬於已許可時才會存活。** 這就是 `additionalDirectories` 不是可選項的原因。少了它，`cd` 會在指令結束後被靜靜取消，下一項檢查就跑在遠端的儲存庫裡。

## 假設一：複合指令可以事先核准

第一版用 skill 的 `allowed-tools` 規則事先核准了入口步驟：

```
Bash(cd ${CLAUDE_SKILL_DIR}/../../.. && pwd)
```

這條規則永遠不會命中。[權限文件](https://code.claude.com/docs/en/permissions)寫得很明白：可辨識的分隔符是 `&&`、`||`、`;`、`|`、`|&`、`&` 與換行，而且**規則必須逐一命中每個子指令**。一條自身文字裡就含有 `&&` 的規則，對它所要涵蓋的那道指令的任一半都不會命中。

它什麼也沒授予，而且是無聲地什麼也沒授予。這一步之所以仍然沒有跳出提示，是因為 `cd` 到已許可的目錄與 `pwd` 都是內建的唯讀指令。這條規則被刪掉而不是拆開，因為那一步跳出提示是一個值得留著的*訊號*。

**那個訊號代表什麼，是到第二次修正才寫對的。** 最初的版本說，那裡跳出提示代表目標不是已許可的目錄。它也可能代表在那個情境下 Bash 工具沒有被授予——旁邊的 `svg` skill 遇到的正是這一種：它被加上了一個用 `cd` 的必要步驟，而它的 `allowed-tools` 裡一個 `Bash` 項目也沒有。兩種原因、兩種不同的修法，只講其中一種會把讀者送去錯的檔案。

### 同一條規則也拆穿了第二個說法

同一條分割規則也拆穿了第二個說法。我先前否決過一個在每道指令前加上 `cd … &&` 的設計，而代理當時把這個否決解釋成「加前綴會破壞現有那條沒有前綴的 `Bash(pnpm check)` 規則」。並不會。因為是逐一比對子指令，`cd X && pnpm check` 完全能命中 `pnpm check` 的規則。

我否決加前綴的理由是：為了罕見的情況付出滿篇的更動。這個理由今天仍然成立，寫在它底下的那個理由則不成立。

## 假設二：/add-dir 可以用

`/add-dir <path>` 一道指令就做完符號連結與設定鍵合起來做的事。在第一次發布時，它是唯一被寫進文件的入口。

**它只有 CLI 才有。VS Code 與 JetBrains 的擴充套件裡沒有這個指令。** 而我實際撰寫的地方正是那裡。這個功能出貨時，帶著一扇從它所服務的那個房間打不開的大門。

文件裡沒有任何地方提到這件事。權限頁面只寫「`--add-dir` 選項或 `/add-dir` 指令」，沒有例外說明，而全篇唯一被記下的擴充套件差異是 `/bug`。[Issue #36123](https://github.com/anthropics/claude-code/issues/36123) 曾要求在 IDE 擴充套件支援它，最後以 `NOT_PLANNED` 關閉，而且是因為長期無人回應由自動流程關掉的，不是誰做了判斷。它也屬於一個更大的已知落差：擴充套件的斜線指令清單相對 CLI [並不完整](https://github.com/anthropics/claude-code/issues/8590)。

### 為什麼那個顯而易見的變通做法沒有用

在整合終端機裡執行 `claude --add-dir <repo>` 完全可行，選項是真的、變數替換也是真的。但它會開啟一個**新的工作階段**，而值得寫下來的材料在*當前*工作階段的紀錄裡。那份紀錄正是撰寫要從別處開始的全部理由。

`/add-dir` 仍以第二條路徑留在文件裡，因為在有它的環境它更好：它會載入所加入儲存庫的整個 `.claude/skills/` 目錄，包含旁邊的 skill，並且在同一個動作裡讓那個儲存庫成為已許可的目錄。它不需要符號連結，也不需要設定鍵。

## 假設三：符號連結是透明的

這一個花掉兩次發布，也是最值得帶走的部分。

一個符號連結的目錄**向下透明，向上不透明**。往它*裡面*的讀取會解析到實體檔案；用 `..` 往*外面*爬的則不會，因為路徑是以**字面方式**正規化的：`..` 是對著文字上的路徑折疊，從來看不到連結的目標。

*Figure — SymlinkAsymmetry: 往連結目錄內部讀取的路徑會到達儲存庫。每一個 `..` 都改為沿著連結自身的上層走，最後落在家目錄。*

在 `~/.claude/skills/blog` 建立連結後量測的結果：

| 路徑 | 結果 |
| --- | --- |
| `<link>/voice.md` | 解析到實體檔案 |
| `cd <link>/../../..` | **錯誤**，落在 `~` |
| `cd -P <link>/../../..` | 正確，且與沒有符號連結時相同 |
| 讀取 `<link>/../../../docs/glossary.md` | `File does not exist` |
| 讀取 `<link>/../svg/web-figure.md` | `File does not exist`，連旁邊的 skill 也一樣 |

由此得到兩個結論。

**`cd` 需要 `-P`。** shell 保有的是*邏輯上的*工作目錄，所以沒有加選項的 `cd` 會在符號連結的路徑上走 `../../..`。`-P` 會先以實體方式解析連結，再從真實位置套用 `..`。在沒有符號連結時結果相同，所以把它設成無條件並不吃虧。

**檔案類工具沒有 `-P`，而且疊再多 `..` 也到不了儲存庫。** `${CLAUDE_SKILL_DIR}` 展開的是連結自身的路徑，所以也不能靠它往外走。任何位於 skill 目錄之外的東西，都必須從入口 `cd` 印出的根路徑組出來。這就是那一步會回報 `pwd`、而 skill 之後都以那個值的名稱來稱呼它的原因。

## 假設裡面的假設

把上面這些都寫下來之後，skill 仍然主張對*它自己*的檔案使用相對路徑是可行的，而且替這個主張貼上了「經過量測，不是假設」的標籤。

它並沒有被量測過。被測過的是 `~/.claude/skills/blog/voice.md`，也就是**絕對**路徑的寫法；skill 實際寫的是 `[voice.md](voice.md)`，也就是**相對**路徑的寫法。兩個只差一個字的不同主張，而帶著「量測」二字的是錯的那一個。

從無關儲存庫執行的一次診斷回傳了十一項通過與三項失敗，而那三項正是這條流程在讀自己的寫作規範：

```text
File does not exist. Note: your current working directory is
/Users/…/oharu-tech-blog.
```

**檔案類工具不會以 skill 的目錄為基準去解析相對路徑。** 即使是就躺在被讀檔案旁邊的檔案也一樣。

修正的方式很機械：把橫跨十個檔案的六十一條 Markdown 連結改寫成帶著自身目錄的形式。這樣算繪出來的分派表會寫成 `[new.md](/Users/…/.claude/skills/blog/new.md)`，原封不動傳過去就是對的。

寫成一條規則大概也會有用，因為遠端工作階段在每一項用到同類規則的檢查裡都正確地套用了它。之所以仍然改寫連結，是為了一個不對稱：預留位置*看起來就是未解析的*，忽略它的人拿到的是明顯壞掉的路徑；而 `[voice.md](voice.md)` 看起來像一條已經能用的連結。它讀起來正確、算繪出來正確，只在被使用的那一刻失敗。這就是它撐過兩次程式碼審查與一次發布的原因。

*Figure — AssumptionChain: 每一次發布都修掉前一次的假設。推翻它們的東西每次都來自工作之外：編輯器、官方文件，以及在另一個儲存庫裡跑一次。*

## 如何測試一個只在別處壞掉的流程

這是我打算在任何有「遠端」與「本地」兩側的東西上重複使用的部分，而走到這裡之前問錯了三輪。

問題是結構性的。錯誤只存在於那邊的儲存庫，修正只存在於這邊，而這兩者是彼此看不見的兩個工作階段。問「在你那邊能動嗎」，回來的是散文，而那是所有可得形式裡最沒用的一種：它把觀察到的與推論的混在一起，而且那邊沒想到要提的東西就會漏掉。

有效的做法是**在一則提示裡放進編號的檢查清單與固定的輸出格式**，然後請對方把表格交回來。大致長這樣：

```text
Run a diagnostic. This is a TEST: change nothing, write nothing.

T1  Report the base directory the skill loaded from.
T2  Report the exact path the entry step printed.
T3  In a FRESH shell call, run pwd. Report it verbatim.
T4  Read voice.md via the link as the skill writes it. First line?
…
T14 NEGATIVE CONTROL. This is EXPECTED TO FAIL. Try to read exactly:
      ~/.claude/skills/blog/../../../docs/glossary.md
    If it SUCCEEDS, say so loudly.

OUTPUT — reply with only this:
| ID | PASS/FAIL | Evidence (verbatim, one line) |

ANY PATH THAT FAILED TO RESOLVE
ANYTHING YOU COULD NOT DO
```

這個形狀不是第一次的嘗試。第一輪是請遠端工作階段跑兩個子指令、再描述發生了什麼，結果拿到的正是那種好讀卻藏東西的散文。我要求把它重整成一則帶固定輸出格式的提示，值得留下來的是那個版本。

### 這份檢查清單為什麼有效

有四點，而且每一點都是先漏掉才學到的。

- **要逐字的證據，不要判定。** 寫著 `PASS` 的欄位是一種主張；裝著 `# Voice` 的欄位才是事實。當讀表的人並不在現場，區分真正的通過與看似的通過的，就是證據那一欄。
- **放進一個反向對照。** 一項*預期會失敗*的檢查，才能證明這套量法是誠實的。如果連不該通過的東西都通過了，那次執行什麼也沒量到。
- **問對方有什麼做不到。** 這產生了整件事裡最有用的一行，而且是沒被要求就寫出來的：遠端工作階段回報，它的反向對照所得到的錯誤訊息與三項真正的失敗一字不差，所以單靠那次執行無法區分兩種互相競爭的解釋。少了這一行，那個結果就會被當成這項測試其實還沒掙得的證據。
- **寫上「什麼都不要修」。** 一個悄悄換上正確路徑重試的工作階段會回報成功，並且毀掉證據。無聲的自行復原，正是最初那個錯誤主張得以存活的途徑。

之後的循環就是：在那邊跑檢查清單、把表格貼回這邊、在這邊修、再跑一次。**遠端工作階段不編輯任何東西**，這讓變更只發生在一個儲存庫裡。

也值得知道它做不到什麼。針對舊行為寫的提示，會在行為改變的那一刻過期。把連結改成絕對路徑之後，仍有三項檢查在要求「skill 寫的那條相對連結」，而它已經不存在了，於是它們什麼也沒測到就失敗了。遠端工作階段連這件事也注意到了，並且兩種讀法都測了，而不是逕自挑一種。

## 真正抓到這些的是什麼

值得直說，因為它決定了剩下的工作要怎麼驗證。

兩次程式碼審查與一次發布，都讓那個相對路徑的主張過關了。從另一個儲存庫執行的十四項診斷，一次就抓到它。審查擅長找出兩段文字之間的矛盾，但抓不到一個本身說得通、卻與世界不符的句子。

另外兩個也是同樣被抓到的，從迴圈外面。`/add-dir` 不存在這件事，是我實際去輸入才發現的。複合規則的錯誤，是在一個驗證用的代理去讀權限文件、而不是讀程式碼時才浮現的。

### 那次診斷的界線

反向對照回傳的錯誤與三項真正的失敗一字不差，所以**單靠那次執行，無法區分「爬出目錄後被折疊掉」與「相對路徑根本沒有基準」**。字面折疊這個模型之所以站得住，靠的是另一批證據，那是用絕對路徑取得的，因此與工作目錄無關。一項無法區分兩種解釋的測試，不論判定欄寫了什麼，都還沒有在兩者之間做出選擇。

有一個問題是刻意留著沒答的。相對路徑究竟*以什麼為基準*解析，是工作階段的目錄還是 shell 的目錄，從來沒有用能區分兩者的路徑測過，而 skill 就這樣寫著，沒有選邊。因為用絕對路徑在兩種情況下都正確，所以沒有東西取決於這個答案。用推論去把它定案，正是前面三個出錯的那條路。

## 總結

要讓一個 Claude Code skill 能從任何儲存庫執行：

- **把 skill 的目錄用符號連結接到 `~/.claude/skills/`**，包含它會呼叫的每一個旁邊的 skill。連結不會過時；複本會蓋住原件，然後過時。
- **把儲存庫加進 `permissions.additionalDirectories`。** 它給的是檔案存取權，也讓那個目錄成為已許可的目錄；少了這一點，工作目錄就不會再跨呼叫保持。
- **每個子指令都以 `cd -P ${CLAUDE_SKILL_DIR}/../../.. && pwd` 開始**，並回報那條路徑。一次 `cd` 就涵蓋 skill 裡所有的 shell 指令，而讓它在符號連結底下也正確的是 `-P`。
- **完全不要使用相對的檔案路徑**，連同一個目錄裡的檔案也一樣。連結要寫成帶著自身目錄的形式，skill 之外的一切則從入口步驟印出的路徑組出來。
- **`/add-dir` 一道指令就取代前兩步，但它在 IDE 擴充套件裡不存在。** 在有它的地方就用它。
- **用編號的檢查清單與固定的輸出格式來測試**，從真正的遠端工作階段執行，並要求逐字的證據、一個反向對照，以及明確的「有什麼是你做不到的」。

一再出現的失敗，並不是某一條路徑寫錯了，而是在測了旁邊的東西之後就把機制寫了下來：測了絕對路徑卻寫下相對路徑的行為，測了本地工作階段卻寫下遠端的行為。每一次，帶來修正的都不是再讀一遍，而是讓它真的在別的地方跑一次。

## 參考連結

- [Extend Claude with skills, including the note that a skill directory entry may be a symlink](https://code.claude.com/docs/en/skills)
- [Configure permissions, covering additional directories and the recognised command separators](https://code.claude.com/docs/en/permissions)
- [Issue #36123: Support /add-dir in IDE extensions (VS Code / JetBrains), closed NOT_PLANNED](https://github.com/anthropics/claude-code/issues/36123)
- [Issue #8590: VS Code extension has an incomplete slash command list compared to the CLI](https://github.com/anthropics/claude-code/issues/8590)
- [Issue #14836: /skills does not find skills in symlinked directories, still open](https://github.com/anthropics/claude-code/issues/14836)
