Skip to content

ripplethor/macFUSEGui

Repository files navigation

macfuseGui

macfuseGui is a macOS menu bar agent app for mounting remote directories with sshfs + macFUSE.

  • Website: https://www.macfusegui.app/
  • macOS 13+
  • Apple Silicon first (arm64), Intel supported (ARCH_OVERRIDE=x86_64)
  • Release flow defaults to dual-arch artifacts (ARCH_OVERRIDE=both)
  • NSStatusItem menu bar UX, no Dock icon (LSUIElement=true)
  • SwiftUI settings + AppKit menu bar integration

Key Features

  • Multiple remotes with per-remote connect/disconnect.
  • Three auth modes: SSH Private Key, System SSH, and Password.
  • Startup duplicate-instance guard (singleton lock + running-app check) to avoid duplicate menu bar icons.
  • Secure persistence:
    • Non-sensitive config in JSON.
    • Passwords in macOS Keychain.
  • Test Connection in add/edit (real temporary mount + unmount validation).
  • Per-remote startup toggle: Auto-connect on app launch.
  • Recovery handling for sleep/wake and network restoration.
  • Busy unmount diagnostics (shows blocking processes via lsof).
  • Open-in-editor plugin system:
    • Built-in editor plugins (VS Code, VSCodium, Cursor, Zed)
    • Preferred editor + fallback across active plugins
    • Real-time enable/disable in Settings
    • External plugin manifests from disk
  • Finder-style remote browser:
    • Sidebar (Favorites / Recents / Roots)
    • Breadcrumb navigation
    • Directories-only table view
    • Sticky cache with reconnect-state banners (no silent blank state)
  • Diagnostics snapshot + Copy Diagnostics menu action.

Dependency Install

brew install --cask macfuse
brew install sshfs-mac

Authentication Modes

macfuseGui currently supports three SSH auth modes:

  • SSH Private Key: requires an absolute private key path and appends IdentityFile=... to the sshfs invocation.
  • System SSH: uses your normal OpenSSH setup without appending an explicit key path. Use this for SSH agent, macOS agent-backed keys, ~/.ssh/config hosts, and Tailscale SSH-style setups.
  • Password: requires a stored password and pins the mount/test path to password or keyboard-interactive auth so a wrong password cannot silently succeed through key, agent, keychain, or other SSH fallback methods.

Notes:

  • Finder mounts and Test Connection support all three auth modes.
  • The built-in remote browser currently supports only Password and SSH Private Key because it uses libssh2. System SSH still works for Finder mounts and Test Connection.

Install via Homebrew (App)

brew tap ripplethor/macfusegui https://github.com/ripplethor/macfuseGUI
brew install --cask ripplethor/macfusegui/macfusegui

Expected sshfs search order:

  1. /opt/homebrew/bin/sshfs
  2. /usr/local/bin/sshfs
  3. sshfs from $PATH

Open-In-Editor Plugins

The menu popover provides:

  • Primary one-click action: Open in <Preferred Editor>
  • Explicit picker action: Open In…

Default behavior:

  • Built-ins shipped: vscode, vscodium, cursor, zed
  • Only vscode starts active by default
  • Preferred editor auto-rehomes to the next active plugin if needed
  • If all attempts fail, app falls back to Finder and records diagnostics
  • Each built-in plugin manifest is editor-specific (no cross-editor mixed attempts inside one plugin)
  • Built-in manifests live in codebase under:
    • macfuseGui/Resources/EditorPlugins/vscode/plugin.json
    • macfuseGui/Resources/EditorPlugins/vscodium/plugin.json
    • macfuseGui/Resources/EditorPlugins/cursor/plugin.json
    • macfuseGui/Resources/EditorPlugins/zed/plugin.json

Settings behavior:

  • Toggle plugins on/off in real time (no restart)
  • Select preferred editor from active plugins
  • Reload plugin manifests manually with Reload Plugins
  • Open dedicated Editor Plugins… window from Settings
  • Reveal plugin directory in Finder
  • Create a new plugin manifest from template (New Plugin JSON)
  • Edit manifest JSON inline for any selected plugin (Inline JSON Editor)
  • Remove external plugins directly from the plugin catalog (Trash)

External manifests:

  • Directory: ~/Library/Application Support/macfuseGui/editor-plugins
  • File type: *.json (one plugin per file)
  • The app auto-creates this folder on first load with:
    • README.md usage guide
    • examples/custom-editor.json.template
    • builtin-reference/*.json (reference definitions for shipped editors; not loaded as external plugins)
  • Security rules:
    • only /usr/bin/open and /usr/bin/env executables
    • launch attempt must include {folderPath} placeholder
    • command arrays only; no shell interpolation

Example manifest:

{
  "id": "windsurf",
  "displayName": "Windsurf",
  "priority": 50,
  "defaultEnabled": false,
  "launchAttempts": [
    {
      "label": "open app Windsurf",
      "executable": "/usr/bin/open",
      "arguments": ["-a", "Windsurf", "{folderPath}"],
      "timeoutSeconds": 3
    }
  ]
}

Build / Run / Clean

From repo root:

./scripts/build.sh
./scripts/run.sh
./scripts/clean.sh

build.sh includes a pre-step:

  • ARCH_OVERRIDE=<arch> ./scripts/build_libssh2.sh

Supported ARCH_OVERRIDE values:

  • arm64
  • x86_64
  • both
  • universal

Third-party output roots are arch-specific:

  • build/third_party/openssl-arm64, build/third_party/openssl-x86_64, build/third_party/openssl-universal
  • build/third_party/libssh2-arm64, build/third_party/libssh2-x86_64, build/third_party/libssh2-universal

App output paths:

  • Single-arch (arm64, x86_64, universal): build/macfuseGui.app
  • Dual-arch (both): build/macfuseGui-arm64.app and build/macfuseGui-x86_64.app

DerivedData roots:

  • build/DerivedData-arm64
  • build/DerivedData-x86_64
  • build/DerivedData-universal

Make Targets

make build
make run
make clean

Common Build Overrides

# Intel build
ARCH_OVERRIDE=x86_64 ./scripts/build.sh

# Build separate arm64 + x86_64 apps
ARCH_OVERRIDE=both ./scripts/build.sh

# Universal app build
ARCH_OVERRIDE=universal ./scripts/build.sh

# Release build
CONFIGURATION=Release ./scripts/build.sh

# Allow signing if needed
CODE_SIGNING_ALLOWED=YES ./scripts/build.sh

Release

# Default release mode is dual-arch (both)
./scripts/release.sh

# Verify dual-arch release actions without publishing
ARCH_OVERRIDE=both ./scripts/release.sh --dry-run

# Force a single-arch release if needed
ARCH_OVERRIDE=arm64 ./scripts/release.sh
ARCH_OVERRIDE=x86_64 ./scripts/release.sh

Xcode CLI Fallback

ARCH_OVERRIDE=arm64 ./scripts/build.sh
xcodebuild -project macfuseGui.xcodeproj -scheme macfuseGui -configuration Debug -derivedDataPath build/DerivedData build

Run Tests

ARCH_OVERRIDE=arm64 ./scripts/build.sh
xcodebuild -project macfuseGui.xcodeproj -scheme macfuseGui -configuration Debug -derivedDataPath build/DerivedData -destination 'platform=macOS,arch=arm64' test CODE_SIGNING_ALLOWED=NO

Reliability Gate

ARCH_OVERRIDE=arm64 ./scripts/build.sh
python3 scripts/audit_mount_calls.py && xcodebuild -project macfuseGui.xcodeproj -scheme macfuseGui -configuration Debug -derivedDataPath build/DerivedData -destination 'platform=macOS,arch=arm64' test CODE_SIGNING_ALLOWED=NO

VS Code

Included:

  • .vscode/tasks.json
  • .vscode/launch.json
  • .vscode/settings.json

Tasks:

  • build -> scripts/build.sh
  • run -> build + launch app
  • clean -> scripts/clean.sh
  • sourcekit-reset-cache -> scripts/reset_sourcekit_cache.sh

Debug launch:

  • Program: build/macfuseGui.app/Contents/MacOS/macfuseGui
  • Pre-launch task: build

Documentation

Security Notes

  • All external command execution uses Process with argument arrays.
  • No shell interpolation for user input.
  • Password mode uses a temporary SSH_ASKPASS helper script (0700 in a per-mount tempdir) plus a uniquely-named env var (MACFUSEGUI_ASKPASS_PASSWORD_<uuid>) that the script reads. The script and tempdir are deleted as soon as the connect call returns, but the env var is inherited by sshfs/ssh at fork/exec and therefore remains in those processes' environment for the entire lifetime of the mount — visible to other processes running as the same user (e.g. via ps eww on Linux, or KERN_PROCARGS2/sysctl on macOS). This is an inherent trade-off of the SSH_ASKPASS protocol; the password is never on the command line or in any persistent file.
  • System SSH mode intentionally does not append an explicit IdentityFile; it relies on normal OpenSSH config and agent resolution.
  • Password auth pinning is injected through ssh_command=/usr/bin/ssh -o ... because current Homebrew macOS sshfs builds reject raw PubkeyAuthentication=...-style mount options at the FUSE layer.
  • Passwords are never stored in JSON or logs.
  • KeychainService.readPassword trims leading/trailing whitespace on read — prevents silent auth failures from clipboard-pasted trailing newlines without altering the stored credential.
  • IPv6 host addresses are automatically bracketed ([::1]) in sshfs arguments; bare IPv6 input is also rejected at the validation layer.
  • Diagnostics redact sensitive content.

Browser Subsystem Notes

Browser internals are session-based (openSession / listDirectories / health / closeSession) with per-session health and sticky-cache behavior.

Current transport implementation uses native libssh2 SFTP through a C bridge (LibSSH2Bridge.c/.h) behind an internal transport abstraction.

Auth support:

  • Password and SSH Private Key are supported in the browser today.
  • System SSH is not supported in the browser yet; use Finder mounts or Test Connection for that mode.

Recovery contract:

  • Keepalive runs every 12s only while the browser session is idle.
  • Keepalive failures do not hard-close sessions; they schedule list-based recovery.
  • Recovery backoff: 0.2s, 0.8s, 2s, 5s.
  • Empty listings are confirmation-checked before being treated as truly empty.
  • Browser keeps last-good entries visible during transient failures.

Troubleshooting

Missing dependencies

brew install --cask macfuse
brew install sshfs-mac

xcodebuild first-launch issues

sudo xcodebuild -runFirstLaunch

VS Code SourceKit stale errors

./scripts/reset_sourcekit_cache.sh

Then in VS Code:

  1. Reload window
  2. Restart Swift LSP
  3. Re-index project

Mount/unmount issues

  • Use Copy Diagnostics from the app menu.
  • For busy unmount, diagnostics now include blocking process hints.

Duplicate menu icon or stale second app process

pkill -x macfuseGui
open -a /Applications/macfuseGui.app

If this still happens, check for multiple running processes:

pgrep -lf macfuseGui

Browser diagnostics interpretation

  • state=healthy with isConfirmedEmpty=true: path is reachable and truly has no subfolders.
  • state=reconnecting or state=degraded with fromCache=true: browser is showing last-good cached data while retrying.
  • lastSuccessAt and lastLatencyMs in Browser Sessions show the last confirmed successful list.
  • Repeated keepalive failed followed by recovery attempt means auto-recovery is active; use Retry now in the browser UI to force an immediate list call.

Mount concurrency proof logs (actor funnel check)

Use this during wake/reconnect testing:

log stream --style compact --predicate 'process == "macfuseGui"' \
  | rg 'Operation start remoteID=|Operation end remoteID=|mount call op=|actor enter op=|probe start op=sshfs-connect|probe end op=sshfs-connect'

Interpretation:

  • Healthy parallelism: different remotes show overlapping probe start/end op=sshfs-connect windows.
  • Funnel warning: one remote repeatedly waits with high queueDelayMs before actor enter.

License

This project is licensed under the GNU General Public License v3.0.

If you distribute a modified version, you must also provide the source under GPLv3.

See LICENSE.