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

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

白色卡片上的赭紅色 Claude 星形標誌與黑色字樣
本頁目錄

引言

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

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

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

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

可運作的設定

兩條符號連結與一個設定 key,只需設定一次。

終端機視窗
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
~/.claude/settings.json
{
"permissions": {
"additionalDirectories": ["/path/to/blog-repo"]
}
}

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

這兩半做的是不同的工作,誰也不能取代誰。符號連結讓 skill 可以被發現:官方文件說明,personal、project 或 enterprise 任一位置的 skill 名稱項目都可以是符號連結,Claude Code 會從連結的目標讀取 SKILL.md。設定 key 那一半給的是對 repo 的檔案存取權,同時讓那個目錄成為已許可的目錄,這一點的作用下面會談。

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

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

實際用起來是什麼樣子

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

/blog new astro service worker cache eviction

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

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

文章檔案跨到部落格 repo,對話紀錄留在原地兩個 repo 並排。左邊是你當時工作的 repo,存放這次工作的對話紀錄,new 與 figures 兩個子指令在這裡執行。一個箭頭帶著文章檔案跨到右邊。右邊是部落格 repo,存放文章與建置流程,publish 在這裡執行。註記說明對話紀錄本身不會移動。在工作發生的地方撰寫,在建置網站的地方發布你當時工作的 repo存放這次工作的對話紀錄/blog new/blog figures部落格 repo存放文章與建置流程/blog publish文章檔案對話紀錄不會移動,所以是撰寫流程過去找它。
跨過去的只有文章檔案。構成這篇文章的對話紀錄不會移動,所以是撰寫流程過去找它,而不是把材料交接出來。

接著回到部落格 repo:

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

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

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

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

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

終端機視窗
cd -P ${CLAUDE_SKILL_DIR}/../../.. && pwd

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

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

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

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

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

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

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

這條規則永遠不會命中。權限文件寫得很明白:可辨識的分隔符是 &&、||、;、|、|&、& 與換行,而且規則必須逐一命中每個子指令。一條自身文字裡就含有 && 的規則,對它所要涵蓋的那道指令的任一半都不會命中。

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

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

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

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

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

假設二:/add-dir 可以用

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

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

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

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

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

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

假設三:符號連結是透明的

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

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

往符號連結內部讀取可解析,用 .. 向外則否兩列對照。第一列中,往符號連結的 skill 目錄內部下降的路徑到達 repo 內的真實檔案,標示為可解析。第二列中,同一目錄後接三層 .. 的路徑到達家目錄底下而非 repo,標示為找不到。符號連結向下透明,向上不透明寫下的路徑實際到達的位置~/.claude/skills/blog/voice.md<repo>/.claude/skills/blog/voice.md可解析往連結目錄內部讀取的路徑會到達 repo。~/.claude/skills/blog/../../../docs/glossary.md~/docs/glossary.md找不到.. 會沿著連結自身的上層走,疊幾層都到不了 repo。
往連結目錄內部讀取的路徑會到達 repo。每一個 .. 都改為沿著連結自身的上層走,最後落在家目錄。

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

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

由此得到兩個結論。

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

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

假設裡面的假設

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

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

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

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) 看起來像一條已經能用的連結。它讀起來正確、算繪出來正確,只在被使用的那一刻失敗。這就是它撐過兩次程式碼審查與一次發布的原因。

每個錯誤假設都是從工作之外被推翻的三列,將假設與推翻它的事情並列。認為 /add-dir 在每個用戶端都可用,被在 VS Code 擴充套件實際輸入一次推翻。認為權限規則可以包含雙 and 符號,被閱讀權限官方文件推翻。認為相對連結會以 skill 目錄為基準解析,被從無關 repo 執行的一次診斷推翻。沒有一個是靠重讀成果本身找到的。三個假設,以及推翻它們的事情假設被什麼推翻1/add-dir 在每個用戶端都是可用的入口在 VS Code 擴充套件實際輸入一次2allowed-tools 規則可以包含 &&讀了權限的官方文件3相對連結會以 skill目錄為基準解析從無關的 repo執行一次診斷沒有一個是靠重讀自己的成果找出來的。
每一次發布都修掉前一次的假設。推翻它們的東西每次都來自工作之外:編輯器、官方文件,以及在另一個 repo 裡跑一次。

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

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

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

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

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

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

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

真正抓到這些的是什麼

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

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

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

那次診斷的界線

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

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

總結

要讓一個 Claude Code skill 能從任何 repo 執行:

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

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

參考連結

分享這篇文章