Claude Code SwiftUI 真正值得學的,不是叫 AI 把終端機「換皮」,而是把既有 CLI 留在它最擅長的位置,再補上一個能點、能看、也能被驗收的 macOS 原生介面。核心等式很簡單:原生小工具=可驗證的 CLI 核心+穩定資料邊界+薄 SwiftUI 外殼。
這個方向近期引發很多討論。Thomas Ptacek 主張 AI 已大幅降低個人 macOS GUI 的原型門檻,但他同時說 CLI 通常仍值得保留;Simon Willison 也分享兩個由 AI 協助完成、自己持續使用的 menu-bar App。不過,這些都是實作者案例,不是生產力 benchmark,更不能推成「GUI 成本歸零」或「所有 TUI 都該消失」。本篇會取兩者最可守住的交集:保留 CLI,讓 Claude Code 幫你建立可選的原生入口,最後用測試而不是截圖判定完成。如果你還在比較 coding agent,可先看 Claude Code vs Codex 的工作方式差異。
本篇寫給已會在 Terminal 執行基本指令、有一個可用 CLI,但第一次做 macOS 原生 App 的讀者。你不必先精通 Swift;我們會把每一步都綁到可觀察的完成條件,讓 Claude Code 寫程式,人負責批准邊界與驗收結果。
先說結論:CLI 是核心,SwiftUI 是可選入口
讀完你會完成六件事:挑選適合包裝的 CLI、凍結 JSON contract、用 Claude Code 規劃、建立 MenuBarExtra、以 Process 安全接回 helper,最後補齊資料、錯誤、無障礙與分發驗收。這不是「從此不用終端機」,而是讓非 CLI 使用者多一條低摩擦入口。
先選對 CLI:四個條件少一個都先別包 GUI
第一次練習,請選「單機、低風險、讀取型、輸出可固定」的小工具。例如顯示同步狀態、整理本機專案摘要、查詢固定 API,或把一組診斷結果放進 menu bar。不要從刪檔、改權限、金流、客戶資料或系統管理工具開始;「read-only」也不是零風險,它仍可能把路徑、環境變數或敏感輸出寫進 log。
- 能力邊界:GUI 只呼叫已批准的固定子命令,不接受任意 shell 字串。
- 資料邊界:新增版本化的
--json模式,stdout 放資料、stderr 放診斷,保留原有 exit code 語意。 - 行為邊界:CLI 原本能被腳本、SSH、排程與測試呼叫的方式不變。
- 失敗邊界:先寫下 missing binary、non-zero exit、逾時、空資料、壞 JSON 與超大輸出的畫面。
如果 CLI 本來就是 Swift,優先把業務邏輯抽成 shared library,讓 CLI 與 App 共用同一核心;若重構可能改變既有行為,Apple 也有正式的 內嵌 command-line helper 路徑。這和「LLM 提案、程式裁決」的 Deterministic Core 原則相同:不要把已經能測的部分重新交給模型猜。

Claude Code SwiftUI 步驟 1:先固定環境,再只做規劃
MenuBarExtra 從 macOS 13 起可用,因此本文用 ObservableObject、@Published 與 @StateObject 保持 macOS 13 相容;若直接採用 @Observable,部署下限會到 macOS 14。建置 App 需要完整 Xcode,只有 Command Line Tools 並不含 xcodebuild。
xcode-select --print-path
xcodebuild -version
swift --version
claude --version
cd /path/to/your-cli
claude --permission-mode plan
先讓 Claude Code 探索 repo、列出現有 build/test 指令與 CLI contract,再批准實作。Anthropic 的 官方最佳實務也建議 explore → plan → implement,並把 build、test、exit code、fixtures 或 screenshot 變成可執行的 pass/fail。注意:Plan mode 是工作模式,不是整台電腦的安全邊界;CLAUDE.md 也是行為指引,不會取代 permissions、sandbox 或 deny rules。需要把安全控制做成硬規則時,可搭配 Agent Runtime Controls 的四道閘門思路。
步驟 2:把「完成條件」寫進提示,而不是只說做一個 App
下面這份提示可以直接改。它刻意限制 Claude Code 不重寫核心,也不把成功編譯當成終點:
先探索專案並提出計畫,不要立刻改檔。
目標:保留現有 CLI 與測試,新增 macOS menu-bar SwiftUI App。
邊界:
1. GUI 只能執行 bundled helper 的固定 snapshot --format json。
2. 禁止 /bin/sh -c、任意使用者命令與 inherited PATH。
3. stdout/stderr 分流;保留 exit code;加入 timeout 與 output cap。
4. UI 必須有 idle/loading/ready/empty/failure 狀態。
5. macOS 13 target,使用 ObservableObject;必要的 AppKit 放在單一 boundary。
驗收:
- CLI 與 GUI 共用同一組 JSON fixtures。
- 測 missing executable、non-zero exit、invalid JSON、timeout、oversized output。
- xcodebuild build/test、鍵盤操作、VoiceOver、dark mode、clean reinstall。
- 未完成任何一項就列為 blocker,不得宣告完成。
把這類可重跑規範留在 repo,而不是只存在聊天紀錄。若你常把規則、Skills 與 Hooks 打包,可延伸閱讀 Cursor Plugin 的可驗收封裝方式;工具不同,版本化工作合約的概念相同。
步驟 3:用 MenuBarExtra 做薄殼,不把終端機塞進視窗
最小 SwiftUI shell 只需要一個狀態模型與一個 scene。.window style 適合放正常按鈕與狀態卡;若要隱藏 Dock 圖示,還要在 Info.plist 明確設定 LSUIElement=true,MenuBarExtra 不會自動替你做。
@main
struct CLIPulseApp: App {
@StateObject private var model = StatusModel()
var body: some Scene {
MenuBarExtra("CLI Pulse", systemImage: model.symbolName) {
StatusPanel(model: model)
}
.menuBarExtraStyle(.window)
}
}
介面不要直接理解 CLI 的人類可讀文字。讓 StatusModel 把資料轉成 idle / loading / ready / empty / failure 五種狀態,View 只渲染狀態。這樣 CLI 格式變動時,你會在 decoder 或 fixture test 看到明確失敗,而不是得到一張看似正常、數字卻錯的卡片。
步驟 4:用 Process 接回 helper,避開 shell 注入與 PATH 陷阱
Apple 的 Foundation Process 會把 arguments 直接放進 argv,不會做 shell expansion,因此不要先把參數拼成一串字,也不要用 /bin/sh -c。內嵌 helper 時,用 bundle 解析絕對位置:
guard let helper = Bundle.main.url(
forAuxiliaryExecutable: "ReadOnlyCore"
) else { throw CLIError.missingExecutable }
let process = Process()
process.executableURL = helper
process.arguments = ["snapshot", "--format", "json"]
process.currentDirectoryURL = URL(fileURLWithPath: "/", isDirectory: true)
process.environment = ["LANG": "en_US.UTF-8", "LC_ALL": "en_US.UTF-8"]
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
正式 wrapper 還要在背景執行、同時 drain stdout 與 stderr、限制總輸出、設定 timeout/cancel,最後依序驗證 exit code、UTF-8、JSON schema 與欄位語意。不要在 MainActor 上同步 waitUntilExit();單純包進 Task {} 也可能繼承 MainActor。程序環境則應採 allowlist,避免把 token、shell function 或不受控 PATH 一起帶進 App。
步驟 5:補齊錯誤、鍵盤、VoiceOver 與 AppKit 邊界
漂亮的 ready state 只占五分之一。至少把下列狀況畫成可辨識、可恢復的介面:尚未執行、讀取中、無資料、成功、失敗。錯誤訊息不能只靠紅色;重新整理要有可見按鈕與 Command-R,退出可用 Command-Q,icon-only control 要加 accessibility label、value 或 hint。用 VoiceOver 實際完成任務,而不是只看元件有沒有標籤;macOS 可用 Command-F5 開啟 VoiceOver,並用 Xcode 的 Accessibility Inspector 稽核。
「UI 可被 VoiceOver 操作」和「App 需要 macOS 的 Accessibility 權限」是兩件事。當 App 透過 Accessibility API 存取或控制其他 App、系統 UI 時,才進入相關 TCC 授權議題;單純顯示 MenuBarExtra、啟動自己的 helper,不能據此宣稱需要這項權限。AppKit 也應集中在一個 boundary,處理 activation policy、測試視窗、quit 或必要的公告,不要讓整個 View tree 依賴它。
Claude Code SwiftUI 步驟 6:用四層驗收阻止「能開」冒充完成
驗收順序應由內往外。第一層是核心正確性:CLI 與 GUI 吃同一組 JSON fixtures。第二層是程序失敗:測 binary 不見、permission/non-zero、壞 JSON、壞 payload、timeout、超大輸出。第三層才是畫面與操作:固定 window size、golden screenshot、鍵盤腳本、深色模式、VoiceOver。第四層是交付:archive、簽章、乾淨重裝、啟動、讀 crash report。

為確認流程不是紙上談兵,本篇另建了最小 proof:在 macOS 26.1、Xcode 26.3、Swift 6.2.4(Swift 5 language mode、package deployment target macOS 14)下,兩個 Xcode scheme 成功建置,8 個程序與 payload 測試通過,ad-hoc nested signing 與 codesign --verify --deep --strict 通過,App 也被重新複製到獨立 temp install 目錄後啟動,讀到 bundled CLI 的 ready state。這不是乾淨機器或正式分發驗證,只證明最小架構可行;前述 macOS 13 寫法有 Apple API 文件支持,但沒有在這次本機 proof 執行。自動 screenshot 無法產生預期圖片,後續鍵盤 artifacts 也沒有出現;VoiceOver automation 則尚未實作或執行。這三項仍是 blocker,不能被「build 成功」抵銷。這種雙重驗收思路,也可參考 Claude Code OfficeCLI 的資料與視覺驗收。
打包分流:自用、Developer ID、Mac App Store 不同路
ad-hoc signing 不構成受 Gatekeeper 信任、可公證的 Developer ID 發佈。走 Developer ID 直接分發時,需要合適的 Developer ID 憑證、Hardened Runtime 與 notarization;notarytool 上傳的是 ZIP、DMG 或 flat PKG,不是裸 .app。ZIP 本身不能 staple,應 staple App/DMG/PKG 後再重新封裝。走 Mac App Store 則必須處理 App Sandbox;內嵌 helper 可以存在,但會繼承宿主 sandbox,不能靠啟動 shell 繞過權限。
下方是 Developer ID 路徑的順序範本,不是所有專案都能原封不動執行。先在 Xcode 設定實際 Team、certificate 與 signing,再建立有效的 ExportOptions.plist;本機 Xcode 26.3 顯示的 Developer ID export method 是 developer-id,其他鍵值要以你的專案與當版 xcodebuild -help 為準。
xcodebuild -project CLIPulse.xcodeproj -scheme CLIPulse \
-configuration Release -archivePath .build/CLIPulse.xcarchive archive
xcodebuild -exportArchive \
-archivePath .build/CLIPulse.xcarchive \
-exportPath .build/export \
-exportOptionsPlist ExportOptions.plist
ditto -c -k --keepParent .build/export/CLIPulse.app .build/CLIPulse.zip
xcrun notarytool submit .build/CLIPulse.zip \
--keychain-profile AC_NOTARY --wait
xcrun stapler staple .build/export/CLIPulse.app
xcrun stapler validate .build/export/CLIPulse.app
ditto -c -k --keepParent .build/export/CLIPulse.app \
.build/CLIPulse-notarized.zip
依賴任意 Homebrew binary、任意檔案路徑或廣泛系統權限的 wrapper,通常需要重新設計 helper 邊界,或改走適合的直接分發方案。先決定 distribution,再決定 sandbox 與 entitlements;不要反過來讓 Claude Code 猜。
CLI、TUI 還是原生 App?用工作情境決定
- 保留純 CLI:主要在 SSH、CI、腳本、低頻寬或跨平台環境工作。
- 選 TUI:需要高密度鍵盤操作,但仍要留在 terminal session、tmux 或遠端主機。
- 加 menu-bar App:任務是 glanceable status、少量固定操作、通知或快速切換,且主要使用者在 macOS 桌面。
- 做完整視窗 App:需要多步驟導覽、文件編輯、複雜拖放或大量設定;不要把所有流程硬塞進 menu bar。
AI 降低的是探索與樣板成本,不會自動消除 correctness、無障礙、簽章、公證、更新與維護成本。保留 CLI,等於同時保留一條可測、可回退、可遠端操作的 escape hatch。
Claude Code SwiftUI 常見問題
1. 一定要重寫原本 CLI 嗎?
不一定。能安全抽 library 就共用核心;否則先把原 binary 當 bundled helper,保留既有 contract,再逐步重構。
2. MenuBarExtra 最低支援哪個 macOS?
macOS 13。更早系統需改用 AppKit 的 NSStatusBar;若使用 Observation 的 @Observable,則要注意 macOS 14 的部署下限。
3. 可以直接呼叫使用者安裝的 Homebrew CLI 嗎?
可以設計,但 PATH、CPU 架構、版本、簽章與 sandbox 都會變成變數。新手專案優先內嵌已簽章 helper,或讓使用者明確選定並驗證路徑。
4. 用 Process 就沒有 injection 風險嗎?
不是。Process 避開 shell expansion,但你仍要固定 executable、驗證 arguments、限制 environment、輸出大小、timeout 與可存取資料。
5. Screenshot test 能證明 CLI 結果正確嗎?
不能。Screenshot 只驗視覺回歸;數值與狀態要靠 CLI/GUI 共用 fixtures、schema 與 semantic assertions。
6. Menu-bar App 一定要 Accessibility 權限嗎?
不一定。讓 UI 支援 VoiceOver 不等於要求 TCC Accessibility 權限;只有實際用相關 API 存取或控制其他 App、系統 UI 時,才依功能判定。
7. Build 成功後可以直接給別人下載嗎?
不是。Build 成功本身不代表適合放心公開發佈;正常 Developer ID 路徑還要完成 release archive、正式簽章、notarization 與 staple。乾淨機器安裝、更新與回復策略則是交付 QA,不要混成「編譯通過」的一部分。
8. 這套方法也適合 Windows 或 Linux 嗎?
架構原則可以參考,但 SwiftUI、MenuBarExtra、App Sandbox 與 notarization 都是 Apple 平台細節,不能直接外推到 WinUI、GTK 或跨平台產品。
給新手的 6 個帶走重點
- 先選低風險、讀取型、輸出可固定的 CLI。
- 保留 CLI contract,新增版本化 JSON 邊界。
- SwiftUI 只渲染有限狀態,不解析人類文字。
- Process 使用固定 helper、靜態參數與最小 environment。
- build、資料、錯誤、無障礙、重裝要分層驗收。
- 分發方式先決定,簽章、sandbox 與公證才不會做到一半重來。
接著閱讀
左右滑動查看更多推薦
結語:先包一個狀態,不要重寫整個工具
最好的第一步不是叫 Claude Code「把 CLI 變成漂亮 App」,而是挑一個穩定的 status --json,做出只會顯示、重新整理與報錯的 menu-bar shell。等同一組 fixtures 能同時證明 CLI 與 GUI 正確,再加入更多操作。這樣 AI 省下的是介面原型時間,而不是把工程風險藏進更漂亮的視窗。
想繼續拆解 AI 工具與開發方法,可以回到 AlphaLab AI 專區;需要依序練習的完整路線,則可查看 AlphaLab 線上課程。






