Upload the current file or selected code to a GitHub Gist straight from Xcode's Editor menu, powered by the GitHub CLI (gh).
Three commands under Editor → Gist Uploader:
| Command | Description |
|---|---|
| Upload File to Gist | Uploads the whole file. The gist keeps the original file name. |
| Upload Selection to Gist | Uploads only the selected code (multiple selections supported). Named with a timestamp. |
| Upload Multiple Files to Gist… | Opens a file picker (multi-select) and puts all chosen files into a single gist. Each file keeps its own name. |
On success the gist URL is copied to your clipboard and a notification pops up. Gists are created public — anyone can find and read them, so double-check for secrets before uploading.
All commands are also available from Xcode's Quick Actions (⇧⌘A):
The companion app shows the upload result with a clickable gist link:
- macOS 14+, Xcode 14+
- GitHub CLI:
brew install gh, thengh auth login(needs thegistscope) - An Apple Developer Team ID (a free personal team works) — see Signing
- Edit Signing.xcconfig and set
DEVELOPMENT_TEAMto your own Team ID. - Run:
./install.sh- Check Gist Uploader in System Settings → General → Login Items & Extensions → Xcode Source Editor.
- Restart Xcode. The commands appear at the bottom of the Editor menu whenever a source file has focus.
The script builds the Release configuration, installs GistUploader.app into /Applications, and registers the extension with PluginKit. Run it again any time you pull an update.
A real Team ID is required — ad-hoc signing (-) does not work. XcodeKit.framework is embedded inside the .appex, and dyld refuses to load a nested library whose Team ID differs from the loading process; two unsigned ad-hoc identities do not count as matching.
Find your Team ID with:
security find-certificate -c "Apple Development" -p | openssl x509 -noout -subjectand read the OU field, or look under Membership at developer.apple.com.
- Upload Multiple Files to Gist… cannot see your project files from inside the sandboxed extension, so it hands off to the companion app, which shows a standard Open panel (⌘/⇧-click to select several files). The panel starts in the folder of the workspace currently open in Xcode. File names inside a gist are flat, so two files with the same name are rejected before upload. The same picker is available from the "選擇多個檔案上傳成一個 Gist…" button in the app window, without going through Xcode.
- Upload File to Gist names the gist after the file you are editing. Since XcodeKit deliberately hides file names from extensions, the container app reads the front Xcode window title via AppleScript — the first upload triggers a one-time Automation permission prompt ("GistUploader wants to control Xcode"); allow it. If the lookup fails, a timestamped name is used instead.
- The container app briefly comes to the foreground during upload to show progress and the resulting URL.
ghis looked up in/opt/homebrew/bin,/usr/local/bin, then/usr/bin.
Xcode ─▶ GistUploaderExtension.appex (sandboxed)
│ writes the code to a temp file (multi-file: no temp file,
▼ just opens gistuploader://pick)
gistuploader://upload?file=…&name=…&kind=…
│
▼
GistUploader.app (not sandboxed)
│ runs: gh gist create --public - --filename <name>
│ or gh gist create --public <file1> <file2> … (from the Open panel)
▼
gist URL → clipboard + notification
The split exists because macOS refuses to register an app extension that is not sandboxed, while a sandboxed process cannot run gh. The app only accepts file paths inside the extension's own sandbox container, so the URL scheme cannot be abused by other processes.
Hard-won lessons from building this (macOS 27 / Xcode 27):
- The extension target's
productTypemust becom.apple.product-type.xcode-extension— not the genericapp-extension. With the wrong type the binary lacks theXCExtensionSubsystemlinker glue, the log showsmisconfigured plugin; external subsystem [XCExtensionSubsystem] not present, and the Editor menu shows a grayed-out name with no submenu. - The appex must have the App Sandbox entitlement or
pkdwill not register it at all (pluginkit -mfinds nothing). - Keep the app in
/Applicationsfor reliable PluginKit discovery. Useful commands:pluginkit -m -v -i <bundle-id>(a leading+means enabled),lsregister -f,pluginkit -a <appex>. - Keep the extension binary's linkage minimal (Foundation + XcodeKit), matching the template.
xcodebuildauto-registers the build-directory app with LaunchServices, which produces duplicate entries in System Settings —install.shunregisters it after installing.
透過 GitHub CLI(gh),直接在 Xcode 的 Editor 選單把目前檔案或選取的程式碼上傳成 GitHub Gist。
Editor → Gist Uploader 底下有三個指令:
| 指令 | 說明 |
|---|---|
| Upload File to Gist | 上傳整個檔案,gist 沿用原始檔名 |
| Upload Selection to Gist | 只上傳選取的程式碼(支援多重選取),以時間戳記命名 |
| Upload Multiple Files to Gist… | 跳出檔案選擇面板(可多選),把選到的檔案全部放進同一個 gist,各檔沿用原始檔名 |
上傳成功後 gist 網址會自動複製到剪貼簿並跳出通知。建立的是 public gist(任何人都能搜尋、閱讀),上傳前請先確認內容不含機密資訊。
所有指令也可以從 Xcode 的快速動作(⇧⌘A)呼叫:
容器 App 會顯示上傳結果與可點擊的 gist 連結:
- macOS 14+、Xcode 14+
- GitHub CLI:
brew install gh,並執行gh auth login(需要gistscope) - Apple Developer Team ID(免費的個人團隊即可)——見下方「簽章」
- 編輯 Signing.xcconfig,把
DEVELOPMENT_TEAM改成你自己的 Team ID。 - 執行:
./install.sh- 到「系統設定 → 一般 → 登入項目與延伸功能 → Xcode Source Editor」勾選 Gist Uploader。
- 重新啟動 Xcode,編輯器有焦點時 Editor 選單最下方就會出現指令。
腳本會建置 Release 版、把 GistUploader.app 安裝到 /Applications 並向 PluginKit 註冊。之後更新程式碼再跑一次即可。
必須使用真實的 Team ID,ad-hoc(-)簽章行不通:XcodeKit.framework 嵌在 .appex 裡,dyld 會拒絕載入 Team ID 與主程序不同的內嵌函式庫,而兩個未簽名的 ad-hoc 身分不算相同。
查 Team ID:
security find-certificate -c "Apple Development" -p | openssl x509 -noout -subject讀 OU 欄位,或到 developer.apple.com 的 Membership 頁面查看。
- Upload Multiple Files to Gist… 因為 sandbox 內的 extension 看不到專案裡的其他檔案,會交給容器 App 跳出標準的「打開」面板(⌘ / ⇧ 點選可多選),預設位置是 Xcode 目前開啟的 workspace 所在資料夾。gist 內檔名是扁平的,選到同名檔案會在上傳前被擋下。App 視窗裡也有「選擇多個檔案上傳成一個 Gist…」按鈕,不經 Xcode 也能用。
- Upload File to Gist 會用你正在編輯的檔名。因為 XcodeKit 基於隱私不提供檔名,容器 App 會用 AppleScript 讀取 Xcode 最前方視窗標題——第一次上傳會跳出一次性的「自動化」權限詢問(GistUploader 想要控制 Xcode),請允許。若查詢失敗會改用時間戳記檔名。
- 上傳期間容器 App 會短暫跳到前景顯示進度與結果網址。
gh依序在/opt/homebrew/bin、/usr/local/bin、/usr/bin尋找。
Xcode ─▶ GistUploaderExtension.appex(sandboxed)
│ 把程式碼寫進暫存檔 (多檔上傳:不寫暫存檔,
▼ 直接開 gistuploader://pick)
gistuploader://upload?file=…&name=…&kind=…
│
▼
GistUploader.app(未 sandbox)
│ 執行:gh gist create --public - --filename <name>
│ 或 gh gist create --public <file1> <file2> …(從「打開」面板選的檔案)
▼
gist 網址 → 剪貼簿 + 通知
拆成兩塊是因為:macOS 不註冊未 sandbox 的 app extension,而 sandbox 內又不能執行 gh。App 只接受來自 extension 自己沙盒容器內的檔案路徑,URL scheme 不會被其他程式濫用。
開發過程踩過的坑(macOS 27 / Xcode 27):
- Extension target 的
productType必須是com.apple.product-type.xcode-extension,不能用一般的app-extension。用錯的話執行檔會缺XCExtensionSubsystem連結膠水,log 出現misconfigured plugin; external subsystem [XCExtensionSubsystem] not present,Editor 選單只會有反灰名字、沒有子選單。 - Appex 必須帶 App Sandbox entitlement,否則
pkd完全不註冊(pluginkit -m查不到)。 - App 要放在
/Applications,PluginKit 才會可靠掃描。常用指令:pluginkit -m -v -i <bundle-id>(開頭+代表已啟用)、lsregister -f、pluginkit -a <appex>。 - Extension 執行檔的連結保持最小(Foundation + XcodeKit),和範本一致。
xcodebuild會自動把 build 目錄的 App 註冊進 LaunchServices,造成系統設定出現重複項目——install.sh安裝後會幫忙取消註冊。


