
hermes-swift-mac-app
Build and configure the native macOS desktop app wrapper for Hermes Web UI with Swift and WKWebView
- Installs
- 144
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/reason-machines/hermes-skills --skill hermes-swift-mac-app- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads hermes-swift-mac-app automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Hermes Swift Mac App Skill
Skill by ara.so — Hermes Skills collection
Overview
Hermes Agent is a native macOS desktop wrapper for Hermes Web UI. Built with Swift and WKWebView, it provides a standalone Mac app experience with no Electron or heavy dependencies. The app supports both local Hermes instances and remote servers via SSH tunneling.
Key features:
- Native macOS app using WKWebView (requires macOS 12+)
- Direct connection mode for local Hermes Web UI
- SSH tunnel mode for remote servers
- Clipboard integration (text and images)
- System notifications for AI responses
- Auto-update via Sparkle framework
- Signed and notarized for Gatekeeper
Installation
For End Users
Download the latest DMG from releases:
# Visit https://github.com/hermes-webui/hermes-swift-mac/releases
# Download Hermes-Agent-vX.X.X.dmg
# Open DMG and drag to Applications folder
For Developers - Build from Source
# Install Xcode Command Line Tools if needed
xcode-select --install
# Clone and build
git clone https://github.com/hermes-webui/hermes-swift-mac.git
cd hermes-swift-mac
./build.sh
The build.sh script:
- Compiles the Swift code via
swift build -c release - Creates the
.appbundle structure - Converts
icon.pngtoAppIcon.icns - Copies binaries and resources
- Installs to
/Applications/Hermes Agent.app
Project Structure
Sources/HermesAgent/
├── main.swift # Entry point, signal handling
├── AppDelegate.swift # App lifecycle, menu bar, Sparkle updater
├── BrowserWindowController.swift # WKWebView window, clipboard, notifications
├── TunnelManager.swift # SSH tunnel management
├── PreferencesWindowController.swift # Settings UI
└── SplashWindowController.swift # Launch splash screen
Package.swift # SPM manifest
build.sh # Build and install script
scripts/release.sh # Release automation
Configuration Modes
Direct Mode (Local Hermes)
For Hermes Web UI running on the same Mac:
Default settings:
- Target URL:
http://localhost:8787 - Connection Mode: Direct
// In PreferencesWindowController.swift
// Default URL stored in UserDefaults
UserDefaults.standard.string(forKey: "targetURL") ?? "http://localhost:8787"
SSH Tunnel Mode (Remote Hermes)
For Hermes Web UI on a remote server:
Required settings:
- Connection Mode: SSH Tunnel
- Username: SSH user on remote server
- Host: Server hostname or IP
- Local Port: Port on Mac (default 8787)
- Remote Port: Port where Hermes runs on server (default 8787)
SSH requirements:
- Key-based authentication must work (
ssh user@hostwithout password) ~/.ssh/known_hostsfile accessible- No password authentication support
// TunnelManager.swift constructs SSH command:
ssh -o StrictHostKeyChecking=accept-new \
-o ExitOnForwardFailure=yes \
-N -L \(localPort):localhost:\(remotePort) \
\(username)@\(host)
Code Examples
Reading User Preferences
// From PreferencesWindowController.swift
let defaults = UserDefaults.standard
// Get connection mode
let mode = defaults.string(forKey: "connectionMode") ?? "direct"
// Get target URL
let targetURL = defaults.string(forKey: "targetURL") ?? "http://localhost:8787"
// Get SSH settings (tunnel mode)
let sshUsername = defaults.string(forKey: "sshUsername") ?? ""
let sshHost = defaults.string(forKey: "sshHost") ?? ""
let localPort = defaults.integer(forKey: "localPort")
let remotePort = defaults.integer(forKey: "remotePort")
Starting SSH Tunnel
// From TunnelManager.swift
class TunnelManager {
private var sshProcess: Process?
func startTunnel(username: String, host: String,
localPort: Int, remotePort: Int) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
process.arguments = [
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ExitOnForwardFailure=yes",
"-N",
"-L", "\(localPort):localhost:\(remotePort)",
"\(username)@\(host)"
]
try? process.run()
self.sshProcess = process
// Monitor tunnel health
monitorTunnel()
}
func stopTunnel() {
sshProcess?.terminate()
sshProcess = nil
}
}
Testing Connection
// From PreferencesWindowController.swift
func testConnection(url: String, completion: @escaping (Bool) -> Void) {
guard let testURL = URL(string: url) else {
completion(false)
return
}
var request = URLRequest(url: testURL)
request.timeoutInterval = 5.0
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
completion(true)
} else {
completion(false)
}
}
}.resume()
}
Clipboard Integration
// From BrowserWindowController.swift
// Paste handler for text and images
@objc func handlePaste(_ sender: Any?) {
let pasteboard = NSPasteboard.general
if let image = pasteboard.readObjects(forClasses: [NSImage.self])?.first as? NSImage,
let data = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: data),
let pngData = bitmap.representation(using: .png, properties: [:]) {
let base64 = pngData.base64EncodedString()
let js = "window.pasteImage('data:image/png;base64,\(base64)');"
webView.evaluateJavaScript(js)
} else if let string = pasteboard.string(forType: .string) {
let js = "window.pasteText(\(jsonEscape(string)));"
webView.evaluateJavaScript(js)
}
}
System Notifications
// From BrowserWindowController.swift
// Show notification when AI response completes
func showNotification(title: String, body: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: nil
)
UNUserNotificationCenter.current().add(request)
}
Auto-Update with Sparkle
// From AppDelegate.swift
import Sparkle
class AppDelegate: NSObject, NSApplicationDelegate {
private var updaterController: SPUStandardUpdaterController?
func applicationDidFinishLaunching(_ notification: Notification) {
// Initialize Sparkle updater
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
// Check for updates on launch
updaterController?.updater.checkForUpdatesInBackground()
}
@objc func checkForUpdates(_ sender: Any?) {
updaterController?.updater.checkForUpdates()
}
}
Building and Running
Development Build
# Build in debug mode
swift build
# Run directly
swift run
# Run tests
swift test
Release Build
# Build release binary
swift build -c release
# Or use build script (compiles + installs)
./build.sh
The build script handles:
- Release compilation
- App bundle creation at
HermesAgent.app/ - Icon conversion (PNG → ICNS)
- Binary signing (if configured)
- Installation to
/Applications/
Icon Generation
# Convert PNG to ICNS (done by build.sh)
sips -s format icns icon.png --out AppIcon.icns
# Or manually with iconutil
mkdir icon.iconset
sips -z 16 16 icon.png --out icon.iconset/icon_16x16.png
sips -z 32 32 icon.png --out icon.iconset/icon_16x16@2x.png
sips -z 32 32 icon.png --out icon.iconset/icon_32x32.png
sips -z 64 64 icon.png --out icon.iconset/icon_32x32@2x.png
sips -z 128 128 icon.png --out icon.iconset/icon_128x128.png
sips -z 256 256 icon.png --out icon.iconset/icon_128x128@2x.png
sips -z 256 256 icon.png --out icon.iconset/icon_256x256.png
sips -z 512 512 icon.png --out icon.iconset/icon_256x256@2x.png
sips -z 512 512 icon.png --out icon.iconset/icon_512x512.png
sips -z 1024 1024 icon.png --out icon.iconset/icon_512x512@2x.png
iconutil -c icns icon.iconset
Releasing
Create a Release
# Use release script (pushes main, then tag separately)
scripts/release.sh v1.0.9
Why separate pushes? GitHub sometimes drops workflow triggers when commit and tag are pushed together. The script:
- Pushes
mainbranch - Waits briefly
- Pushes the tag separately
This ensures the GitHub Actions workflow fires reliably.
Manual Release Process
# Tag the release
git tag v1.0.9
git push origin main
git push origin v1.0.9
# If workflow doesn't trigger, run manually:
# Actions → Build and Release macOS App → Run workflow → enter tag
GitHub Actions Workflow
The .github/workflows/build-release.yml (not shown in README but typical) would:
- Build the app on
macos-latest - Sign with Developer ID certificate (from secrets)
- Notarize with Apple (requires Apple ID credentials)
- Create DMG with
create-dmgorhdiutil - Upload DMG as release asset
Common Patterns
Checking Tunnel Status
// From TunnelManager.swift
func isTunnelRunning() -> Bool {
guard let process = sshProcess else { return false }
return process.isRunning
}
func monitorTunnel() {
// Check if local port is accessible
let socket = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
0, nil, nil)
// ... probe localhost:localPort
}
Handling WebView Navigation
// From BrowserWindowController.swift
extension BrowserWindowController: WKNavigationDelegate {
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// Open external links in Safari
if let url = navigationAction.request.url,
navigationAction.navigationType == .linkActivated {
NSWorkspace.shared.open(url)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
}
Global Keyboard Shortcut
// From AppDelegate.swift
func applicationDidFinishLaunching(_ notification: Notification) {
// Register ⌘⇧H to bring app forward
NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
if event.modifierFlags.contains([.command, .shift]),
event.charactersIgnoringModifiers == "h" {
NSApp.activate(ignoringOtherApps: true)
}
}
}
Troubleshooting
Connection Error on Launch
Symptom: Blank page or "Unable to connect" error
Solutions:
- Ensure Hermes Web UI is running:
cd ~/hermes-webui-public bash start.sh - Check Target URL in Preferences (⌘,)
- Use Test Connection button to verify
- Check Console.app for errors from "Hermes Agent"
SSH Tunnel Fails Immediately
Symptom: Status shows "Disconnected" right after connecting
Solutions:
- Test SSH key auth works:
ssh user@your-server # Should connect without password - Set up SSH keys if needed:
ssh-keygen -t ed25519 ssh-copy-id user@your-server - Verify remote port is correct (where Hermes actually runs)
- Check
~/.ssh/known_hostsisn't corrupted
Voice Input Not Working
Symptom: Microphone button doesn't respond
Solutions:
- Grant microphone permission:
- System Settings → Privacy & Security → Microphone
- Enable "Hermes Agent"
- Restart app after granting permission
- Check Info.plist includes
NSMicrophoneUsageDescription
Gatekeeper Blocks App
Symptom: "App can't be opened because it is from an unidentified developer"
Solutions:
- Download latest release (v1.0.4+) — these are signed and notarized
- If building from source without signing:
xattr -cr /Applications/Hermes\ Agent.app - Right-click app → Open (first time only)
Blurry App Icon
Symptom: Icon looks pixelated in Dock after building
Solution:
# Refresh icon cache
killall Dock
Port Already in Use
Symptom: "Port forwarding failed" when starting tunnel
Solutions:
# Find what's using the port
lsof -i :8787
# Kill the process or change Local Port in Preferences
Environment Variables
The app doesn't use environment variables directly, but for development:
# Enable WebKit debug logging
export WEBKIT_DEBUG=1
# Sparkle update feed (set in Info.plist normally)
export SPARKLE_APPCAST_URL=https://example.com/appcast.xml
Testing
# Run all tests
swift test
# Run specific test
swift test --filter TunnelManagerTests
# Run with verbose output
swift test --verbose
Example Test
// From Tests/HermesAgentTests/TunnelManagerTests.swift
import XCTest
@testable import HermesAgent
final class TunnelManagerTests: XCTestCase {
func testTunnelCreation() {
let manager = TunnelManager()
XCTAssertFalse(manager.isTunnelRunning())
manager.startTunnel(
username: "test",
host: "localhost",
localPort: 8787,
remotePort: 8787
)
// Wait briefly for process to start
sleep(1)
XCTAssertTrue(manager.isTunnelRunning())
manager.stopTunnel()
XCTAssertFalse(manager.isTunnelRunning())
}
}
Dependencies
Managed via Swift Package Manager in Package.swift:
// Package.swift
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.3.0")
],
targets: [
.executableTarget(
name: "HermesAgent",
dependencies: [
.product(name: "Sparkle", package: "Sparkle")
]
)
]
Runtime dependencies:
- macOS 12 (Monterey) or later
- Xcode Command Line Tools (for building)
- SSH (provided by macOS)
No external dependencies for users — fully self-contained app.
FAQ
What does the hermes-swift-mac-app skill do?
Build and configure the native macOS desktop app wrapper for Hermes Web UI with Swift and WKWebView
How do I install hermes-swift-mac-app?
Run npx skills add https://github.com/reason-machines/hermes-skills --skill hermes-swift-mac-app in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/hermes-swift-mac-app/ (personal) or .claude/skills/hermes-swift-mac-app/ (project).
Which AI agents can use hermes-swift-mac-app?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is hermes-swift-mac-app safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains hermes-swift-mac-app?
It is published by reason-machines in reason-machines/hermes-skills. It has 144 recorded installs.
More from reason-machines/hermes-skills
hermes-agent-self-evolution
reason-machines/hermes-skills
Evolutionary self-improvement for Hermes Agent using DSPy + GEPA to optimize skills, prompts, and code
hermes-webui-agent
reason-machines/hermes-skills
Expert in deploying, configuring, and using Hermes WebUI—a web interface for Hermes Agent with persistent memory, scheduled jobs, and multi-platform messaging.
awesome-hermes-agent-ecosystem
reason-machines/hermes-skills
Open the skill to read its SKILL.md instructions.
hermes-agent-framework
reason-machines/hermes-skills
Hermes Agent framework by Nous Research - self-improving AI agent with built-in learning loop, three-layer memory, and automatic skill evolution
hermes-workspace-ai-agent-ui
reason-machines/hermes-skills
Native web workspace for Hermes Agent with chat, terminal, memory, skills, inspector, and multi-agent orchestration
hermes-web-ui-dashboard
reason-machines/hermes-skills
Web dashboard for managing Hermes Agent multi-platform AI chat sessions, analytics, scheduled jobs, and platform channels
Popular mobile skills
design-mobile-apps
designed-by-ai/skills
Design mobile app screens with Sleek, edit Sleek projects, and implement their designs in React Native or HTML.
ios-design
uizze.sh
Design and review iOS UI/UX for iPhone apps in SwiftUI, UIKit, or React Native. Use for mobile app design, screen redesigns, navigation flows, typography, forms, keyboard and safe-area layout, and accessibility checks. Apply Apple Human Interface Guidelines and consult real product references through the optional Uizze connection.
vercel-react-native-skills
vercel-labs/agent-skills
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
animate-expo
emilkowalski/skills
Build animations in React Native and Expo, making the decisions in the order that determines whether they feel right — should it animate, which thread it runs on, which properties, spring or timing, how the gesture hands off, how it degrades. Writes the implementation with Reanimated, Gesture Handler, Expo Router and expo-haptics. Use when animating anything in an Expo app, adding gestures, sheets, screen transitions, press feedback or haptics, or fixing motion that stutters on device. For web animation use animate.
xcode-project-setup
firebase/agent-skills
Safely modifies Xcode projects (.pbxproj) to add Swift Packages and link files. Use this skill whenever an iOS project needs dependencies installed (e.g. Firebase, Alamofire).
firebase-remote-config-basics
firebase/agent-skills
Manages Firebase Remote Config templates, feature flags, loading strategies, and SDKs (Android, iOS). Use when downloading/deploying remoteconfig JSON templates, managing version history/feature flags, setting in-app defaults, fetchAndActivate(), real-time listeners, or SDK setup. Don't use for Firebase Hosting, Auth, Firestore, Data Connect, Crashlytics, or App Hosting.