Claude Code deep links 完整教學:把 runbook 的下一步變成一個連結
值班的人半夜三點被 call 起來,翻開 runbook 第三步:
- 開一個終端機,
cd到 web-gateway 的 repo,啟動 Claude Code,把下面這段貼進去:
1
2 5xx rate is elevated on web-gateway. Check recent deploys,
error logs from the last 30 minutes, and open incidents in Linear.
看起來很完整。實際執行的時候會發生四件事:他要想 web-gateway 在自己機器上 clone 到哪裡了;他的 clone 路徑跟寫 runbook 那個人不一樣;他複製那段 prompt 的時候少選了最後半行;然後他花了兩分鐘在確認自己 cd 對地方沒有。
這四件事每一件都很小。加起來就是為什麼半夜的 runbook 總是比白天演練的時候慢。
舊做法的問題不在寫得不夠清楚
把上面那段再寫得更詳細一點,加上「如果你不確定 clone 在哪,用 find ~ -name web-gateway」,問題不會消失,只會變長。
因為那段文字要求讀的人做四次轉譯:從文字轉成 cd 指令、從自己的心智模型轉成實際路徑、從畫面上的段落轉成剪貼簿內容、從剪貼簿轉成輸入框。每一次轉譯都是一次出錯的機會,而半夜三點的出錯率跟白天不是同一個數量級。
新做法把這四次轉譯壓成一次點擊:
1 | ## High 5xx rate on web-gateway |
點下去,一個新的終端機視窗開起來,Claude Code 已經跑在他自己那份 clone 裡,prompt 已經在輸入框裡,游標在後面等他按 Enter。
它跟 mailto: 走同一條路
claude-cli:// 是一個自訂的 URL scheme,跟 mailto: 是同一種東西。
mailto: 你早就在用了。網頁上一個 email 連結點下去,瀏覽器自己不會處理,它把整串 URL 交給作業系統,作業系統查表發現 mailto: 這個前綴登記給你的郵件軟體,就把它叫起來。claude-cli:// 走的是完全一樣的路徑,只是查表查到的是 Claude Code。
流程分四步:瀏覽器把 URL 交給作業系統 → 作業系統認出前綴、啟動 Claude Code → 一個新終端機視窗開起來,工作目錄是連結指定的那個,prompt 已經填好 → 你讀過、想改就改,按 Enter 才送出。
最後那句是重點,等下會回來講。
三個參數,兩種指定目錄的方式
claude-cli://open 是 handler 唯一接受的路徑,後面接查詢參數。最短的形式什麼都不帶:
1 | claude-cli://open |
這樣會在你的家目錄開一個空的 session。要有用得加參數,總共只有三個:
q 是要預先填進輸入框的文字。必須 URL-encode,換行用 %0A,上限 5,000 字元。
cwd 是絕對路徑。網路路徑和 UNC 路徑會被拒絕,路徑裡含有隱形字元或雙向控制字元的也會被拒絕。後者是防止有人用從右到左的覆寫字元把 /tmp/evil 顯示成看起來像 /home/you/project 的樣子。
repo 是 GitHub 的 owner/name slug,Claude Code 會把它解析成一份它見過的本地 clone。
cwd 和 repo 都給的話,cwd 贏,repo 被忽略——就算 cwd 指的那個路徑根本不存在也一樣。這條規則不太直覺,但它是確定的行為,不是「盡量」。
cwd 還是 repo,看誰會點這個連結
這兩個參數不是新舊關係,是兩種不同情境。
用 cwd,前提是點連結的每個人都把專案放在同一個絕對路徑下。標準化的 devcontainer、公司統一的 VM image,這種環境下 cwd 最直接。
用 repo,前提是連結要給一群 clone 在不同地方的人。它的解析邏輯值得知道細節,因為它決定了什麼時候會失敗:
每次你在一個 Git repo 裡跑 claude,Claude Code 就把那個目錄的路徑記在該 repo 的 GitHub slug 底下。deep link 進來的時候,repo 會開你最近用過的那一個。多份 clone、多個 worktree 是分開追蹤的,所以它挑的是你最後工作的那份。
失敗的情況只有一種:你從來沒有在那份 clone 裡跑過 claude。查表查不到,session 就開在家目錄。這也是為什麼 runbook 裡的 repo 連結對新人常常「沒反應」。不是連結壞了,是他還沒在那個 repo 裡跑過一次。
session 開起來的時候,歡迎訊息會顯示它挑了哪個路徑,可以拿來確認開對地方了。另外,連結不會幫你切 branch,開起來是什麼狀態就是什麼狀態。
handler 什麼時候才註冊
這是最多人卡住的一格,而且卡住的原因跟直覺相反。
Claude Code 在 macOS、Linux、Windows 上都會註冊 handler,但註冊的時機是你送出互動式 session 的第一個 prompt 的那一刻。開了 claude 然後直接離開,沒送過任何 prompt,handler 不會註冊。沒有另外的安裝指令要跑。
註冊只寫使用者層級的位置:
| 平台 | handler 位置 |
|---|---|
| macOS | ~/Applications/Claude Code URL Handler.app |
| Linux | $XDG_DATA_HOME/applications 下的 claude-code-url-handler.desktop,預設 ~/.local/share/applications |
| Windows | HKEY_CURRENT_USER\Software\Classes\claude-cli |
不確定自己註冊好了沒,在 macOS 上可以直接翻那個 app 的 Info.plist 來看,不用真的點一個連結:
1 | plutil -p ~/Applications/"Claude Code URL Handler.app"/Contents/Info.plist |
我在自己機器上跑出來是這樣:
1 | { |
CFBundleURLSchemes 裡有 claude-cli 就代表這個 app 有宣告要處理這個 scheme。但宣告歸宣告,作業系統認不認又是一回事,要查 LaunchServices 的實際登記:
1 | /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump | grep -i "claude-cli" |
看到 claimed schemes: claude-cli: 就是真的登記上去了。
LSBackgroundOnly 那一行也解釋了一個小疑問:這個 app 不會在 Dock 上跳出來,它只負責把請求轉給終端機。
至於開哪個終端機,macOS 會記住你最近一次互動 session 用的那個並沿用,支援 iTerm2、Ghostty、kitty、Alacritty、WezTerm 和 Terminal.app。Linux 先看 $TERMINAL 環境變數,再找 x-terminal-emulator,再往下試常見的。Windows 的順序是寫死的:Windows Terminal、PowerShell、cmd.exe。所以在 macOS 上開錯終端機的解法很單純:在你想要的那個終端機裡跑一次 claude 就好。
不用點也能觸發
deep link 不是只能點。腳本、alias、監控系統的 webhook 都可以叫它,用作業系統各自的開啟指令:
1 | # macOS |
1 | # Windows PowerShell |
cmd.exe 有個陷阱:start 會把第一個帶引號的參數當成視窗標題,所以要先塞一個空標題進去。
1 | start "" "claude-cli://open?repo=acme/payments&q=review%20open%20PRs" |
Linux 上如果 xdg-open 找不到,它屬於 xdg-utils 套件,精簡的 server image、容器和 WSL 常常沒裝。裝完還是沒動靜的話,那台可能根本沒有桌面環境可以派發。
會被吃掉的地方
寫好連結貼進 GitHub README,會發現它變成一段沒有連結的純文字。
GitHub 渲染 Markdown 的時候只放行 http 和 https,其他 scheme 一律剝掉。README、issue、PR、wiki 全都一樣,[label](claude-cli://...) 只剩下 label,連結沒了,URL 也看不到。
這件事的麻煩不在於不能用,在於它失敗得很安靜。你貼上去、預覽看起來有字、就以為好了,實際上讀的人看到一個不能點的詞。
在 GitHub 上的做法是把整串 URL 放進 code block,讓人看得到、可以複製到網址列。內部 wiki、Notion、Slack 這些允許自訂 scheme 的地方就沒有這個問題。
填進來的 prompt 在你按 Enter 之前是死的
現在回到前面那句「按 Enter 才送出」。
deep link 本身不執行任何東西。它只做兩件事:選一個目錄、把文字填進輸入框。從一個你不信任的頁面點進來,那段 prompt 還是死的,在你讀過並按下 Enter 之前不會有任何東西送到模型。
session 開起來的時候,輸入框下面會有一行警告寫著 Prompt from an external link,一直留到你送出或清掉為止。prompt 超過 1,000 字元的話,這行警告還會附上字元數,提醒你捲上去把全文看完。因為長 prompt 可以把真正的指令推到畫面外,這是這類攻擊最常見的手法。
權限規則、CLAUDE.md、對該目錄的信任提示,全部照常套用,跟你自己開的 session 沒有差別。
想整個關掉的話,settings.json 把 disableDeepLinkRegistration 設成 "disable" 就不會註冊。組織要強制、不讓使用者自己打開,就設在 managed settings 裡。
runbook 從說明書變成入口
回到半夜三點那個人。
舊做法裡,runbook 是一份說明書,它描述一組動作,由人負責執行。新做法裡,runbook 的那一行是一個入口,動作已經編碼在裡面,人只負責決定要不要進去。
差別不在少打幾個字。差別在出錯的地方變了。舊做法可能錯在 cd 錯目錄、複製漏一行、記錯 repo 名字,這些錯誤跟你要解的問題完全無關,卻要佔用你半夜最清醒的那五分鐘。新做法把這些錯誤集中到一個地方:連結寫錯了。而連結是白天寫的,可以被 review、可以被測、只要錯一次就會被修掉。
同樣的道理可以往外推。CI 失敗的通知可以帶一個把失敗 job 名稱填好的連結;監控告警可以帶一個針對那個指標的調查 prompt;新人的 onboarding 文件可以讓每一節都直接開在對的子專案。q 塞不下的長 prompt,官方的建議是把它存成 repo 裡的一個 skill,讓 q 只負責喊那個 skill 的名字。
有兩件事要說清楚。第一,我實際驗證過的只有 handler 在我這台 Mac 上的註冊狀態:app bundle 的 scheme 宣告確認過,LaunchServices 的實際登記也確認過。上面那些參數行為與各平台路徑是照官方 deep links 文件整理的,Linux 和 Windows 我沒有實跑。
第二,VS Code 擴充有它自己的一套 handler,走 vscode://anthropic.claude-code/open,開的是編輯器分頁不是終端機視窗。那組參數跟這篇講的不完全一樣。
要試的話,最小的一步是打開瀏覽器網址列,貼 claude-cli://open 按 Enter。如果一個終端機視窗跳出來,你的 handler 就是好的,剩下的都是在這個基礎上加參數而已。如果沒跳出來,回去跑一次 claude 並且送出一個 prompt,再試一次。




























































































































































