claude-watch-apple-watch
SkillAI & modelsControl 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the claude-watch-apple-watch skill
About this capability
Trending Claude Code skills
What this skill tells your AI
The instructions your AI receives, as published by aradotso/trending-skills in skills/claude-watch-apple-watch/SKILL.md and read by ahel’s review.
---
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:
ClaudeWatchandClaudeWatchWatch - Run
ClaudeWatchscheme → iPhone - Run
ClaudeWatchWatchscheme → Apple Watch
Step 5: Pair
- iPhone: Enter 6-digit code from bridge banner
- Watch: Auto-discovers via Bonjour; fallback: enter IP manually
Key Commands
| Command | Description |
|---|---|
node server.js | Start bridge (port 7860, auto-increments to 7869) |
./skill/setup-hooks.sh | Install Claude Code hooks globally |
./skill/setup-hooks.sh --remove | Remove hooks |
xcodegen generate | Regenerate Xcode project from project.yml |
curl http://127.0.0.1:7860/status | 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+ |
Signals
- GitHub stars
- 78
- Forks
- 13
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
claude-watch-apple-watch- Source
- github.com/aradotso/trending-skills