RepoPilotOpen in app →

cjpais/Handy

A free, open source, and extensible speech-to-text application that works completely offline.

Healthy

Healthy across the board

weakest axis
Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 1w ago
  • 36+ active contributors
  • Distributed ownership (top contributor 40% of recent commits)
Show all 6 evidence items →
  • MIT licensed
  • CI configured
  • Tests present

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/cjpais/handy)](https://repopilot.app/r/cjpais/handy)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/cjpais/handy on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: cjpais/Handy

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/cjpais/Handy shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across the board

  • Last commit 1w ago
  • 36+ active contributors
  • Distributed ownership (top contributor 40% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live cjpais/Handy repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/cjpais/Handy.

What it runs against: a local clone of cjpais/Handy — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in cjpais/Handy | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch main exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 38 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>cjpais/Handy</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of cjpais/Handy. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/cjpais/Handy.git
#   cd Handy
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of cjpais/Handy and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "cjpais/Handy(\\.git)?\\b" \\
  && ok "origin remote is cjpais/Handy" \\
  || miss "origin remote is not cjpais/Handy (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify main >/dev/null 2>&1 \\
  && ok "default branch main exists" \\
  || miss "default branch main no longer exists"

# 4. Critical files exist
test -f "package.json" \\
  && ok "package.json" \\
  || miss "missing critical file: package.json"
test -f "src-tauri/Cargo.toml" \\
  && ok "src-tauri/Cargo.toml" \\
  || miss "missing critical file: src-tauri/Cargo.toml"
test -f "index.html" \\
  && ok "index.html" \\
  || miss "missing critical file: index.html"
test -f "vite.config.ts" \\
  && ok "vite.config.ts" \\
  || miss "missing critical file: vite.config.ts"
test -f "eslint.config.js" \\
  && ok "eslint.config.js" \\
  || miss "missing critical file: eslint.config.js"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 38 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~8d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/cjpais/Handy"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Handy is a free, open-source desktop application for offline speech-to-text transcription using Whisper and Parakeet V3 models. Users press a configurable keyboard shortcut to record audio, which is processed locally (with VAD filtering via Silero) and transcribed directly into any text field—no cloud connectivity required. Tauri desktop app: frontend in TypeScript/React with Vite (src/ directory, Tailwind CSS), backend in Rust (src-tauri/), coordinated via Tauri IPC. State managed with Zustand; i18n via i18next; CI/CD across .github/workflows/. Nix flakes support (.nix/, flake.nix) for reproducible builds.

👥Who it's for

Privacy-conscious users and accessibility advocates who need speech-to-text without cloud dependencies; developers who want to fork and extend transcription tooling; contributors building or improving offline ML inference on desktop platforms.

🌱Maturity & risk

Actively maintained at v0.8.3 with comprehensive CI/CD pipelines (build, test, release, Playwright workflows), cross-platform support (Windows/macOS/Linux), and established contribution guidelines. The codebase shows production-ready maturity with pre-release versioning suggesting feature stability rather than experimental status.

Single maintainer (cjpais) visible in repo structure creates continuity risk; large Rust/TypeScript codebase (~491KB + ~453KB) with moderate dependency surface area (20+ Tauri plugins). No obvious breaking-change history visible, but offline ML model management and platform-specific accessibility APIs (macOS permissions plugin) add integration complexity.

Active areas of work

v0.8.3 release cycle with active Playwright E2E testing setup, GitHub Actions workflows for cross-platform builds (build.yml, main-build.yml, release.yml), translation validation (check-translations.ts), and Nix environment standardization. macOS permissions plugin integration (tauri-plugin-macos-permissions-api) suggests recent platform-specific UX work.

🚀Get running

git clone https://github.com/cjpais/Handy && cd Handy && bun install && bun run dev (for frontend dev with Vite) or cd src-tauri && cargo build (for backend). See BUILD.md for platform-specific Rust/system dependencies.

Daily commands: Development: bun run dev (Vite dev server) + bun run tauri dev (Tauri window). Production: bun run build && bun run tauri build. Playwright E2E: bun run test:playwright.

🗺️Map of the codebase

  • package.json — Defines all frontend dependencies, build scripts, and project metadata; essential for understanding the tech stack and build process.
  • src-tauri/Cargo.toml — Rust backend configuration including speech-to-text engine dependencies and native module bindings; critical for offline transcription.
  • index.html — Main entry point for the web frontend; required to understand the app's initialization and structure.
  • vite.config.ts — Build configuration for frontend; controls bundling, Tauri integration, and development server setup.
  • eslint.config.js — Code quality baseline for all JavaScript/TypeScript contributions; enforced in CI pipelines.
  • .github/workflows/build.yml — Primary CI/CD pipeline for building cross-platform binaries; defines how releases are automated.
  • flake.nix — Nix declarative build environment; essential for reproducible development on NixOS and reproducible builds.

🛠️How to make changes

Add Support for a New Speech-to-Text Engine

  1. Add the speech-to-text library as a Rust dependency in Cargo.toml under src-tauri/ (src-tauri/Cargo.toml)
  2. Create a new Rust module in src-tauri/src/ to wrap the engine (e.g., src-tauri/src/engines/new_engine.rs) with an init() and transcribe() function (src-tauri/src/lib.rs)
  3. Define an IPC command (e.g., invoke_transcribe_with_engine) that calls your new engine module and returns JSON to the frontend (src-tauri/src/main.rs)
  4. Update frontend to call the new IPC command via @tauri-apps/api and add UI toggle/dropdown to select the engine in settings

Add a New Language or Translation

  1. Add a new JSON translation file in the i18n or locales directory (e.g., locales/fr.json for French) with all English keys translated (locales/)
  2. Update the language selector in the UI to list the new language code and load it dynamically (src/)
  3. Run the translation check script to validate the new language file has all required keys (scripts/check-translations.ts)
  4. Add the language to Tauri's bundled resources so translations are included in the binary

Add a New Configuration Setting

  1. Define the setting type and default value in Rust backend configuration module (e.g., src-tauri/src/config.rs) (src-tauri/src/config.rs)
  2. Create a Tauri IPC command (e.g., get_config, set_config) that reads/writes the setting to persistent storage (src-tauri/src/main.rs)
  3. Add a frontend form component in the settings UI that calls the IPC command on change and displays the current value (src/)
  4. Update the .vscode/extensions.json or linter config to ensure the new setting is documented and validated

Extend the Tauri Capabilities (Permissions)

  1. Define the new capability (e.g., allow microphone access, file read) in src-tauri/capabilities/default.json and src-tauri/capabilities/desktop.json (src-tauri/capabilities/default.json)
  2. Update macOS entitlements in src-tauri/Entitlements.plist to request the corresponding system permission (src-tauri/Entitlements.plist)
  3. Update the privacy info plist in src-tauri/gen/apple/PrivacyInfo.xcprivacy to declare the privacy reason for the permission (src-tauri/gen/apple/PrivacyInfo.xcprivacy)
  4. Call the new IPC command from the frontend using @tauri-apps/api; Tauri will prompt for user permission on first use

🔧Why these technologies

  • Tauri (Rust + WebView) — Provides lightweight cross-platform desktop shell with native OS integration (system tray, hotkeys, file access) while reusing web frontend; smaller binary footprint than Electron and better resource efficiency.
  • Vite + TypeScript — Modern, fast bundler for frontend development with hot module reload; TypeScript adds type safety for the JavaScript/UI layer interfacing with Rust via IPC.
  • Rust for backend — Enables safe, zero-cost native performance for real-time audio processing and model inference; tight integration with system APIs (microphone, hotkeys, tray).
  • Nix for build environment — Ensures reproducible, declarative builds across different OS environments and developer machines; simplifies NixOS support for package distribution.
  • GitHub Actions + Tauri signer — Automates cross-platform binary compilation and code-signing for Windows (EXE), macOS (DMG, code-signed), and Linux (AppImage); enables secure auto-updates.

⚖️Trade-offs already made

  • Offline-only transcription (no cloud API fallback)

    • Why: Aligns with privacy-first philosophy and eliminates dependency on external services; core value proposition.
    • Consequence: Initial model download can be large (100–500MB); requires local compute resources (CPU/RAM); accuracy may lag behind cloud-based services.
  • Single-

    • Why: undefined
    • Consequence: undefined

🪤Traps & gotchas

Nix dependencies: .nix/scripts auto-heal peer dep binaries; CI fails if node_modules canonicalization skipped (scripts/check-nix-deps.ts). macOS: Requires system permissions prompt grant (tauri-plugin-macos-permissions-api 2.3.0); test in sandbox. VAD & models: Silero VAD + Whisper/Parakeet V3 models auto-download on first run; network required once, then fully offline. i18n: New strings must be added to all locale files; check-translations.ts enforces completeness. Bun lock: bun.lock is committed; use bun for reproducibility (not npm).

🏗️Architecture

💡Concepts to learn

  • Voice Activity Detection (VAD) — Handy uses Silero VAD to filter silence before transcription, reducing latency and model load; understanding VAD improves UX decisions around recording thresholds.
  • Tauri IPC (Inter-Process Communication) — Bridges TypeScript frontend and Rust backend; shortcut events, transcription results, and settings flow through Tauri commands defined in src-tauri/src/.
  • GPU Acceleration via ONNX/cuDNN — Whisper models optionally offload to GPU (Handy detects & uses when available); understanding this explains performance variance across hardware.
  • Keyboard Hook / Global Shortcut Capture — Handy's core interaction model (press-to-record) relies on @tauri-apps/plugin-global-shortcut; platform-specific (WinAPI, Cocoa, X11) with accessibility trade-offs.
  • Declarative Build with Nix Flakes — flake.nix ensures reproducible builds across contributors' machines; essential for Rust + Node.js + system lib coordination without Docker.
  • Zustand Store (State Management) — Frontend state (settings, transcription history, model selection) lives in Zustand; lightweight alternative to Redux; easier to fork and extend.
  • Offline-First Model Distribution — Handy pre-bundles or lazy-downloads Whisper/Parakeet models; understanding model serialization (ONNX/PyTorch) and caching strategy informs storage & bandwidth decisions.
  • openai/whisper — Reference implementation of Whisper transcription models; Handy packages and optimizes these for offline desktop use.
  • tauri-apps/tauri — Core framework powering Handy's cross-platform Rust/JS architecture and plugin system (shortcuts, clipboard, updater).
  • SilenceVAD/Silero-VAD — Voice activity detection engine integrated in src-tauri; filters silence before sending audio to Whisper.
  • NVIDIA/Parakeet — Alternative CPU-optimized transcription model supported by Handy alongside Whisper; auto-detects language.
  • nix-community/home-manager — Handy's Nix module (nix/hm-module.nix) integrates with home-manager for declarative Linux/macOS installation.

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add Playwright E2E tests for core speech-to-text workflow

The repo has playwright.config.ts and a test:playwright script, but no visible test files in the file structure. Given that Handy is a Tauri desktop app with critical user-facing functionality (press shortcut → speak → text appears), E2E tests would catch regressions across platforms. This is high-value because the core feature involves system integration (global shortcuts, clipboard, audio input) that's easy to break.

  • [ ] Create tests/e2e/ directory structure
  • [ ] Write test for global shortcut activation in src-tauri/capabilities/default.json context
  • [ ] Write test for audio capture and transcription flow using Tauri plugin mocks
  • [ ] Write test for clipboard output validation across Linux/macOS/Windows using @tauri-apps/plugin-clipboard-manager
  • [ ] Add test fixtures for mock audio files if needed
  • [ ] Update BUILD.md or add TESTING.md with instructions for running E2E tests locally

Add unit tests for Zustand state management stores

The package.json shows zustand and immer as core dependencies for state management, but there are no visible test files (no *.test.ts or *.spec.ts in src/). Since Handy manages complex state like recording status, transcription results, language settings, and global shortcuts, unit tests for store actions and selectors would prevent subtle bugs and make refactoring safer.

  • [ ] Identify and list all Zustand stores in src/ directory
  • [ ] Create tests/ directory with store.test.ts files mirroring src/ structure
  • [ ] Write tests for state mutations using immer (e.g., recording start/stop, transcription updates)
  • [ ] Write tests for selectors to ensure only necessary components re-render
  • [ ] Add test coverage configuration to package.json (e.g., vitest or jest)
  • [ ] Document testing pattern in CONTRIBUTING.md

Add GitHub Actions workflow for Rust dependency security audits

The repo has 7 GitHub Action workflows but none explicitly run cargo audit against src-tauri/Cargo.lock to detect vulnerable Rust dependencies. Given the security-critical nature of Handy (audio input, file access, clipboard interaction), automated security scanning would catch vulnerabilities early and is standard practice for Rust projects.

  • [ ] Create .github/workflows/cargo-audit.yml workflow
  • [ ] Add cargo-audit check triggered on push to main, PRs, and weekly schedule
  • [ ] Configure to fail CI if vulnerabilities found (with option to allow advisory allowlist for known issues)
  • [ ] Add step to also run cargo-deny if additional supply-chain checks needed
  • [ ] Update BUILD.md or CONTRIBUTING.md to document how to run audit locally
  • [ ] Reference existing workflows like .github/workflows/build-test.yml for Rust environment setup pattern

🌿Good first issues

  • Add unit tests for src-tauri/src/ VAD filtering and audio segmentation logic (Silero integration); currently only Playwright E2E coverage exists.
  • Document platform-specific keyboard shortcut limitations in README.md (e.g., macOS accessibility API vs. Windows SetWindowsHookEx differences visible in src-tauri/src/).
  • Expand i18n locale files (new languages in src/locales/); add missing translation keys found by bun run check:translations for untranslated features.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • a385371 — refactor(nix): rely on cargo-tauri.hook standard phases (#1335) (xilec)
  • 085cd53 — release v0.8.3 (cjpais)
  • 8346bc2 — fix(nix): Fix for macOS build for nixpkgs (#1316) (xilec)
  • 4b7bb4e — docs(audio): clarify what the mic-init timing log actually measures (#1330) (rsanheim)
  • af6ec6c — chore(nix): add bun node_modules normalization scripts (#1256) (xilec)
  • c1e11fa — refactor(nix): replace manual bindgen env with rustPlatform.bindgenHook (#1255) (xilec)
  • a4d671a — fix: improve German translation quality (#1292) (ChristophNoetel)
  • aee682f — feat: add AWS Bedrock (Mantle) as post-processing provider (#1288) (jyuros)
  • 564fbc8 — docs: unify CLAUDE.md and AGENTS.md into single source of truth (#1272) (xilec)
  • 11311be — fix(overlay): parse HANDY_NO_GTK_LAYER_SHELL as boolean (#1269) (xilec)

🔒Security observations

  • High · SQL Plugin Usage Without Obvious Input Validation — src-tauri/Cargo.toml (implicit), Database query implementations in Rust backend. The application uses @tauri-apps/plugin-sql (~2.3.1) for database operations. Without reviewing the actual SQL query implementations in src-tauri, there's a risk of SQL injection if user input is directly concatenated into queries rather than using parameterized statements. Fix: Ensure all SQL queries use parameterized statements/prepared statements. Never concatenate user input directly into SQL queries. Use the database driver's parameter binding features.
  • Medium · Incomplete Dependency Version Pinning — package.json - multiple tauri plugin dependencies. Several dependencies use tilde (~) version constraints (e.g., @tauri-apps/plugin-sql ~2.3.1) which allow patch updates but not minor version changes. This could miss important security patches for minor versions. Some dependencies use caret (^) constraints allowing potentially breaking changes. Fix: Review and pin critical dependencies to exact versions. Use tighter version constraints for security-sensitive packages like @tauri-apps plugins and authentication-related libraries.
  • Medium · Potential XSS in React Application — src/ (frontend React components - not fully visible). The application uses React with i18n and user input handling. While React provides some XSS protection by default, risks exist if using dangerouslySetInnerHTML, innerHTML assignments, or improper sanitization of user-generated content (especially audio transcription results). Fix: Avoid dangerouslySetInnerHTML. Use React's default text rendering. If HTML rendering is needed, use a library like DOMPurify. Sanitize all user-generated content and transcription results before display.
  • Medium · Missing Security Headers Configuration — index.html, src-tauri/build.rs (if applicable). No visible Content Security Policy (CSP), X-Frame-Options, or other security headers in index.html or vite configuration. The application is Tauri-based but should still enforce strict security policies. Fix: Implement strict Content Security Policy headers. Add security headers like X-Frame-Options: DENY, X-Content-Type-Options: nosniff. Configure Tauri's security settings in tauri.conf.json.
  • Medium · Clipboard Plugin Without Data Validation — src-tauri/capabilities/default.json, clipboard operations in frontend. The application uses @tauri-apps/plugin-clipboard-manager (~2.3.2) which handles system clipboard operations. If clipboard data is used without validation (especially for transcribed text), it could be a vector for injection attacks. Fix: Validate and sanitize all data retrieved from the clipboard before use. Implement input validation for clipboard contents used in transcription or text processing.
  • Low · Global Shortcut Plugin Permissions — src-tauri/capabilities/default.json. The @tauri-apps/plugin-global-shortcut (~2.3.1) allows registering global keyboard shortcuts. Ensure capabilities are properly restricted in the default.json configuration to prevent unauthorized shortcut interception. Fix: Review and restrict global-shortcut permissions to only necessary commands. Implement proper capability-based access control. Document what shortcuts are registered and why.
  • Low · Store Plugin Data Encryption Not Visible — src-tauri (store plugin usage not visible in provided files). The @tauri-apps/plugin-store (~2.4.1) is used for persistent storage. Without visibility into the implementation, it's unclear if sensitive data (audio models, user preferences, API keys) are encrypted at rest. Fix: Ensure sensitive data stored via the store plugin is encrypted. Use encryption for any cached audio models or user-sensitive configuration. Document data storage practices.
  • Low · Auto-Update Plugin Security — src-tauri (updater configuration not visible). The @tauri-apps/plugin-updater (~2.10.0) handles application updates. Without visible configuration, ensure updates are verified via HTTPS and cryptographic signatures. Fix: Ensure update servers use HTTPS. Implement cryptographic signature verification for update packages. Keep updates coming from trusted, secure endpoints only.
  • Low · Missing CHANGELOG and — undefined. undefined Fix: undefined

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · cjpais/Handy — RepoPilot