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) NSStatusItemmenu bar UX, no Dock icon (LSUIElement=true)- SwiftUI settings + AppKit menu bar integration
- Multiple remotes with per-remote connect/disconnect.
- 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
- Built-in editor plugins (
- 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 Diagnosticsmenu action.
brew install --cask macfuse
brew install gromgit/fuse/sshfs-macbrew tap ripplethor/macfusegui https://github.com/ripplethor/macfuseGUI
brew install --cask ripplethor/macfusegui/macfuseguiExpected sshfs search order:
/opt/homebrew/bin/sshfs/usr/local/bin/sshfssshfsfrom$PATH
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
vscodestarts 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.jsonmacfuseGui/Resources/EditorPlugins/vscodium/plugin.jsonmacfuseGui/Resources/EditorPlugins/cursor/plugin.jsonmacfuseGui/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.mdusage guideexamples/custom-editor.json.templatebuiltin-reference/*.json(reference definitions for shipped editors; not loaded as external plugins)
- Security rules:
- only
/usr/bin/openand/usr/bin/envexecutables - launch attempt must include
{folderPath}placeholder - command arrays only; no shell interpolation
- only
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
}
]
}From repo root:
./scripts/build.sh
./scripts/run.sh
./scripts/clean.shbuild.sh includes a pre-step:
ARCH_OVERRIDE=<arch> ./scripts/build_libssh2.sh
Supported ARCH_OVERRIDE values:
arm64x86_64bothuniversal
Third-party output roots are arch-specific:
build/third_party/openssl-arm64,build/third_party/openssl-x86_64,build/third_party/openssl-universalbuild/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.appandbuild/macfuseGui-x86_64.app
DerivedData roots:
build/DerivedData-arm64build/DerivedData-x86_64build/DerivedData-universal
make build
make run
make clean# 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# 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.shARCH_OVERRIDE=arm64 ./scripts/build.sh
xcodebuild -project macfuseGui.xcodeproj -scheme macfuseGui -configuration Debug -derivedDataPath build/DerivedData buildARCH_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=NOARCH_OVERRIDE=arm64 ./scripts/build.sh
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=NOIncluded:
.vscode/tasks.json.vscode/launch.json.vscode/settings.json
Tasks:
build->scripts/build.shrun-> build + launch appclean->scripts/clean.shsourcekit-reset-cache->scripts/reset_sourcekit_cache.sh
Debug launch:
- Program:
build/macfuseGui.app/Contents/MacOS/macfuseGui - Pre-launch task:
build
- New Contributor Guide: Start here if you want to modify the app.
- Architecture & Jargon Buster: Visual diagrams and plain-English explanations.
- Safe Change Rules (AGENTS.md): Critical rules for AI agents and developers.
- Commit & Push Workflow (COMMIT_WORKFLOW.md): Commit format, changelog prefix rules, and release notes guidance.
- All external command execution uses
Processwith argument arrays. - No shell interpolation for user input.
- Password mode uses temporary
SSH_ASKPASShelper (0700) and ephemeral env vars. - Passwords are never stored in JSON or logs.
KeychainService.readPasswordtrims 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 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.
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.
brew install --cask macfuse
brew install gromgit/fuse/sshfs-macsudo xcodebuild -runFirstLaunch./scripts/reset_sourcekit_cache.shThen in VS Code:
- Reload window
- Restart Swift LSP
- Re-index project
- Use
Copy Diagnosticsfrom the app menu. - For busy unmount, diagnostics now include blocking process hints.
pkill -x macfuseGui
open -a /Applications/macfuseGui.appIf this still happens, check for multiple running processes:
pgrep -lf macfuseGuistate=healthywithisConfirmedEmpty=true: path is reachable and truly has no subfolders.state=reconnectingorstate=degradedwithfromCache=true: browser is showing last-good cached data while retrying.lastSuccessAtandlastLatencyMsinBrowser Sessionsshow the last confirmed successful list.- Repeated
keepalive failedfollowed byrecovery attemptmeans auto-recovery is active; useRetry nowin the browser UI to force an immediate list call.
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-connectwindows. - Funnel warning: one remote repeatedly waits with high
queueDelayMsbeforeactor enter.
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.