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/hermes-desktop-macos" ~/.claude/skills/aradotso-trending-skills-hermes-desktop-macos && rm -rf "$T"
manifest:
skills/hermes-desktop-macos/SKILL.mdsource content
--- name: hermes-desktop-macos description: Native macOS SSH workspace for Hermes Agent — sessions, terminal, files, usage, and skills over direct SSH triggers: - set up Hermes Desktop on my Mac - connect Hermes Desktop to my SSH host - configure Hermes Desktop SSH connection - browse Hermes sessions in the desktop app - edit Hermes memory files from Mac - build Hermes Desktop from source - troubleshoot Hermes Desktop SSH connection - add a new connection profile in Hermes Desktop --- # Hermes Desktop macOS > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. Hermes Desktop is a native macOS app (Swift/SwiftUI, macOS 14+) that gives Hermes Agent users a first-class Mac workspace over direct SSH. It reads sessions from `~/.hermes/state.db`, edits canonical memory files (`USER.md`, `MEMORY.md`, `SOUL.md`), browses skills, shows token usage dashboards, and embeds a real SSH terminal — all without a gateway API, local file mirrors, or a remote helper service. --- ## Installation ### Option A: Download the release binary ```bash # 1. Download from GitHub Releases # 2. Unzip unzip HermesDesktop.app.zip # 3. Move to Applications mv HermesDesktop.app /Applications/ # 4. Verify the bundle signature codesign --verify --deep --strict /Applications/HermesDesktop.app # 5. Check SHA-256 matches the release asset shasum -a 256 HermesDesktop.app.zip
First-launch gatekeeper bypass (app is not yet notarized): Right-click → Open, or go to System Settings → Privacy & Security → Open Anyway.
Option B: Build from source
git clone https://github.com/dodo-reach/hermes-desktop cd hermes-desktop # Requires Xcode 15+ and macOS 14 SDK ./scripts/build-macos-app.sh # The built app lands in the repo root or DerivedData — check script output open HermesDesktop.app
Requirements
| Requirement | Detail |
|---|---|
| macOS | 14 Sonoma or newer |
| SSH auth | Key-based, no interactive prompts from this Mac |
| Host key | Already accepted once in Terminal |
| Remote | available in the SSH environment |
| Hermes data | present on the remote host |
Quick sanity check — if this works without prompts, the app will work:
ssh your-host echo ok # → ok
Connecting to a Hermes Host
Using an SSH alias (recommended)
Add an entry to
~/.ssh/config on your Mac:
Host hermes-pi HostName 192.168.1.42 User pi IdentityFile ~/.ssh/id_ed25519 Host hermes-vps HostName vps.example.com User deploy Port 2222 IdentityFile ~/.ssh/id_ed25519 Host hermes-local HostName localhost User alex
In the app: Connections → New Profile → SSH alias →
. Leave Host/User/Port blank.hermes-pi
Using explicit host details
In the app:
- Host or IP:
vps.example.com - User:
deploy - Port:
2222
Testing the connection
Click Test before Use Host. The preflight check verifies:
- SSH target is reachable
- Authentication completes without prompts
exists in the remote SSH environmentpython3
App Sections
Overview
Displays on first connect:
- Remote
path$HOME - Hermes root (
)~/.hermes - Which canonical files are present
- Session source (
orstate.db
fallback).jsonl
Files — editing canonical Hermes files
The app provides conflict-aware editing for three files:
| File | Purpose |
|---|---|
| User context fed to the agent |
| Agent working memory |
| Agent persona/soul definition |
Conflict protection flow:
- App records the remote file hash at load time
- Before saving, it re-fetches the remote hash
- If the file changed on the host since load → save is blocked, edits preserved
- Prompt: Reload from Remote → review diff → re-apply edits → save
Sessions
- Source:
(SQLite, canonical)~/.hermes/state.db - Fallback:
if SQLite unavailable~/.hermes/sessions/*.jsonl - Features: search, metadata display, remote deletion, refresh-on-entry
Usage
Aggregate token dashboard sourced from
~/.hermes/state.db:
- Total input / output tokens
- Top sessions by token count
- Per-model breakdowns
- Recent session trend
Skills
Recursive browser for
~/.hermes/skills/**/SKILL.md:
- Discovers all nested skill files
- Quick filter/search in the list
- Full markdown preview
Terminal
Embedded SSH terminal with tabs. Uses
/usr/bin/ssh — the same binary as Terminal.app. Connects to the same host as the active profile.
SSH Configuration Patterns
Key-based auth setup (if not already done)
# Generate a key on your Mac if needed ssh-keygen -t ed25519 -C "hermes-desktop" -f ~/.ssh/id_hermes # Copy public key to the remote host ssh-copy-id -i ~/.ssh/id_hermes.pub user@host # Or manually: cat ~/.ssh/id_hermes.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" # Test — must return without any prompt ssh -i ~/.ssh/id_hermes user@host echo ok
SSH config for common topologies
# Tailscale (MagicDNS hostname) Host hermes-tailscale HostName my-machine.tail1234.ts.net User alex IdentityFile ~/.ssh/id_ed25519 # Jump host / bastion Host hermes-behind-bastion HostName 10.0.0.5 User ubuntu ProxyJump bastion.example.com IdentityFile ~/.ssh/id_ed25519 # Same Mac via localhost Host hermes-local HostName localhost User alex IdentityFile ~/.ssh/id_ed25519 StrictHostKeyChecking no
Note: Hermes Desktop uses standard
. If your setup requires the separate/usr/bin/sshcommand, use a ProxyCommand instead:tailscale ssh
Host hermes-tailscale-cmd HostName my-machine User alex ProxyCommand tailscale ssh --nc %h %p
Building from Source — Key Files
hermes-desktop/ ├── scripts/ │ └── build-macos-app.sh # Universal (arm64 + x86_64) build script ├── HermesDesktop/ │ ├── App/ │ │ └── HermesDesktopApp.swift # SwiftUI @main entry point │ ├── Views/ │ │ ├── SessionsView.swift │ │ ├── FilesView.swift │ │ ├── UsageView.swift │ │ ├── SkillsView.swift │ │ └── TerminalView.swift │ ├── Models/ │ │ └── ConnectionProfile.swift │ └── SSH/ │ └── SSHClient.swift # Wraps /usr/bin/ssh process calls └── HermesDesktop.xcodeproj
Build script usage
# Full universal build ./scripts/build-macos-app.sh # Verify the result is a universal binary lipo -info HermesDesktop.app/Contents/MacOS/HermesDesktop # → Architectures in the fat file: arm64 x86_64
Xcode build (development)
# Debug build for current arch xcodebuild -scheme HermesDesktop -configuration Debug build # Run tests xcodebuild -scheme HermesDesktop test
Swift Patterns Used in the Codebase
Running SSH commands (non-interactive)
import Foundation func runSSHCommand( alias: String, command: String, completion: @escaping (Result<String, Error>) -> Void ) { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh") process.arguments = [ "-o", "BatchMode=yes", // never prompt interactively "-o", "ConnectTimeout=10", alias, command ] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe process.terminationHandler = { proc in let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" if proc.terminationStatus == 0 { completion(.success(output)) } else { completion(.failure(SSHError.nonZeroExit(output))) } } do { try process.run() } catch { completion(.failure(error)) } } // Usage runSSHCommand(alias: "hermes-pi", command: "python3 --version") { result in switch result { case .success(let version): print("python3 ok:", version) case .failure(let err): print("preflight failed:", err) } }
Reading a remote file with conflict detection
struct RemoteFile { let content: String let sha256: String // hash at load time } func fetchRemoteFile( alias: String, path: String, completion: @escaping (Result<RemoteFile, Error>) -> Void ) { // Fetch content and hash in one SSH call let cmd = "python3 -c \"" + "import hashlib, pathlib; " + "p = pathlib.Path('\(path)').expanduser(); " + "b = p.read_bytes(); " + "print(hashlib.sha256(b).hexdigest()); " + "print(b.decode())\"" runSSHCommand(alias: alias, command: cmd) { result in switch result { case .success(let output): let lines = output.components(separatedBy: "\n") guard let hash = lines.first else { completion(.failure(RemoteFileError.parseError)); return } let content = lines.dropFirst().joined(separator: "\n") completion(.success(RemoteFile(content: content, sha256: hash))) case .failure(let err): completion(.failure(err)) } } } func saveRemoteFile( alias: String, path: String, newContent: String, originalHash: String, completion: @escaping (Result<Void, Error>) -> Void ) { // Check current remote hash before writing let checkCmd = "python3 -c \"" + "import hashlib, pathlib; " + "p = pathlib.Path('\(path)').expanduser(); " + "print(hashlib.sha256(p.read_bytes()).hexdigest())\"" runSSHCommand(alias: alias, command: checkCmd) { result in switch result { case .success(let currentHash): if currentHash.trimmingCharacters(in: .whitespacesAndNewlines) != originalHash { completion(.failure(RemoteFileError.conflict)) return } // Safe to write — pipe content via stdin let writeCmd = "cat > \(path)" // ... write newContent via stdin to the process completion(.success(())) case .failure(let err): completion(.failure(err)) } } } enum RemoteFileError: Error { case parseError case conflict // remote changed since load — prompt user to reload }
SwiftUI connection profile model
import Foundation struct ConnectionProfile: Identifiable, Codable { var id: UUID = UUID() var name: String var sshAlias: String? // preferred: uses ~/.ssh/config var host: String? var user: String? var port: Int = 22 /// The argument passed to /usr/bin/ssh var sshTarget: String { if let alias = sshAlias, !alias.isEmpty { return alias } let u = user.map { "\($0)@" } ?? "" let p = port != 22 ? " -p \(port)" : "" return "\(u)\(host ?? "localhost")\(p)" } }
Troubleshooting
Test
fails: "Authentication failed"
Test# Verify key auth works without prompts ssh -o BatchMode=yes -o ConnectTimeout=10 your-host echo ok # If it asks for a password, set up key-based auth: ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host # Check the SSH agent has the key loaded ssh-add -l ssh-add ~/.ssh/id_ed25519
Test
fails: "python3 not found"
Test# On the remote host, check python3 path which python3 # If it's in a non-standard location like /usr/local/bin, ensure PATH is set # in ~/.bashrc or ~/.zshrc (non-interactive shells may have a minimal PATH) # Quick fix: symlink to a standard location sudo ln -s /usr/local/bin/python3 /usr/bin/python3
Test
fails: "Host key verification failed"
Test# Accept the host key interactively first ssh your-host # Type 'yes' when prompted # Or clear a stale entry ssh-keygen -R your-host ssh your-host # accept fresh key
Sessions view shows no sessions
The app tries
~/.hermes/state.db first. Check on the remote host:
ssh your-host "ls -lh ~/.hermes/state.db" # If missing, check for jsonl fallback: ssh your-host "ls ~/.hermes/sessions/*.jsonl | head -5"
Conflict error when saving a memory file
The remote file changed after you opened it. In the app:
- Click Reload from Remote
- The editor will show the current remote content
- Re-apply your edits manually
- Save again
Tailscale: SSH works with tailscale ssh
but not ssh
tailscale sshsshHermes Desktop uses
/usr/bin/ssh. Configure a ProxyCommand in ~/.ssh/config:
Host hermes-tailscale HostName my-machine User alex ProxyCommand /usr/local/bin/tailscale nc %h %p
Then use
hermes-tailscale as the SSH alias in the app.
Monitor live network activity
# Watch what the app connects to sudo nettop -p HermesDesktop # Or use lsof to see open connections lsof -i -n -P | grep HermesDesktop
Key Design Constraints
| Constraint | Reason |
|---|---|
| No gateway API | Remote host is the only source of truth |
| No local file mirror | Avoids stale state and sync conflicts |
| No remote helper service | Zero install footprint on the host |
Uses | Same path as Terminal — no novel auth surface |
on all non-terminal SSH calls | Prevents silent interactive prompt hangs |
| Conflict check before every save | Protects against agent overwriting newer remote state |