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.
- Favorite remotes (star in the menu popover) pinned to the top of the list.
- Three auth modes:
SSH Private Key,System SSH, andPassword. - Optional jump host (
ProxyJump) for Finder mounts and Test Connection. - Per-remote cache mode:
Prioritize freshness over speed(default,nolocalcaches) or a faster cached mode for dev/code mounts. - 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)
- Host-key verification against
~/.ssh/known_hosts
- Diagnostics snapshot +
Copy Diagnosticsmenu action. - Localized UI: English, German, Spanish, French, Japanese, Korean, Brazilian Portuguese, Simplified Chinese.
brew install --cask macfuse
brew install sshfs-macmacfuseGui currently supports three SSH auth modes:
SSH Private Key: requires an absolute private key path and appendsIdentityFile=...to thesshfsinvocation.System SSH: uses your normal OpenSSH setup without appending an explicit key path. Use this for SSH agent, macOS agent-backed keys,~/.ssh/confighosts, 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 Connectionsupport all three auth modes. - The built-in remote browser currently supports only
PasswordandSSH Private Keybecause it uses libssh2.System SSHstill works for Finder mounts andTest Connection.
Turn on Use jump host (ProxyJump) in the remote editor and enter an OpenSSH ProxyJump value, for example user@bastion.example.com:22. Use commas for multi-hop chains; spaces are not allowed.
- Applied to Finder mounts and
Test Connectionin all three auth modes. InPasswordmode it is added inside the pinnedssh_command. - The built-in browser cannot tunnel through a jump host yet, so
Browse Remote…is disabled while ProxyJump is active.
brew tap ripplethor/macfusegui https://github.com/ripplethor/macfuseGUI
brew install --cask ripplethor/macfusegui/macfuseguiThe cask requires macOS 13 (Ventura) or later. If Homebrew says the app only runs on Ventura, your Homebrew is older than 5.1.11, which changed how depends_on macos: is read. Run brew update and try again.
Expected sshfs search order:
- Custom sshfs path set in Settings (optional override)
- The previously discovered backend (pinned after first successful lookup)
/opt/homebrew/bin/sshfs/usr/local/bin/sshfs/opt/local/bin/sshfs(MacPorts)/usr/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.sh.github/workflows/release-macos.yml runs scripts/release.sh on a GitHub-hosted macOS runner (workflow_dispatch). From a Windows checkout:
scripts\release.cmd
scripts\release.cmd -DryRun
scripts\release.cmd -Arch arm64 -ReleaseVersion 0.2.0The wrapper does the following before dispatching the workflow on the current branch:
- requires a clean work tree
- checks
originreachability andgh auth status - pushes the current branch
The release workflow does not run the audit or tests, and xcodebuild is not available on Windows. Run the reliability gate on a Mac before releasing.
ARCH_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
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=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 a temporary
SSH_ASKPASShelper script (0700in 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 bysshfs/sshat 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. viaps ewwon Linux, orKERN_PROCARGS2/sysctlon macOS). This is an inherent trade-off of theSSH_ASKPASSprotocol; the password is never on the command line or in any persistent file. System SSHmode intentionally does not append an explicitIdentityFile; it relies on normal OpenSSH config and agent resolution.- Password auth pinning is injected through
ssh_command=/usr/bin/ssh -o ...because current Homebrew macOSsshfsbuilds reject rawPubkeyAuthentication=...-style mount options at the FUSE layer. - 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. - Host keys:
- Finder mounts use
StrictHostKeyChecking=accept-new. - The built-in browser verifies the server key against
~/.ssh/known_hostsbefore authenticating. Unknown hosts are trusted on first use and recorded. A changed key is a hard failure that reports the offered SHA256 fingerprint.
- Finder mounts use
- OpenSSL and libssh2 source tarballs are pinned by SHA256 in
scripts/build_libssh2.sh. Builds refuse a tarball whose hash does not match. - 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.
Auth support:
PasswordandSSH Private Keyare supported in the browser today.System SSHis not supported in the browser yet; use Finder mounts orTest Connectionfor that mode.- Jump hosts (
ProxyJump) are not tunnelled by the browser yet. - Host keys are checked against
~/.ssh/known_hostsafter the SSH handshake and before authentication.
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 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.
See ACKNOWLEDGEMENTS.md for community contributions and release-specific credits.