From aradotso-trending-skills-37
Guides installation, SSH connection configuration, and usage of Hermes Desktop macOS app for native access to Hermes Agent sessions, memory files, terminal, skills, and token usage over direct SSH.
npx claudepluginhub joshuarweaver/cascade-ai-ml-agents-misc-1 --plugin aradotso-trending-skills-37This skill uses the workspace's default tool permissions.
```markdown
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
---
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.
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
| 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 | python3 available in the SSH environment |
| Hermes data | ~/.hermes/ present on the remote host |
Quick sanity check — if this works without prompts, the app will work:
ssh your-host echo ok
# → ok
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 → hermes-pi. Leave Host/User/Port blank.
In the app:
vps.example.comdeploy2222Click Test before Use Host. The preflight check verifies:
python3 exists in the remote SSH environmentDisplays on first connect:
$HOME path~/.hermes)state.db or .jsonl fallback)The app provides conflict-aware editing for three files:
| File | Purpose |
|---|---|
~/.hermes/memories/USER.md | User context fed to the agent |
~/.hermes/memories/MEMORY.md | Agent working memory |
~/.hermes/SOUL.md | Agent persona/soul definition |
Conflict protection flow:
~/.hermes/state.db (SQLite, canonical)~/.hermes/sessions/*.jsonl if SQLite unavailableAggregate token dashboard sourced from ~/.hermes/state.db:
Recursive browser for ~/.hermes/skills/**/SKILL.md:
Embedded SSH terminal with tabs. Uses /usr/bin/ssh — the same binary as Terminal.app. Connects to the same host as the active profile.
# 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
# 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
/usr/bin/ssh. If your setup requires the separatetailscale sshcommand, use a ProxyCommand instead:
Host hermes-tailscale-cmd
HostName my-machine
User alex
ProxyCommand tailscale ssh --nc %h %p
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
# 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
# Debug build for current arch
xcodebuild -scheme HermesDesktop -configuration Debug build
# Run tests
xcodebuild -scheme HermesDesktop test
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)
}
}
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
}
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)"
}
}
Test fails: "Authentication failed"# 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"# 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"# 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
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"
The remote file changed after you opened it. In the app:
tailscale ssh but not sshHermes 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.
# Watch what the app connects to
sudo nettop -p HermesDesktop
# Or use lsof to see open connections
lsof -i -n -P | grep HermesDesktop
| 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 /usr/bin/ssh | Same path as Terminal — no novel auth surface |
BatchMode=yes on all non-terminal SSH calls | Prevents silent interactive prompt hangs |
| Conflict check before every save | Protects against agent overwriting newer remote state |