This file is for AI agents. Human contributors, see CONTRIBUTING.md.
Cmdr is an extremely fast AI-native file manager written in Rust, free forever for personal use on macOS (BSL license). Downloadable at the website.
- Dev server:
pnpm devat repo root - Prod build:
pnpm buildat repo root - Both must run at repo root. The root
package.jsonhas notauriscript, sopnpm tauri devonly works from insideapps/desktop/. Prefer the root form: both paths flow throughtauri-wrapper.jsand are equivalent, but the root form is what's documented and what other tooling assumes.
These are general principles for the whole project. These are not just empty sentences on our wall, we live these:
- Deliver delightful UX. We always go the extra mile to make it absolutely delightful to use our software. Not just
functional, but noticeably pleasant. Thoughtful phrasing. Accessible focus indicators. Putting real effort in
dark/light modes. Nice images and anims. OS-custom everything. Respect the system font, sizing, theme,
prefers-reduced-motion, etc. - Elegance above all. We have time to do outstanding work. We prefer a clean and elegant architecture over hacks, both internally for ourselves, and externally toward the user. We think about the long run.
- The app should feel rock solid. The UI must always be responsive. We never block the main thread. Every user action triggers immediate feedback, even if it's just a spinner. We communicate what's actually happening. Show progress. An ETA when possible. No progress bars stuck at 100%; show the real state. Long operations are always cancelable, stopping background work too, not just the UI. The user is always in control. Assume the hostile case (dead network mount, huge directory, crashed mid-operation) and handle it gracefully.
- Protect the user's data. Use safe overwrite patterns like temp+rename. Offer rollback for destructive operations. Use atomic ops where possible. Design for the crash mid-operation. Test the shit out of the parts that write data.
- Be respectful to the user's resources. Minimize CPU use, memory use, don't thrash the disks.
- Think from first principles, capture intention. Add logs. Run the code. Do benchmarks. Then document the "why"s and link the data where needed.
- Invest in finding the right tradeoff. Elegance lives between duplication and overengineering. No premature abstractions, but no copy-paste either.
- Smart backend, thin frontend. Complex logic lives in Rust. The frontend's job is to deliver a delightful UX: presenting the right states, errors, progress, and feedback. Display logic can get complex, but the business logic lives in the backend.
- Organized by feature, not by layer. Frontend components, backend modules, tests, and docs are colocated with the
feature they belong to. Colocated
CLAUDE.mdfiles, colocated tests, feature-shaped directories. If we could technically merge a Svelte component with its Rust counterpart into one feature unit, we would. - Thin IPC layer. Tauri commands are pass-throughs. no branching, no transformation. Business logic lives in subsystem modules that can be tested independently.
- Subscribe, don't poll. Whenever possible, we make it so that consumers can subscribe to events and receive updates. If not possible, we resort to polling. But we make an effort to avoid polling.
- Invest in testability. We have virtual MTP devices, Docker-based SMB servers, feature flags for E2E. Tools to guarantee stability.
- Invest in tooling. We have check runners, linters, coverage, CI. Tooling must be fast so we use it, and strict so it doesn't allow us to make mistakes.
This is a monorepo containing these apps:
- Cmdr: Currently for macOS only. Rust, Tauri 2, Svelte 5, TypeScript, and custom CSS.
- Analytics dashboard: Private SvelteKit metrics dashboard. Deployed to Cloudflare Pages.
- getcmdr.com website: Astro + Tailwind v4. Deployed via Docker + Caddy.
- API server: Cloudflare Worker + Hono. Licensing, telemetry, crash reports, downloads, and admin endpoints.
Core structure:
/.github/workflows/- GitHub Actions workflows/apps/analytics-dashboard/- Private metrics dashboard (SvelteKit + CF Pages)desktop/- The Tauri desktop apptest/e2e-linux/- WebDriverIO + tauri-driver tests (Docker, tests real Tauri app)src/- Svelte frontend. Uses SvelteKit with static adapter. TypeScript strict mode. Custom CSS with design tokens.lib/- Componentsroutes/- Routes
src-tauri/- Latest Rust, Tauri 2, serde, notify, tokiostatic/- Static assetstest/- Vitest unit tests
api-server/- Cloudflare Worker (Hono). Licensing, telemetry, crash reports, downloads, and admin endpoints.website/- Marketing website (getcmdr.com)
/scripts/check/- Go-based unified check runner/docs/- Dev docsguides/- How-to guidesnotes/- Temporary reference notes (benchmarks, analysis) linked from CLAUDE.md filestooling/- Internal tooling docsarchitecture.md- Map of all subsystems with links to theirCLAUDE.mdfilesstyle-guide.md- Writing, code, and design style rulessecurity.md- Security policiesmaintenance.md- Recurring maintenance tasks (dep bumps, allowlist trims, doc sweeps) and a log of past runs
- Feature-level docs live in colocated
CLAUDE.mdfiles next to the code (for example,src/lib/settings/CLAUDE.md). Claude Code auto-discovers these. Seedocs/architecture.mdfor the full map.
Before adding or modifying tests, read docs/testing.md: the testing playbook (decision table, anti-patterns, per-feature checklist). The companion file docs/tooling/testing.md is the tools inventory.
Always use the checker script for compilation, linting, formatting, and tests. Its output is concise and focused: no
2>&1, head, or tail needed. Don't run raw cargo check, cargo clippy, cargo fmt, cargo nextest run, etc.
- Specific checks:
./scripts/check.sh --check <name>(e.g.--check clippy,--check rustfmt). Use--helpfor the full list, or multiple--checkflags. - All Rust/Svelte checks:
./scripts/check.sh --rustor--svelte - All checks:
./scripts/check.sh
Three cadences. Pick the one that matches where you are in the work, not the one closest to "done."
./scripts/check.sh --fast— every few file edits, on a self-imposed rhythm (~7 s). Don't wait for "before commit"; that's too late, by then a regression is buried under follow-up edits. Run after a small natural unit of work: a function rewritten, a test added, a config touched. Catches roughly half the things the full suite catches, for ~5% of the wall time, so use it liberally. The lane is editorially curated, not derived from timings; mutually exclusive with--include-slow/--only-slow. Covers:- All formatters (
oxfmt,rustfmt,gofmt) and most non-compiling static linters (cfg-gate,log-error-macro,error-string-match,ipc-enum-camelcase,cargo-machete,knip,import-cycles,type-drift,stylelint,css-unused,a11y-contrast,btn-restyle,a11y-coverage,e2e-linux-typecheck). - Go:
go-vet,staticcheck,ineffassign,misspell,gocyclo,go-tests. - API server:
typecheck,tests. - Website:
html-validate(self-skips whendist/is absent). - Warn-only metrics:
file-length,claude-md-reminder,changelog-links. - Does NOT cover:
clippy, Rust tests,cargo-audit,cargo-deny,jscpd,bindings-fresh, desktop ESLint /svelte-check/ Svelte tests, website ESLint / typecheck / build / e2e,docker-build, or any E2E suite.
- All formatters (
./scripts/check.sh— before every commit. The full default suite (everything not markedIsSlow). Catches what--fastskips:clippy, Rust tests, audit/deny, svelte-check, website build, etc. This is the contract that what you're committing won't break CI../scripts/check.sh --include-slow— before wrapping a milestone, declaring a feature done, or pushing a branch you've been sitting on. Adds the slow lane on top of the default suite:desktop-e2e-linux,desktop-e2e-playwright,rust-tests-linux,eslint-typecheck. Allow ~20 min; this is the gate before "I'm done."oxfmtmust always run before you call a task done. It's monorepo-wide (markdown, YAML, JSON, JS/TS across every app) and takes ~1 second, so there's no reason to skip it. It's registered underAppOther, which means--rustand--sveltedo NOT include it. If you only ran those, CI will catch unformatted markdown / JSON / etc. that you missed. Always finish with either./scripts/check.sh(the full suite) or at minimum./scripts/check.sh --check oxfmtafter your other checks. No exceptions.- Specific tests by name (the one exception where direct commands are fine):
- Rust:
cd apps/desktop/src-tauri && cargo nextest run <test_name> - Svelte:
cd apps/desktop && pnpm vitest run -t "<test_name>" - Playwright: see
apps/desktop/test/e2e-playwright/CLAUDE.md§ "Running a single spec"
- Rust:
- When iterating on one test, run only that test. The full suite is for confirming CI-green before declaring done, not for each tweak. Running the whole Playwright suite for one new spec wastes ~10 minutes per cycle and produces noisy "cascade" failures when the broken test takes the app down with it (subsequent specs fail with connection errors). Same principle at smaller scales for Rust and Vitest.
- E2E (Playwright): See
apps/desktop/test/e2e-playwright/CLAUDE.md. Build withplaywright-e2efeature, start app, run tests - Ubuntu test VM: See
apps/desktop/test/e2e-linux/CLAUDE.md§ "Ubuntu test VM" - Docker SMB containers: 14 Samba containers (guest, auth, readonly, slow, flaky, unicode, deep nesting, etc.) for
integration tests. Start with
apps/desktop/test/smb-servers/start.sh. Connect from Rust viasmb2::testing::guest_port()and friends. Seeapps/desktop/test/smb-servers/README.mdfor details. - CI: Runs on PRs and pushes to main for changed files. Full run: Actions → CI → "Run workflow".
- Data dirs (dev and prod are separate!): Prod:
~/Library/Application Support/com.veszelovszki.cmdr/, Dev:~/Library/Application Support/com.veszelovszki.cmdr-dev/. Dev path is set viaCMDR_DATA_DIRenv var bytauri-wrapper.js; resolved insrc-tauri/src/config.rs. - Logging: Frontend and backend logs appear together in terminal and in the log dir (dev:
<CMDR_DATA_DIR>/logs/, prod:~/Library/Logs/com.veszelovszki.cmdr/). Read docs/tooling/logging.md before usingRUST_LOG: it has copy-paste recipes for every subsystem. Key gotcha: the Rust library target iscmdr_lib, notcmdr. UseRUST_LOG=cmdr_lib::module=debug. Note:cmdr_lib(lib) andCmdr(bin) are both in thecmdrCargo package, soCompiling cmdrin build output covers BOTH targets. Cargo won't showCompiling cmdr_libseparately. - Crash reports: When the app crashes, it writes a crash file to the data dir (
crash-report.jsonalongsidesettings.json). On next launch, the app detects this file and offers to send a crash report. Seesrc-tauri/src/crash_reporter/CLAUDE.mdfor architecture details. - Error reports: When triaging an error report bundle (zip +
manifest.json), readsrc-tauri/src/error_reporter/CLAUDE.mdfirst: it documents the bundle layout, what each piece captures, and the redaction conventions. - Hot reload:
pnpm devhot-reloads. Max 15s for Rust, max 3s for frontend. - Index DB queries: The index SQLite DB uses a custom
platform_casecollation, so thesqlite3CLI can't query it. Usecargo run -p index-query -- <db_path> "<sql>"instead. See docs/tooling/index-query.md for examples and DB paths.
Two MCP servers are available when the app is running via pnpm dev:
- cmdr (port 19224 prod / 19225 dev): high-level app control: navigation, file operations, search, dialogs, state
inspection. This is the primary way to test and interact with the running app. Architecture docs:
src-tauri/src/mcp/CLAUDE.md. - tauri (port 9223): low-level Tauri access: screenshots, DOM inspection, JS execution, IPC calls. Use for visual verification and UI automation.
Before making any MCP calls, read docs/tooling/mcp.md for usage patterns, connection resilience, and common pitfalls.
- User-generic preferences (e.g. "never use git stash", "don't take external actions without approval") →
~/.claude/CLAUDE.md. These apply across all projects. - Project-specific instructions →
AGENTS.md(this file) for repo-wide rules, or colocatedCLAUDE.mdfiles for module-specific docs. These are version-controlled and visible to all contributors. - Don't use the project-level
memory/MEMORY.mdfor either category. It's not transparent and not in the repo.
- ❌ NEVER use
git stash,git checkout,git reset, or any git write operation unless explicitly asked. Multiple agents may be working simultaneously. - ❌ NEVER add dependencies without checking license compatibility (
cargo deny check) and verifying the latest version from npm/crates.io/GitHub. Never trust training data for versions. - ❌ When adding code that loads remote content (
fetch,iframe), ask whether to disable in dev mode.withGlobalTauri: truein dev mode is a security risk. - ❌ When testing the Tauri app, DO NOT USE THE BROWSER. Use the MCP servers.
- ❌ Don't ignore linter warnings. Fix them or justify with a comment.
- Icons: We use
unplugin-iconswith@iconify-json/lucide. Import as Svelte components from~icons/lucide/{icon-name}(inline SVGs, no runtime cost). Seedocs/style-guide.md§ Icons for usage, sizing, coloring, and how to find new icons. - Always use CSS variables defined in
apps/desktop/src/app.css. Stylelint catches undefined/hallucinated variables. - Never use raw
pxvalues forfont-size,border-radius,font-family, orz-index>= 10. Usevar(--font-size-*),var(--radius-*),var(--font-*), andvar(--z-*)tokens. Stylelint enforces this. - Coverage allowlist is a last resort. Extract pure functions and test them. Only allowlist what genuinely can't be tested. Name the specific untestable API in the reason.
- When adding a new user-facing action, add it to
command-registry.tsandhandleCommandExecuteinroutes/(main)/command-dispatch.ts. - If you added a new Tauri command touching the filesystem, check
docs/architecture.md§ Platform constraints. - ❌ Don't read TCC-protected paths or call NSWorkspace icon/LaunchServices APIs at app launch without the FDA gate.
~/Downloads,~/Documents,~/Desktop,~/Pictures,~/Movies,~/Music,~/Library/CloudStorage, and anyNSWorkspace.iconForFile:call (even on/Applicationsor the iCloud root) can trigger macOS TCC popups during onboarding. We had 5–10 popups stacked on top of the in-app FDA modal before this gate landed. Usecrate::fda_gate::is_fda_pending_runtime()for launch-time call sites, orcrate::fda_gate::is_fda_pending(fda_choice, os_fda_granted)for pure logic and tests. After Allow + restart, or Deny in-session viastart_indexing_after_fda_decision, the gate clears and the same call sites run normally. Seeapps/desktop/src-tauri/src/fda_gate.rsandapps/desktop/src/lib/onboarding/CLAUDE.md§ "FDA gate". - ❌ Tauri APIs fail silently without permissions. Whenever you call a new Tauri API from a window (
setMinSize,setTitle,show, plugin commands, anything new), add the matching permission to that window's capability file insrc-tauri/capabilities/{default,settings,viewer}.json. Without it, the call rejects with a generic "not allowed" error and your feature looks broken with no obvious cause. Surface failures byawait-ing the call inside atry/catchand logging the error rather thanvoid-ing the promise. Seesrc-tauri/capabilities/CLAUDE.mdfor the per-window split and naming conventions. - We use mise to manage tool versions (Go, Node, etc.), pinned in
.mise.toml. Shims are on PATH via~/.bashrcand~/.zshenv, sogoandnodeshould just work. Ifgois "not found", check that~/.local/share/mise/shimsis on$PATH. - After bumping npm deps, run
pnpm dedupe. Without it, transitive deps (e.g.postcss-html'spostcss,@axe-core/playwright's@playwright/test) can stay pinned to older nested versions, producing weird false-positive failures: stylelint 17.9 misparses Svelte inlinestyle="..."attributes against an old postcss; website-typecheck fails on aPagetype mismatch when AxeBuilder gets a different Playwright version than the e2e specs. - ❌ NEVER use
eprintln!,println!, ordbg!insrc-tauri/code. They bypass the fern logger: no level filtering, no file output, no inclusion in error-report bundles. Clippy denies them at the crate root. Uselog::debug!/log::info!/log::warn!/log::error!with a scopedtarget:(for examplelog::debug!(target: "open_with", "...")) so logs are filterable viaRUST_LOG. READapps/desktop/src-tauri/src/logging/CLAUDE.mdbefore adding any log call or touching the log pipeline: it has the rules and the why. - ❌ NEVER build the Tauri app with raw
cargo build. It produces a binary without the embedded frontend (white screen). Always build viapnpm tauri buildor thenode scripts/tauri-wrapper.js buildwrapper fromapps/desktop/. ThebeforeBuildCommandintauri.conf.jsonruns the llama-server download (Go) and frontend build; skipping it breaks the app. For E2E builds:node scripts/tauri-wrapper.js build --no-bundle --target $(rustc -vV | grep host | cut -d' ' -f2) -- --features playwright-e2e,virtual-mtp,smb-e2e. The binary lands in<repo>/target/<triple>/release/Cmdr.- Don't add your own build-cache layer.
pnpm tauri buildalready caches internally: Cargo's incremental compilation, Vite/SvelteKit's frontend build cache, and thebeforeBuildCommand's own short-circuits all kick in on warm runs. A "skip build if hash matches" check on top of that is redundant and risks shipping a stale binary.
- Don't add your own build-cache layer.
- ❌ No string-matching error or state classification. Don't classify errors, app state, or control flow by checking
substrings of a message, stderr, error title, or any other free-form text. Use a typed enum variant, an errno code, or
an explicit flag on the struct that crosses the IPC boundary. The wording is for the user to read; code that branches
on it breaks silently when copy changes, when the OS localizes, or when an upstream library reformats its messages.
- Tests too: prefer
assert!(matches!(err, VolumeError::AlreadyExists(_)))overerr.message.contains("..."). The variant is the contract; the message is documentation. - Enforced by:
error-string-match(Rust check, scansapps/desktop/src-tauri/src/) andcmdr/no-error-string-match(ESLint rule, scansapps/desktop/src/). - Opt out only when there's no other option (third-party CLI with no exit-code differentiation, etc.). Add
// allowed-error-string-match: <reason>on the line above (Rust) or// eslint-disable-next-line cmdr/no-error-string-match -- <reason>(TS/Svelte). Pair the opt-out withLC_ALL=Con the subprocess and snapshot tests pinning the matched strings against a tool version.
- Tests too: prefer
- ❌ Type-safe IPC: no raw
invoke('...')outside the typed bindings folder. Tauri command names are duplicated across the Rust#[tauri::command]site and every TS call site, with no compile-time link. Renaming the Rust side silently breaks runtime IPC with a generic "not allowed" error. The repo wirestauri-spectato generate typed bindings intoapps/desktop/src/lib/ipc/; call them ascommands.commandName(args)instead.- Enforced by:
cmdr/no-raw-tauri-invoke(ESLint rule). Bypassed only insidelib/ipc/(the bindings),routes/debug/(dev-only debug panels), and test files. - Regenerate with
cd apps/desktop && pnpm bindings:regenafter any change to a#[tauri::command]surface or a Type-derived DTO. CI'sbindings-freshcheck fails if the committedbindings.tsis stale. - At call sites, prefer named locals over inline primitives.
commands.renameFile(from, to, force, volumeId)is fine;commands.foo(true, null, 5)isn't. Extractconst force = true; const volumeId = null; const retries = 5first. This is the price specta charges for type safety. - For the rules around adding new commands, type shape constraints (
skip_serializing_if,serde_json::Value), and the current exclusion list, readapps/desktop/src/lib/ipc/CLAUDE.md.
- Enforced by:
- When working in a linked git worktree under
.claude/worktrees/, the gitignoredapps/desktop/src-tauri/resources/ai/(llama-server binaries, ~30 MB) starts empty. You don't need to do anything:apps/desktop/src-tauri/build.rsinvokesapps/desktop/scripts/download-llama-server.goon demand, which symlinks the dir from the main clone at~/projects-git/vdavid/cmdr/when its.versionmatches, and falls back to downloading otherwise. So rawcargo checkJust Works in fresh worktrees. Don't paper over a missingresources/ai/with a placeholder file. - When using worktrees, always branch off from local
main(notorigin/main) and rebase and FF local main.
- Always read style-guide.md before touching code. Especially sentence case!
- Cover your code with tests until you're confident. Don't go overboard. Test per milestone.
- We don't use PRs. Changes land directly on
main. The "PR" section in.claude/rules/git-conventions.mdis for the rare case David explicitly asks for one; the default is a regular commit onmain(or merging a feature branch intomain). Nogh pr create, no review-app webhook, none of that. - Never
git push(orgit push --tags) without explicit approval. Even after a clean commit onmain, pushing is an external action, so wait until David says to push. This applies to feature branches and tags too. The user-level rule~/.claude/rules/no-external-actions.mdalready covers this; restating it here so it's impossible to miss.
Happy coding! 🦀✨
Read docs/architecture.md next!