← Back to Blog

Vault: A Search-First File Manager to Replace Finder

Vault is a minimal, cross-platform file manager that behaves like a command palette instead of a filing cabinet. You find files by typing, not by clicking through a tree: one window, no sidebar, no toolbar, everything reachable from the keyboard. It’s written in Go with a React front end, deletes only to the OS trash, and ships an MCP server so an AI client can drive the same operations the GUI does.

Table of Contents

Why a file manager should behave like a command palette

Finder and Explorer are built around a metaphor from 1984: files live in nested folders, and you navigate by opening them one level at a time. That made sense when a hard drive held a few thousand files.

It doesn’t match how anyone works now. You know the file is called something like invoice-march. You don’t know or care whether it’s in Documents/Work/2026/Q1 or Downloads. Every editor, launcher, and IDE figured this out years ago — Cmd+P in VS Code, Spotlight, Raycast — but the file manager, the tool that exists specifically to find files, still makes you click through a tree.

Vault inverts the default. The search bar is focused the moment the window opens. Browsing still works and is sometimes what you want, but it’s the secondary path, not the primary one.

What Vault does

A single window with three zones: a search input across the top, a breadcrumb strip that appears once you’re inside a folder, and a virtualized list or grid. On the left edge, a slim rail of pinned locations you can drag to reorder.

Vault file manager showing search-first navigation with a pinned rail and a virtualized file list

The pieces that matter day to day:

  • Quick preview on Space — images, video with playback, audio, PDF, plain text, and syntax-highlighted code. Arrow keys move between files while the preview stays open.
  • Command palette on Cmd+K, scoped to the current selection: rename, move with a fuzzy folder picker, copy, duplicate, copy path, new folder, compress, trash, pin.
  • Trash only. Deletions go to ~/.Trash on macOS or the Recycle Bin on Windows. There is no permanent-delete path anywhere in the codebase.
  • Undo on Cmd+Z for renames and moves.
  • Image thumbnails decoded, resized, and disk-cached by a bounded worker pool in Go, so the grid doesn’t stall.
  • Real accessibility, not just keyboard support: the list and grid are proper listbox/option widgets with aria-selected, overlays trap focus and restore it on close, async state is announced via aria-live, and prefers-reduced-motion is respected.

Under the hood it’s Wails v3 — a Go backend driving the OS’s native webview — with React, TypeScript, and Tailwind on top. The Go side owns everything filesystem: directory listing, the search index, fsnotify watchers, thumbnail generation, file operations.

Everything is reachable from the keyboard

Vault keyboard shortcut map showing search, navigation, preview, and command palette bindings

Typing anywhere searches. Esc clears and returns home. Cmd+↑ goes to the parent folder. Space previews. Cmd+Enter on a search result reveals it in its folder without leaving the app. There are also Gmail-style single-key shortcuts — / to focus search, C to copy, ? for the shortcut help dialog — that fire whenever you’re not typing into something.

What Vault refuses to do

The non-goals are as deliberate as the features, and they’re written into the spec:

No tabs. No split panes. No cloud integrations. No network drives. No bulk metadata editing. No general third-party plugin system. No file tagging.

Every one of those is individually defensible, and collectively they’re how you get Finder. A file manager that does twelve things adequately is worse at the one thing you actually open it for. The scope is “find and manipulate local files, fast,” and things outside that line stay outside it.

How search works

Search is a Go-side fuzzy matcher (sahilm/fuzzy) over an in-memory index, ranked in three tiers: match quality first, then recency of access, then frequency — frecency, the same idea browsers use for the address bar.

Two behaviors are worth calling out because they took real iteration:

Content search for text files. Source code, markdown, config, and plain text also match on their contents, capped at ~200KB per file. Content matches are a plain case-insensitive substring check appended after all name matches, because name relevance is a much stronger signal than “this word appears somewhere inside the file.”

Scope-aware results. When you’re browsing a folder, matches under that folder are guaranteed their own share of the result limit, server-side. This fixed a genuinely annoying bug where files in the folder you were looking at got silently crowded out by higher-scoring matches elsewhere in the tree. Results are then staged by proximity — matches in the current folder appear first under “Searching in {folder}”, then the scope visibly widens to each parent in turn. You can see which scope is active instead of getting one flat, unlabeled list.

Indexing only what you visit

The original design indexed the whole home directory at startup. That failed twice, and the second failure is worth knowing about if you write Go that walks a filesystem.

First, the walk never finished. It descended into ~/Library, which on my machine is 96GB of caches, Mail, application support, and CoreSimulator. Search sat there reporting “index not ready yet” indefinitely.

Then, once ~/Library was excluded, the walk failed outright. macOS TCC denies access to privacy-protected folders with EPERM, not EACCES — and Go’s os.IsPermission only recognizes the latter. So a single protected folder anywhere in the tree returned an error the skip-logic didn’t classify as a permission problem, and aborted the entire walk. The function’s own doc comment promised unreadable subtrees would be skipped; it didn’t, and the error type is why.

If you have “skip unreadable directories and continue” logic in a Go filesystem walk on macOS, test it against TCC-protected paths. It probably aborts instead.

The fix wasn’t a better walk — it was to stop walking. The indexer now only indexes directories that have actually been visited. Visit(dir) does one non-recursive ReadDir, upserts those entries, and adds a single watch. The GUI calls it whenever you navigate into a folder; the MCP list_directory tool calls it too.

The tradeoff is real, and documented rather than buried: search covers the folders you’ve opened this session, not your entire home directory from launch. Subfolders shown inside a visited folder are findable by name, but their contents aren’t indexed until you go in. That’s a downgrade from the original ambition, taken because eager indexing doesn’t survive contact with a real home directory.

Performance targets and measured results

The spec set four numeric targets. Three clear comfortably; one misses in a specific worst case.

Vault performance results measured against spec targets for cold start, search latency, scroll FPS, and idle RAM

Target Result Status
Cold start to interactive under 1.5s 353ms avg (330 to 417ms, 5 trials) Pass
Search render under 100ms at 200k files 39ms typical, 84ms broad, 101ms broad scoped to a folder Mostly pass
Scroll at 60fps with 50k entries 120fps avg, 62.9fps worst frame, 0 frames over budget Pass
Idle RAM under 200MB ~80MB, 40s after launch Pass

The 101ms is the interesting one. It’s a broad query matching every entry, scoped to a subfolder — the worst case the ranking logic handles — and it’s the backend call only, excluding the 50ms input debounce and IPC overhead. Real perceived latency in that specific case is higher than 101ms, not lower. The realistic case, a query narrowed by a few typed characters, is 39ms and stays inside target even after debounce and render.

It would have been easy to round that down. docs/PERFORMANCE.md says it misses instead, documents how every figure was produced, and flags the awkward parts: scroll FPS was measured on the same component mounted standalone under headed Chrome because no automation reaches the native webview, the 120fps average is an artifact of a 120Hz display (the number that matters is the 62.9fps worst frame), and none of it runs in CI — it’s a repeatable manual procedure, not a regression gate.

Search latency is a committed Go benchmark you can re-run yourself:

go test ./internal/core/ -run '^$' -bench 'BenchmarkSearch200k' -benchtime=10x -count=5

Driving Vault from Claude or Cursor

Vault exposes nine operations — list directory, search, read file, move, rename, paste-into, new folder, trash, list pinned — and an MCP server (cmd/vault-mcp) that hands all nine to any MCP client:

make mcp-build
./bin/vault-mcp --root ~/Projects   # optional: restrict scope (defaults to $HOME)
./bin/vault-mcp --http :8090        # optional: streamable HTTP instead of stdio
{
  "mcpServers": {
    "vault": {
      "command": "/absolute/path/to/bin/vault-mcp"
    }
  }
}

There’s also an embedded chat panel inside the app with bring-your-own-key support for Claude or ChatGPT. Both surfaces call the same internal/core the GUI does, so there’s exactly one implementation of “move a file” rather than a second backend that drifts.

Two safety defaults, because an agent is a different trust boundary than a human clicking around: operations are confined to the home directory by default and path-traversal-checked, and deletion goes to the OS trash. The worst case for a confused agent is recoverable.

If you’re wiring MCP into your own tools, I’ve covered the pattern in more depth for pREST’s MCP server and the surrounding plugin ecosystem.

Known limitations

From docs/STATUS.md, the ones that would actually affect you:

  • The Windows adapter has never been run. It cross-compiles clean and that’s the entire claim — there’s no Windows machine here to test it. “Cross-platform” describes the code, not verified behavior.
  • The chat panel stores your API key in localStorage in plaintext. Wails v3 has no keychain wrapper, and per-OS secure storage isn’t done.
  • No video thumbnails (needs a decoder dependency the project doesn’t take), “Open With…” is disabled, and Windows packaging produces an NSIS .exe rather than a real .msi.
  • Content search holds indexed text in RAM and scans linearly per query instead of using an inverted index. Fine for a home directory; not benchmarked at 200k files that are mostly text.
  • No progress bar or cancel for long copies. Errors surface as toasts, but a 40GB move gives you nothing to watch.

It’s MIT licensed, early, and a tool I use rather than a product.

FAQ

Why Wails instead of Electron or Tauri?

Wails v3 pairs a Go backend with the OS’s native webview, so the binary stays small and all the filesystem work — indexing, fuzzy matching, thumbnail worker pools, watchers — lives in Go where concurrency is cheap. Idle RAM came in around 80MB against a 200MB target, which Electron would not make easy. Tauri would have been a reasonable alternative; wanting the backend in Go decided it.

Does Vault delete files permanently?

No. Every delete path goes to the OS trash — ~/.Trash on macOS, the Recycle Bin via SHFileOperationW on Windows. There is no permanent-delete implementation in the codebase, and that applies to the MCP server and chat panel too, not just the GUI.

Is Vault ready to replace Finder?

On macOS it’s usable daily, which is what it was built for. On Windows the adapter compiles but has never run, so treat that as unverified. Read docs/STATUS.md before committing to it.

Try it

The repo is at github.com/arxdsilva/vault, MIT licensed.

go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-alpha2.117
cd frontend && npm install && cd ..
wails3 dev                    # hot reload
wails3 build && ./bin/vault   # production build

If something breaks — especially on Windows, where I genuinely can’t test — open an issue.