install
source · Clone the upstream repo
git clone https://github.com/Aradotso/trending-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/claude-watch-apple-watch" ~/.claude/skills/aradotso-trending-skills-claude-watch-apple-watch && rm -rf "$T"
manifest:
skills/claude-watch-apple-watch/SKILL.mdsource content
--- name: claude-watch-apple-watch description: Control Claude Code AI coding sessions from your Apple Watch — stream terminal output, approve permissions, and send voice commands from your wrist triggers: - "set up claude watch on my apple watch" - "control claude code from my watch" - "stream claude terminal output to watch" - "approve claude permissions from apple watch" - "connect claude code to apple watch" - "set up agent watch bridge server" - "install claude code hooks for watch" - "voice commands to claude from wrist" --- # Claude Watch (Agent Watch) > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. Control Claude Code from your Apple Watch. Stream live terminal output, approve/deny permission prompts, answer dynamic questions, and send voice commands — all from your wrist. ## Architecture
Apple Watch <==WCSession==> iPhone <==HTTP/SSE==> Bridge Server (Mac) (SwiftUI) (Relay) (Node.js) | HTTP Hooks | PTY stdin v Claude Code Session
Three components: - **Bridge Server** — Node.js on Mac, receives Claude Code hooks, streams via SSE, blocks on permission requests - **iPhone App** — SwiftUI, discovers bridge via Bonjour, relays events to watch via WCSession - **watchOS App** — SwiftUI, connects directly to bridge over Wi-Fi, renders terminal + permissions ## Installation ### Prerequisites - macOS 13+, Node.js 18+, Xcode 16+ - Apple Watch (watchOS 10+) on **same Wi-Fi** as Mac - Claude Code CLI 2.1+ - Apple Watch: **Settings > Wi-Fi > your network > Private Wi-Fi Address → Off** ### Step 1: Install Bridge ```bash git clone https://github.com/shobhit99/claude-watch cd claude-watch/skill/bridge npm install
Step 2: Install Claude Code Hooks
./skill/setup-hooks.sh
This writes to
~/.claude/settings.json:
{ "hooks": { "PostToolUse": [ { "type": "http", "url": "http://127.0.0.1:7860/hook/post-tool-use" } ], "PreToolUse": [ { "type": "http", "url": "http://127.0.0.1:7860/hook/pre-tool-use" } ], "PermissionRequest": [ { "type": "http", "url": "http://127.0.0.1:7860/hook/permission-request", "timeout": 600 } ], "Stop": [ { "type": "http", "url": "http://127.0.0.1:7860/hook/stop" } ] } }
To remove hooks later:
./skill/setup-hooks.sh --remove
Step 3: Start Bridge Server
cd skill/bridge node server.js
Output:
╔═══════════════════════════════════════╗ ║ AGENT WATCH BRIDGE ║ ╠═══════════════════════════════════════╣ ║ Pairing Code: 648505 ║ ║ IP Address: 192.168.1.4 ║ ║ Port: 7860 ║ ╚═══════════════════════════════════════╝
Step 4: Build iOS + watchOS Apps
cd ios/ClaudeWatch xcodegen generate open ClaudeWatch.xcodeproj
In Xcode:
- Set Development Team on both targets:
andClaudeWatchClaudeWatchWatch - Run
scheme → iPhoneClaudeWatch - Run
scheme → Apple WatchClaudeWatchWatch
Step 5: Pair
- iPhone: Enter 6-digit code from bridge banner
- Watch: Auto-discovers via Bonjour; fallback: enter IP manually
Key Commands
| Command | Description |
|---|---|
| Start bridge (port 7860, auto-increments to 7869) |
| Install Claude Code hooks globally |
| Remove hooks |
| Regenerate Xcode project from |
| Check bridge health |
Bridge Server API
HTTP Endpoints
GET /status — Health check + connection counts GET /events — SSE stream (requires session token) POST /hook/post-tool-use — Claude tool use events (async) POST /hook/pre-tool-use — Claude pre-tool events (async) POST /hook/permission-request — Permission prompts (BLOCKING, 10min timeout) POST /hook/stop — Claude session ended (async) POST /approve — Submit permission decision from watch/phone POST /pair — Pair with 6-digit code, returns session token
SSE Event Types
// Terminal output event { "type": "tool-use", "tool": "Edit", // Read | Edit | Bash | Grep | Write "path": "src/index.ts", "summary": "Editing src/index.ts" } // Permission request (watch shows approval sheet) { "type": "permission-request", "id": "req_abc123", "question": "Do you want to edit this file?", "options": ["Yes", "Yes to all", "No"] } // AskUserQuestion (dynamic options) { "type": "permission-request", "id": "req_xyz456", "question": "Which approach should I take?", "options": [ { "label": "Refactor", "description": "Clean up existing code" }, { "label": "Rewrite", "description": "Start fresh" } ] } // Session ended { "type": "stop", "exitCode": 0 }
Pairing Flow
// iPhone BridgeClient.swift pattern func pair(code: String) async throws -> String { let response = try await post("/pair", body: ["code": code]) return response["sessionToken"] as! String } // All subsequent requests include session token func streamEvents(token: String) -> AsyncStream<SSEEvent> { let url = URL(string: "\(bridgeURL)/events?token=\(token)")! return SSEClient(url: url).stream() }
Swift Code Examples
Watch: Connecting to Bridge (Direct Wi-Fi)
// WatchBridgeClient.swift import Foundation class WatchBridgeClient: ObservableObject { private var sseTask: URLSessionDataTask? private let session = URLSession.shared func connect(to bridgeURL: URL, token: String) { let eventsURL = bridgeURL .appendingPathComponent("events") .appending(queryItems: [URLQueryItem(name: "token", value: token)]) var request = URLRequest(url: eventsURL) request.setValue("text/event-stream", forHTTPHeaderField: "Accept") sseTask = session.dataTask(with: request) { [weak self] data, _, _ in guard let data, let text = String(data: data, encoding: .utf8) else { return } self?.parseSSEEvents(text) } sseTask?.resume() } func submitApproval(bridgeURL: URL, token: String, requestID: String, decision: String) async throws { var request = URLRequest(url: bridgeURL.appendingPathComponent("approve")) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: [ "id": requestID, "decision": decision, "token": token ]) let (_, _) = try await URLSession.shared.data(for: request) } }
Watch: Permission Approval View
// ApprovalView.swift import SwiftUI import WatchKit struct ApprovalView: View { let request: ApprovalRequest let onDecision: (String) -> Void var body: some View { ScrollView { VStack(alignment: .leading, spacing: 8) { Text(request.question) .font(.headline) .padding(.bottom, 4) ForEach(request.options, id: \.self) { option in Button(action: { WKInterfaceDevice.current().play(.click) onDecision(option) }) { Text(option) .frame(maxWidth: .infinity) .padding(.vertical, 6) } .buttonStyle(.bordered) .tint(option.lowercased().contains("no") ? .red : .green) } } .padding() } .navigationTitle("Permission") .onAppear { WKInterfaceDevice.current().play(.notification) } } }
Watch: Voice Command Input
// VoiceInputView.swift import SwiftUI import WatchKit struct VoiceInputView: View { @StateObject private var speech = SpeechService() @Environment(\.dismiss) var dismiss let onCommand: (String) -> Void var body: some View { VStack(spacing: 12) { if speech.isRecording { Image(systemName: "waveform") .font(.largeTitle) .foregroundColor(.blue) .symbolEffect(.variableColor) Text("Listening...") .foregroundColor(.secondary) } else if let result = speech.transcription { Text(result) .multilineTextAlignment(.center) Button("Send") { onCommand(result) dismiss() } .buttonStyle(.borderedProminent) } } .onAppear { speech.startDictation() } } }
iPhone: Bonjour Discovery
// BonjourDiscovery.swift import Foundation class BonjourDiscovery: NSObject, ObservableObject, NetServiceBrowserDelegate, NetServiceDelegate { @Published var discoveredBridgeURL: URL? private let browser = NetServiceBrowser() private var resolving: NetService? func startDiscovery() { browser.delegate = self browser.searchForServices(ofType: "_agentwatch._tcp.", inDomain: "local.") } func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { resolving = service service.delegate = self service.resolve(withTimeout: 5) } func netServiceDidResolveAddress(_ sender: NetService) { guard let host = sender.hostName else { return } let url = URL(string: "http://\(host):\(sender.port)")! DispatchQueue.main.async { self.discoveredBridgeURL = url } } }
iPhone: Relay to Watch via WCSession
// RelayService.swift import WatchConnectivity class RelayService: NSObject, WCSessionDelegate { static let shared = RelayService() private let session = WCSession.default func setup() { guard WCSession.isSupported() else { return } session.delegate = self session.activate() } func relayEvent(_ event: [String: Any]) { guard session.isReachable else { // Fallback: transferUserInfo for non-urgent events session.transferUserInfo(event) return } session.sendMessage(event, replyHandler: nil) } func relayPermissionRequest(_ request: ApprovalRequest) { let payload: [String: Any] = [ "type": "permission-request", "id": request.id, "question": request.question, "options": request.options ] session.sendMessage(payload, replyHandler: nil) } }
Shared Models
// ApprovalRequest.swift struct ApprovalRequest: Identifiable, Codable { let id: String let question: String let options: [String] var timestamp: Date = .now } // TerminalLine.swift struct TerminalLine: Identifiable { let id = UUID() let tool: String // "Read" | "Edit" | "Bash" | "Grep" | "Write" let path: String? let summary: String let timestamp: Date var icon: String { switch tool { case "Read": return "doc.text" case "Edit": return "pencil" case "Bash": return "terminal" case "Grep": return "magnifyingglass" case "Write": return "square.and.pencil" default: return "bolt" } } } // WatchMessage.swift — shared over WCSession enum WatchMessageType: String, Codable { case terminalLine = "terminal-line" case permissionRequest = "permission-request" case sessionStopped = "session-stopped" case connectionStatus = "connection-status" }
Configuration
Bridge Server Environment
PORT=7860 node server.js # Override default port (tries 7860-7869)
project.yml (XcodeGen) Key Settings
# ios/ClaudeWatch/project.yml targets: ClaudeWatch: type: application platform: iOS deploymentTarget: "17.0" dependencies: - target: ClaudeWatchWatch ClaudeWatchWatch: type: application platform: watchOS deploymentTarget: "10.0"
Hook Event Payload Examples
PostToolUse (Edit file)
{ "event": "PostToolUse", "tool_name": "Edit", "tool_input": { "file_path": "src/index.ts", "old_string": "...", "new_string": "..." }, "tool_result": { "success": true } }
PermissionRequest (blocking)
{ "event": "PermissionRequest", "tool_name": "Bash", "tool_input": { "command": "rm -rf dist/" }, "question": "Allow running: rm -rf dist/?", "options": ["Yes", "Yes to all", "No"] }
Bridge blocks responding to this hook until user approves via watch/phone (up to 10 minutes).
Troubleshooting
"Bridge not found" on Watch
# Verify bridge is running curl http://127.0.0.1:7860/status # Check watch Wi-Fi — must match Mac's network # Disable Private Wi-Fi Address on watch: # Settings > Wi-Fi > [Network] > Private Wi-Fi Address → Off # Use manual IP entry in watch app if Bonjour fails
Permission prompts not appearing
# Verify hooks are installed cat ~/.claude/settings.json | grep -A 5 "PermissionRequest" # Watch bridge logs for hook receipt # Should see: "Hook: PermissionRequest received" # Check watch shows green status dot
"unsupported architecture" build error
Xcode: Product > Clean Build Folder (Cmd+Shift+Option+K) Select scheme: ClaudeWatchWatch (not ClaudeWatch) Deploy via paired iPhone destination if direct watch deployment fails
iPhone "Connection failed"
# Bridge must be on same LAN as iPhone curl http://192.168.1.4:7860/status # Use bridge's actual IP # Check firewall allows port 7860 sudo lsof -i :7860
Hooks fire but watch doesn't update
# Test SSE stream directly curl -N "http://127.0.0.1:7860/events?token=YOUR_SESSION_TOKEN" # Check WCSession is active on iPhone # iPhone app must be open or backgrounded (not force-quit)
Bridge exits immediately
The bridge waits for Claude Code hooks — it does not spawn Claude itself. Start Claude Code in a separate terminal; hooks forward events automatically:
# Terminal 1: Bridge node server.js # Terminal 2: Claude Code (hooks auto-fire) claude
Uninstall / Reset
# Remove Claude Code hooks ./skill/setup-hooks.sh --remove # Unpair iPhone: Settings > Forget Mac (in app) # Unpair Watch: Restart app (credentials clear when bridge restarts)
Requirements Summary
| Component | Minimum |
|---|---|
| macOS | 13.0+ |
| Node.js | 18+ |
| Xcode | 16+ |
| iOS | 17.0 |
| watchOS | 10.0 |
| Claude Code | 2.1+ |