Thanks for your interest in contributing to Cmdr! The easiest way to contribute is to fork the repo, make your changes, and submit a PR. This doc will help you get started.
Note: This doc is entirely for humans. AI agents always read AGENTS.md and the colocated CLAUDE.md files
instead of this file.
The project uses mise for tool version management. It handles Node, pnpm, and Go versions. Rust
is managed separately by rustup. This version is tested with Rust 1.92.0.
- Install mise:
brew install mise(see alternatives) - Install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Run
mise installto set up Node, pnpm, and Go - Run
cd apps/desktop && pnpm installto install frontend dependencies
Cmdr stores SMB credentials in macOS Keychain. Keychain ties item access to the binary's code signature; production builds are signed, so users get a one-time prompt. But dev/E2E builds are ad-hoc signed (changes every rebuild), which triggers a Keychain password prompt on every restart.
To avoid this, create a local "Cmdr Dev" code signing certificate:
- Open Keychain Access.app
- Menu: Keychain Access → Certificate Assistant → Create a Certificate...
- Name:
Cmdr Dev, Identity Type: Self Signed Root, Certificate Type: Code Signing - Click Create, then trust it for code signing:
security find-certificate -c "Cmdr Dev" -p > /tmp/cmdr-dev.pem security add-trusted-cert -p codeSign -r trustRoot -k ~/Library/Keychains/login.keychain-db /tmp/cmdr-dev.pem rm /tmp/cmdr-dev.pem
- Verify:
security find-identity -v -p codesigningshould list "Cmdr Dev".
Once the certificate exists, the E2E check script (./scripts/check.sh --check desktop-e2e-playwright) auto-signs the
binary before launching. For manual E2E runs, sign after building:
codesign --force -s "Cmdr Dev" ./target/$(rustc -vV | grep host | cut -d' ' -f2)/release/CmdrIf you already have Cmdr Keychain items from unsigned builds, delete them so they get re-created with the correct ACL:
security delete-generic-password -s "Cmdr" -a "smb://yourserver/yourshare"pnpm devThis starts both the Svelte frontend and the Rust backend with hot reload.
To test with a virtual MTP device (simulated Android phone):
pnpm dev -- --features virtual-mtpThis still flows through apps/desktop/scripts/tauri-wrapper.js, which generates the per-instance config (bundle
identifier, CMDR_DATA_DIR, file-backed secret store) on the fly. Pass --worktree <slug> first to isolate a
worktree's data dir from your main dev session: pnpm dev --worktree foo -- --features virtual-mtp.
In dev mode, press Cmd+D to open a debug window. This window is only available in dev builds and provides:
- Dark mode toggle: Switch between light and dark themes without changing your system settings
- Navigation history: Real-time view of back/forward history for both panes, showing current position and available history entries
The debug window is a separate, movable window that updates in real-time as you navigate.
Rust uses the standard RUST_LOG env var for log level control. The pnpm dev command sets a sensible default
(smb=warn,sspi=warn,info) to silence noisy external crate logs. Override with your own RUST_LOG as needed:
# Only warnings and errors
RUST_LOG=warn pnpm dev
# Verbose network debugging
RUST_LOG=cmdr_lib::network=debug,info pnpm dev
# Unsilence MCP logs by running the command directly, without the env vars built-in to `pnpm dev`
pnpm --filter @cmdr/desktop tauri devModule paths follow the Rust crate structure: cmdr_lib::mcp, cmdr_lib::network, cmdr_lib::licensing, etc.
These are slash commands for Claude Code (type /command-name in the CLI):
/plan: use when starting a feature/wrap-up: use before finishing work/release: prepare a release (changelog, versioning, roadmap)/commit-draft: draft a commit message for staged changes
Run all checks before committing with ./scripts/check.sh. And here is a more complete list:
./scripts/check.sh # to run all checks before committing - USE THIS BY DEFAULT
./scripts/check.sh --rust # to run Rust checks
./scripts/check.sh --svelte # to run Svelte checks
./scripts/check.sh --check clippy # to run specific checks
./scripts/check.sh --help` # for more options.
# Alternatively, some specific checks (run from apps/desktop/), but these are rarely needed:
cd apps/desktop/src-tauri
cargo fmt # to format Rust code
cargo clippy # to lint Rust code
cargo audit # to check Rust dependencies for security vulnerabilities
cargo test # to run Rust tests
cd apps/desktop
pnpm format # to format frontend code
pnpm lint --fix # to lint frontend code
pnpm test # to run frontend testsThe Linux E2E tests run against the real Tauri app with WebKitGTK. Since macOS doesn't have a WebDriver for WKWebView,
we need a Linux environment. We use a UTM virtual machine (Apple Virtualization) with Ubuntu, connected to the LAN at
192.168.1.97. The macOS repo is shared via VirtioFS so edits on either side are instant, but uses custom bind mounts
to avoid node_modules and build folders overwriting each other between the host mac and the VM.
How to use it for testing the app:
- Start the VM
cd ~/cmdrpnpm installif it's been a while or you've added new depsmountpoint /mnt/cmdr/cmdr/target && mountpoint /mnt/cmdr/cmdr/node_modulesto verify the bind mounts are healthy- If either mountpoint check fails, run
sudo mount -aand re-check.
- If either mountpoint check fails, run
eval "$(mise activate bash)"to activate mise. It sets up Node/pnpm/Go (not available in the default SSH shell).
From here, either run the app or run E2E tests:
# a) Run the app (dev mode with hot reload)
cd ~/cmdr
WEBKIT_DISABLE_COMPOSITING_MODE=1 pnpm dev
# b) Run E2E tests (in Docker, same path CI runs)
cd ~/cmdr/apps/desktop
pnpm test:e2e:linuxSee apps/desktop/test/e2e-linux/CLAUDE.md for VNC debugging, VM setup details, and WebKitGTK quirks.
From repo root:
pnpm buildOr from the desktop app directory:
cd apps/desktop
pnpm tauri buildThis creates a production build for your current platform in apps/desktop/src-tauri/target/release/.
For an universal installer:
rustup target add x86_64-apple-darwinonce- Then
cd apps/desktop && pnpm tauri build --target universal-apple-darwineach time. - Then the binary is at
apps/desktop/src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cmdr_0.1.0_universal.dmg
The app uses MCP Server Tauri to let AI assistants (Claude Code, Cursor, etc.) control this app: take screenshots, click buttons, read front-end logs, etc. It's quite helpful.
For claude-code, cursor, vscode, or windsurf, there is autoconfig available. Run this command in your terminal
for your specific client: npx -y install-mcp @hypothesi/tauri-mcp-server --client <your-client>.
(source).
If the automated setup doesn't work for you, check the MCP documentation for your specific client. For example:
This snippet will likely come handy:
{
"mcpServers": {
"tauri": {
"command": "npx",
"args": ["-y", "@hypothesi/tauri-mcp-server"]
}
}
}Or add it via CLI like:
Since the agent shares the context with your IDE/client, enabling the MCP server makes the tools available to the agent automatically.
The API server is a Cloudflare Worker. To deploy it or run wrangler commands, you need a Cloudflare API token.
- Go to https://dash.cloudflare.com/profile/api-tokens → Create Token → Custom token
- Permissions:
Account / Workers Scripts / EditAccount / Account Analytics / ReadAccount / Workers Scripts / EditZone / Workers Routes / EditZone / DNS / Edit
- Account resources: the Cmdr account only
- Add to macOS Keychain:
security add-generic-password -a "$USER" -s "CLOUDFLARE_API_TOKEN" -w "your-token"
Wrangler picks up CLOUDFLARE_API_TOKEN from the environment; the shell profile exports it from Keychain on startup.
PostHog is used for session replay and heatmaps on getcmdr.com. To use the PostHog management API (for example, to update project settings), you need a personal API key.
- Go to https://eu.posthog.com/settings/user-api-keys → Create personal API key
- Scope it to the Cmdr project
- Add to macOS Keychain:
security add-generic-password -a "$USER" -s "POSTHOG_API_KEY" -w "phx_your-key"
See posthog.md for API recipes.
Paddle handles payments and subscriptions. Two API keys are needed: one for live, one for sandbox (testing).
- Go to https://vendors.paddle.com → Developer tools → Authentication → Generate API key
- Repeat for sandbox at https://sandbox-vendors.paddle.com
- Add both to macOS Keychain:
security add-generic-password -a "$USER" -s "PADDLE_LIVE_API_KEY" -w "your-live-key" security add-generic-password -a "$USER" -s "PADDLE_SANDBOX_API_KEY" -w "your-sandbox-key"
See the Paddle generic tooling doc for API recipes.
The analytics dashboard at analdash.getcmdr.com is behind Cloudflare Access. To fetch reports via the API, you need a
service token.
- Go to https://one.dash.cloudflare.com → Access → Service Auth → Create Service Token
- Add both values to macOS Keychain:
security add-generic-password -a "$USER" -s "CF_ACCESS_CLIENT_ID_EXPIRES_2027_03_22" -w "your-client-id" security add-generic-password -a "$USER" -s "CF_ACCESS_CLIENT_SECRET_EXPIRES_2027_03_22" -w "your-client-secret"
The token expires 2027-03-22. See analytics-dashboard.md for usage.
ngrok exposes local servers to the internet, useful for testing webhooks (for example, Paddle) against your local API server.
- Go to https://dashboard.ngrok.com → Your Authtoken (or API → API Keys for the API key)
- Add to macOS Keychain:
security add-generic-password -a "$USER" -s "NGROK_API_KEY" -w "your-api-key"
See the ngrok generic tooling doc for API recipes.
To run the API server locally (for testing license activation, generating test keys, etc.), you need a .dev.vars file
with Paddle and Resend secrets. See the API server README for the full
setup. Ask a maintainer for the current values if you don't have dashboard access.
The release workflow runs on a self-hosted macOS runner and can save a bunch of GitHub Actions credits.
To set one up:
- Go to repo → Settings → Actions → Runners → New self-hosted runner
- Select macOS and ARM64
- Follow GitHub's instructions to download, configure, and register the runner, and run it to test it works.
- Quit it, then install it as a launchd service so it starts on boot:
./svc.sh install ./svc.sh start
- Make sure the runner has all build dependencies: Rust (
rustup), Node, pnpm, Go (all viamise install), and Xcode CLI tools. You need these to build the app anyway. - Prevent sleep in System Settings → Energy so the runner stays available during releases.
The runner auto-receives the labels self-hosted, macOS, ARM64, which the release workflow matches on. Apple
Silicon can cross-compile x86_64 and universal builds, so a single ARM64 runner handles all three architectures. Yay!
If you have SSH access to the production server (ssh hetzner) and credentials for services like Umami, Cloudflare, and
Paddle, see docs/architecture.md for the full map of per-service
docs.
Happy coding!