Trending-skills hermes-desktop-macos

```markdown

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.md
source 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

RequirementDetail
macOS14 Sonoma or newer
SSH authKey-based, no interactive prompts from this Mac
Host keyAlready 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

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 →

hermes-pi
. Leave Host/User/Port blank.

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:

  1. SSH target is reachable
  2. Authentication completes without prompts
  3. python3
    exists in the remote SSH environment

App Sections

Overview

Displays on first connect:

  • Remote
    $HOME
    path
  • Hermes root (
    ~/.hermes
    )
  • Which canonical files are present
  • Session source (
    state.db
    or
    .jsonl
    fallback)

Files — editing canonical Hermes files

The app provides conflict-aware editing for three files:

FilePurpose
~/.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:

  1. App records the remote file hash at load time
  2. Before saving, it re-fetches the remote hash
  3. If the file changed on the host since load → save is blocked, edits preserved
  4. Prompt: Reload from Remote → review diff → re-apply edits → save

Sessions

  • Source:
    ~/.hermes/state.db
    (SQLite, canonical)
  • Fallback:
    ~/.hermes/sessions/*.jsonl
    if SQLite unavailable
  • 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

/usr/bin/ssh
. If your setup requires the separate
tailscale ssh
command, use a ProxyCommand instead:

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"

# 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

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:

  1. Click Reload from Remote
  2. The editor will show the current remote content
  3. Re-apply your edits manually
  4. Save again

Tailscale: SSH works with
tailscale ssh
but not
ssh

Hermes 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

ConstraintReason
No gateway APIRemote host is the only source of truth
No local file mirrorAvoids stale state and sync conflicts
No remote helper serviceZero 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 saveProtects against agent overwriting newer remote state