From aradotso-trending-skills-37
Control Claude Code AI coding sessions from your Apple Watch — stream terminal output, approve permissions, and send voice commands from your wrist.
How this skill is triggered — by the user, by Claude, or both
Slash command
/aradotso-trending-skills-37:claude-watch-apple-watchThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```markdown
---
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
./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
cd skill/bridge
node server.js
Output:
╔═══════════════════════════════════════╗
║ AGENT WATCH BRIDGE ║
╠═══════════════════════════════════════╣
║ Pairing Code: 648505 ║
║ IP Address: 192.168.1.4 ║
║ Port: 7860 ║
╚═══════════════════════════════════════╝
cd ios/ClaudeWatch
xcodegen generate
open ClaudeWatch.xcodeproj
In Xcode:
ClaudeWatch and ClaudeWatchWatchClaudeWatch scheme → iPhoneClaudeWatchWatch scheme → Apple Watch| 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 |
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
// 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 }
// 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()
}
// 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)
}
}
// 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)
}
}
}
// 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() }
}
}
// 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 }
}
}
// 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)
}
}
// 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"
}
PORT=7860 node server.js # Override default port (tries 7860-7869)
# ios/ClaudeWatch/project.yml
targets:
ClaudeWatch:
type: application
platform: iOS
deploymentTarget: "17.0"
dependencies:
- target: ClaudeWatchWatch
ClaudeWatchWatch:
type: application
platform: watchOS
deploymentTarget: "10.0"
{
"event": "PostToolUse",
"tool_name": "Edit",
"tool_input": { "file_path": "src/index.ts", "old_string": "...", "new_string": "..." },
"tool_result": { "success": true }
}
{
"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).
# 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
# 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
Xcode: Product > Clean Build Folder (Cmd+Shift+Option+K)
Select scheme: ClaudeWatchWatch (not ClaudeWatch)
Deploy via paired iPhone destination if direct watch deployment fails
# 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
# 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)
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
# 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)
| Component | Minimum |
|---|---|
| macOS | 13.0+ |
| Node.js | 18+ |
| Xcode | 16+ |
| iOS | 17.0 |
| watchOS | 10.0 |
| Claude Code | 2.1+ |
npx claudepluginhub joshuarweaver/cascade-ai-ml-agents-misc-1 --plugin aradotso-trending-skills-37Deep expertise in Claude Code CLI configuration, hooks, MCPs, CLAUDE.md, sub-agents, permissions, and workflows. Activate for setup, optimization, or troubleshooting of Claude Code features.
Maximizes productivity with Anthropic's Claude Code CLI: shortcuts, hooks, MCPs, advanced configuration, workflows, CLAUDE.md, memory, sub-agents, permissions, and ecosystem integrations. Activate for configuring Claude Code, creating hooks, optimizing CLAUDE.md, using MCPs, resolving CLI errors, or advanced workflows.
Provides best practices for using Claude Code effectively, covering subagents, commands, skills, memory, hooks, MCP servers, and orchestration workflows. Activated by questions about Claude Code usage.