Introduction
Halfway through an AI coding run, you want to glance at whether CPU is spiking again — I barely write code myself these days; I type prompts and bounce ideas off a few agents, and it's often several AI IDEs executing coding tasks in parallel, the fans whirring hard enough that I know CPU has hit 100% without even opening a gauge, so you really want something always visible to glance at. But switching to Activity Monitor breaks your flow. You set a 25-minute Pomodoro, but the timer is buried in some browser tab. Your frequently used internal web pages and tools are scattered across the bookmarks bar, the Dock, and everywhere else. Passwords live in a notes app. A question about your own machine pops into your head and you want to ask an AI — but that means opening yet another app…
Each of these is trivial on its own, but repeated dozens of times a day, they add up to constant context switching. Parallel multi-agent work is indeed fast, but the price is that the machine stopped belonging to just me long ago — you need a small screen keeping an eye on it for you. Sprite is that screen: an always-on desktop floating widget, semi-transparent and parked in a corner, folding system monitoring, focus timing, common shortcuts, secrets, and a pocket AI into a single surface.
It's actually a multi-module little tool. Below I walk through how it's built and what the source looks like, organized around the everyday desktop frictions I kept hitting.
TL;DR
- Sprite is an always-on desktop floating widget (Tauri v2 + React) that folds system monitoring / Pomodoro / quick launcher / password vault / built-in LLM / multi-Harness into one window
- System monitoring uses native macOS
df -H(disk) +sysinfo(CPU/memory); the frontend polls every 5 seconds, with 50% / 80% dual-threshold color warnings - The built-in LLM answers questions about the current machine (reads live data: "who's eating CPU"), and Sprite plugs into resolve-studio / spring-harness etc. as a pocket Agent console
- The password vault is added as a "Custom Config" in Settings; its value is stored encrypted locally (never plaintext), with optional master-password lock; local, private, and never leaves the machine; the whole app can have a lock-screen password
- Tauri v2 bundles to ~25MB (reuses the system WebView on macOS instead of bundling Chromium); memory sits steady at 80–120MB
- Minimal display: hit
─to collapse the whole widget into a360×36always-on strip (Esc/⤢restores); each panel can collapse on its own - Clipboard history keeps the last 20 entries and auto-skips sensitive content (keys / card numbers / IDs); a global hotkey
Cmd+Option+Dtoggles visibility; chats export to Markdown / JSON
Minimal, Yet Always There: The Collapse Posture
If every one of those features grabbed a big window and stole focus, Sprite itself would become a new source of distraction. Its baseline is "minimal display" — it stays small first, and only grows when you ask it to.
The window is built to not intrude. It floats in a corner, stays out of the Dock task flow, never steals focus — present in your peripheral vision while you work, one glance away when you want it.
What the window looks like is fixed by a few lines in tauri.conf.json:
{
"app": {
"macOSPrivateApi": true,
"windows": [
{
"title": "Sprite",
"transparent": true,
"alwaysOnTop": true,
"decorations": false,
"shadow": false,
"resizable": true,
"width": 420,
"height": 500,
"minWidth": 340,
"minHeight": 400
}
]
}
}
decorations:false (no system title bar), transparent:true (semi-transparent background), alwaysOnTop:true (always pinned), and the initial size is only 420×500. Note macOSPrivateApi:true — on macOS a transparent window must enable this private-API switch, or the transparent area falls back to black.
When you want it even smaller, hit ─ on the window to collapse the entire widget into a 360×36 always-on strip (useMinimize.ts: MIN_W=360, MIN_H=36) — CPU/memory and the H/S/R quick-open shortcuts stay on the strip, Esc or ⤢ restores it with position and always-on-top preserved, and it follows the current window across monitors so it never lands on the wrong screen. Individual panels (e.g. ResolvePanel) can also collapse into their own strip, so you only see the piece you care about. The tray icon brings the window back whenever it's collapsed away.
That loops back to the opening scene: your AI agents are halfway through a coding run, you glance at CPU — it should be exactly this light and this close, so light it doesn't deserve an "open / close" ritual.
Beyond collapsing, two more touches make "always there" effortless: a global hotkey Cmd+Option+D toggles the whole widget's visibility (remappable in Settings, and it auto-re-registers on save — hotkey.rs); and when you want a big view, hit ⛶ to fill the current display via setSize + setPosition — deliberately not macOS native fullscreen, which grabs its own Space and pushes other windows away, and whose combination with a transparent window is a known black-screen combo (noted in an App.tsx comment).
The Rust command behind show/hide is straightforward — unregister_all first to avoid duplicate registration, then on_shortcut listens for the press and toggles hide / show + set_focus based on current visibility:
// hotkey.rs
#[tauri::command]
pub fn register_toggle_hotkey(app: AppHandle, hotkey_str: String) -> Result<(), String> {
let shortcut = parse_hotkey(&hotkey_str)?;
app.global_shortcut().unregister_all().ok(); // clear old binding first
app.global_shortcut().on_shortcut(shortcut, |app, _s, event| {
if event.state() != ShortcutState::Pressed { return; }
if let Some(win) = app.get_webview_window("main") {
if win.is_visible().unwrap_or(false) {
let _ = win.hide();
} else {
let _ = win.show();
let _ = win.set_focus();
}
}
})?;
Ok(())
}
System Status at a Glance: Always-on Widget & Monitoring
The most direct need: you want to check system resources often, but you don't want to leave your current work to do it. Sprite makes this an always-on floating widget — semi-transparent, non-focus-stealing — where CPU, memory, and disk are one glance away.
The backend system_stats.rs does the collection. There's an easy mistake here: disk is not read via the sysinfo crate. sysinfo only handles CPU and memory in this project; disk goes through the native macOS df -H command:
// system_stats.rs (sketch)
Command::new("df").arg("-H")... // disk, 1000-base, naturally aligns with Finder
// sysinfo handles cpu / memory
df -H is 1000-base by nature, so its numbers match Finder within 0.01GB — what the user sees in Finder and in Sprite should be the same. Public IP is a different story: it's a network request, not locally readable, so the backend caches it for 5 minutes (public_ip fetch logic uses the cache when < 300 seconds), producing only 1–3 requests per day.
The frontend useSystemStats.ts polls every 5 seconds:
const timer = window.setInterval(refresh, 5000);
Idle CPU for the monitoring module is under 0.5%, and frontend-to-Rust RPC latency is mostly 2–8ms. Color warnings come from loadClass:
export function loadClass(percent: number): string {
if (percent >= 80) return "load-high"; // red: act now
if (percent >= 50) return "load-mid"; // amber: take note
return "load-low"; // green
}
50% is "take note", 80% is "act now" — I checked the trigger frequency of these two bands under real load and they matched expectations: amber shouldn't stay lit (or it loses meaning), red shouldn't go months without lighting (or it stops warning).
As a bonus, the widget also works as a focus timer: built-in usePomodoro + PomodoroTimer, giving a lightweight prompt on work/rest switches without stealing focus. Though given how I work now, this Pomodoro is arguably set for the AI agents — work a while, rest a while, and don't keep the CPU pinned in the red; whether I'm setting their pace or they're setting mine, I'd rather not dig into it.
One-Tap Web Apps: Quick Launcher
The second friction: frequently used internal pages, docs, and native apps are scattered across the bookmarks bar, the Dock, and everywhere. Sprite gathers them into a row of entries in the widget via launchers.*.json — to add your own entry, just edit the local JSON file:
urlentries: open common web pages in one tap (GitHub, erishen.cn, etc.)appentries: launch native apps directly (Terminal, VS Code, Finder)scriptentries: run local scripts
Your own entries go straight into launchers.local.json (.gitignored, purely local); the default layout lives in launchers.public.json and can be committed to share — customizing private entries without leaking personal config. The backend launcher.rs runs these commands. Once there are many entries, HudLinks.tsx also offers live keyword filtering: type to filter, matches are highlighted, Esc clears it.
How does "tap to open" work on the backend? launcher.rs maps the three kinds to different local commands, branching per platform with #[cfg(target_os)] — macOS goes through the system open:
// launcher.rs
#[tauri::command]
pub fn launch(kind: String, target: String) -> Result<(), String> {
if target.trim().is_empty() { return Err("empty launch target".into()); }
match kind.as_str() {
"app" => launch_app(&target), // macOS: `open -a "<App>"`
"bundle" => launch_bundle(&target), // macOS: `open -b "<bundle-id>"`
"script" => run_script(&target), // `/bin/sh -c "<script>"`
other => Err(format!("unknown launch type: {other}")),
}
}
#[cfg(target_os = "macos")]
fn launch_app(name: &str) -> Result<(), String> {
Command::new("/usr/bin/open").arg("-a").arg(name).spawn()?;
Ok(())
}
Note: the error-message strings in the snippet above have been translated into English for readability; the original
launcher.rssource returns these errors with Chinese messages.
target comes entirely from your own launchers.*.json, not remote content, so there's no sandboxing here — it's a local shortcut you authorized.
Passwords No Longer Scattered: Local Password Vault
The third friction: account passwords shouldn't live in a notes app. Sprite ships a local password vault — added in Settings as a "Custom Config" — where each entry is a custom item: you give it a button label (e.g. "GitHub password") and a value. The value is stored encrypted: on write, crypto.ts AES-GCM-encrypts it into local localStorage, so it never lands in plaintext; if a master password is enabled, the vault must be unlocked before the content is shown. The whole vault is local, private, and never leaves the machine — it talks to no server, and even copying goes through the 1-second skip window so it never enters clipboard history. It supports full export (exports the ciphertext, via dataManagement.ts) for backup.
The whole app can also have a lock-screen password that auto-locks when idle — a passerby sees a locked widget, not your secrets.
In short: when you have sensitive credentials to keep, prefer this local vault over a notes app, browser autofill, or anything cloud-synced — local, private, never leaves the machine, and built exactly for this.
Ask About "This Machine" Anytime: Built-in LLM (core)
The fourth, and my most-used, thing: a question about your own machine pops up and you want to ask an AI on the spot. A normal chat tool can't answer "who's eating CPU right now" or "why did memory climb" because it can't see your live system. Sprite's built-in LLM can — it runs on the same machine as the backend, which always has the latest SystemStats.
Once builtinApiKey / builtinBase are configured, BuiltinPanel can be summoned anytime to ask about the current system state. In the merge logic mergedConfig.ts, config.llmConfigured is true only when both builtinApiKey and builtinBase are non-empty:
llmConfigured: !!(builtinApiKey && builtinBase),
That avoids a misleading "connected" state when unconfigured. This is my most-used entry point: glance at the widget → ask a quick question → get an answer backed by live data. Conversations can be kept too — the built-in LLM and all three Harness panels export their history to Markdown or JSON in one tap (chatExport.ts calls Rust save_export_file into the Downloads folder, or copies straight to the clipboard).
Bring Agent Services In Too: Multi-Harness Integration
The fifth friction: I run several Agent services (resolve-studio, spring-harness, and a generic harness protocol — usually served by resolve-harness), and normally I'd open each of their UIs to use them. Sprite acts as their frontend directly:
ResolvePanelconnects to resolve-studio's agent (tools + sandbox, streamed)SpringPanelconnects to spring-harness's ReAct Agent (SSE stream:thinking → tool → answer → done)HarnessPanelconnects to any service speaking the generic harness protocol (harness_chat/harness_approve/harness_models; resolve-harness is the current one)
Each is configured independently: resolveBase :8787, springBase :8080, harnessBase :8899. useBackends probes resolve_health / spring_models / harness_health at startup, and the HUD shows online/offline/checking with lit/dim/checking dots. So the widget isn't just a monitor — it's a pocket Agent console on my desktop.
The Tauri Side: How the Always-On Widget Is Built
That all these capabilities fit into "one always-on panel" comes down to Tauri v2 as the skeleton. Below are a few real source snippets showing how "always-on, transparent, summonable, and able to persist locally" land in code.
1. App skeleton (lib.rs)
tauri::Builder wires up the plugins, the invoke_handler (every Rust command the frontend can call), and the setup initialization. On launch it pins the main window to the top-right of the display, then registers the global shortcut and builds the tray:
// src-tauri/src/lib.rs (excerpt)
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.invoke_handler(tauri::generate_handler![
system_stats::system_stats,
launcher::launch,
export::save_export_file,
// …resolve / spring / harness / panel / hotkey / keychain
])
.setup(|app| {
// Pin the main window to the top-right of the display
if let Some(window) = app.get_webview_window("main") {
if let Some(monitor) = app.primary_monitor()? {
let scale = monitor.scale_factor();
let screen = monitor.size().to_logical::<f64>(scale);
let win = window.outer_size()?.to_logical::<f64>(scale);
window.set_position(tauri::LogicalPosition::new(
(screen.width - win.width).max(0.0), 0.0))?;
}
}
// Register global shortcut, build tray, start tray poll …
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2. Sub-window: transparent borderless + "always-on-top retry" (panel.rs)
Opening a Harness panel spins up another transparent borderless window via WebviewWindowBuilder. always_on_top can "fail to stick" for some window types on macOS, so the code force-re-asserts it after 150ms:
// panel.rs (excerpt)
let window = tauri::WebviewWindowBuilder::new(&app, &label, WebviewUrl::App("index.html".into()))
.inner_size(w, h)
.position(x, y)
.transparent(true) // borderless + semi-transparent
.always_on_top(true)
.decorations(false)
.shadow(false)
.resizable(false)
.build()?;
window.set_always_on_top(true);
window.show();
window.set_focus();
// On macOS some window types ignore always_on_top; force it again after 150ms
let window_clone = window.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(Duration::from_millis(150)).await;
let _ = window_clone.set_always_on_top(true);
let _ = window_clone.show();
let _ = window_clone.set_focus();
});
3. Tray: background poll rebuilds the menu (tray.rs)
The tray menu is rebuilt every 15 seconds by a background task — futures_util::join! concurrently probes the three backends plus one system-stats pull, then writes it back via set_menu:
// tray.rs (excerpt)
pub fn start_tray_poll(app: &tauri::AppHandle) {
let app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
let (r, s, h, sys) = futures_util::join!(
resolve_studio::resolve_health("http://127.0.0.1:8787".into()),
spring_harness::spring_models("http://127.0.0.1:8080".into()),
harness::harness_health("http://127.0.0.1:8899".into()),
system_stats::system_stats(),
);
if let Some(tray) = app.tray_by_id("hud-tray") {
tray.set_menu(Some(build_tray_menu(&app, /* backend status + system stats */)));
}
tokio::time::sleep(Duration::from_secs(15)).await;
}
});
}
4. Export: a minimal Tauri command (export.rs)
Chat export lands in ~/Downloads — at its core it's just a #[tauri::command] that reads an env var, builds the path, and writes the file:
// export.rs (excerpt)
#[tauri::command]
pub async fn save_export_file(file_name: String, content: String) -> Result<String, String> {
let home = std::env::var("HOME").map_err(|e| format!("failed to read HOME: {e}"))?;
let downloads_dir = PathBuf::from(&home).join("Downloads");
if !downloads_dir.exists() {
fs::create_dir_all(&downloads_dir).map_err(|e| format!("cannot create Downloads: {e}"))?;
}
let file_path = downloads_dir.join(&file_name);
fs::write(&file_path, content).map_err(|e| format!("write failed: {e}"))?;
let path_str = file_path
.to_str()
.ok_or_else(|| "invalid path".to_string())?
.to_string();
Ok(path_str)
}
These snippets together are the technical skeleton of the "always-on desktop panel": config decides the window's look, the Builder decides what's callable, sub-windows decide the multi-panel layout, the tray decides the background heartbeat, and the commands decide local persistence.
Grab and Go: Clipboard History
One more friction nobody dedicates a tool to: you just copied a command, an ID, an error — then switched away, and the original window is long scrolled past. Sprite quietly records the clipboard — useClipboardHistory polls once per second (dialed back from 500ms to 1s to save CPU), keeps the last 20 text entries, dedupes, clears on demand, and the panel lets you search and click-to-re-copy.
The key point is it respects privacy: content matching credit-card numbers, ID numbers, phone numbers, API keys / tokens, AWS keys, JWTs, or PEM private keys is auto-skipped and never stored (SENSITIVE_PATTERNS); copying a password also sets a "skip window" flag so the master password never leaks into history. That's the same instinct as the password vault being "local, private, never leaves the machine" — the more an always-on desktop tool can read sensitive context, the more it must guard it locally. So when you actually need to keep credentials safe, don't stash them in a notes app or rely on cloud sync — use the local password vault above: local, private, never leaves the machine, which is exactly where account passwords and API keys belong.
After Using It: Before vs After
Before, doing these things meant bouncing between Activity Monitor, browser tabs, a notes app, another AI app, and each Agent's UI. Now they all live in one semi-transparent panel:
- Lightweight: Tauri v2 bundles to ~25MB (frontend 3MB + Rust backend 12MB + Tauri runtime 10MB), because macOS reuses the system WebView instead of bundling Chromium — an order of magnitude smaller than similar Electron apps (typically 80–150MB); memory sits steady at 80–120MB, with no noticeable growth over 24 hours.
- Always-on, non-intrusive: semi-transparent, non-focus-stealing; idle monitoring CPU under 0.5%.
- Extensible: the built-in LLM plus multi-Harness integration turn the widget from a "dashboard" into a console you can query and drive Agents from.
It doesn't solve anything earth-shattering — it just folds dozens of daily context switches into a small panel you barely notice is there.
Source Code Navigation
Rust backend (src-tauri/src/)
system_stats.rs— system monitoring data collection (CPU/memory viasysinfo, disk via nativedf -H, 5-minute public IP cache)builtin.rs— built-in LLM chat commandresolve.rs/resolve_studio.rs— resolve-harness / resolve-studio integration commandsspring_harness.rs— spring-harness ReAct integration commandharness.rs— generic harness protocol integration (harness_chat/harness_approve/harness_models), currently implemented by resolve-harnesskeychain.rs/utils/keychain.ts— LLM / Harness API Key wrapper and system Keychain read/write (keychain_save/keychain_get/keychain_delete, withkeychain_availableprobing)launcher.rs— quick launcher (url / app / script) commandspanel.rs/tray.rs/hotkey.rs/settings.rs— windows, tray, hotkeys, settingsexport.rs— chat-history file export (save_export_fileinto Downloads)
Frontend (src/)
-
App.tsx— main window, lock screen, backend health orchestration,launchers.*.jsonmerge -
useSystemStats.ts— system monitoring polling (5s) -
hooks/useBackends.ts— resolve/spring/harness health probes (resolve_health/spring_models/harness_health) -
BuiltinPanel.tsx— built-in LLM chat window -
ResolvePanel.tsx/SpringPanel.tsx/HarnessPanel.tsx— the three Harness chat windows -
utils/mergedConfig.ts— config merging (settings > .env > defaults) -
utils/crypto.ts/hooks/useCustomItems.ts/components/CustomItemsManager.tsx— password vault (Custom Config): value AES-GCM-encrypted into local localStorage, with optional master-password lock -
useMinimize.ts— collapse the whole window into a360×36always-on strip (MIN_W=360/MIN_H=36,Escrestores) -
hooks/useClipboardHistory.ts/components/ClipboardHistory.tsx— clipboard history (poll 1s, last 20, sensitive-content regex skip; 1s skip-window on password copy) -
utils/chatExport.ts— chat export to Markdown / JSON (exportChatMessages→ Rustsave_export_file) -
components/HudLinks.tsx— quick launcher rendering and live keyword filtering -
config/launchers.public.json— committable public quick actions -
This project's GitHub repository: https://github.com/erishen/sprite
-
Related pluggable Harness projects:
- resolve-studio — agent with tools + sandbox (resolveBase :8787)
- spring-harness — PSE three-role orchestrated ReAct Agent (springBase :8080)
- resolve-harness — the reference implementation of the generic harness protocol: a Python agent skeleton with LangGraph orchestration + LiteLLM model routing + layered memory + a fast path (harnessBase :8899)
Reproducibility
If you want to run it locally and see these numbers, the environment is roughly:
| Item | Version/Status |
|---|---|
| OS | macOS (Apple Silicon) |
| Tauri Version | v2 |
| Frontend | Vite + React 19 + TypeScript |
| Package Manager | pnpm |
| Backend Dependency | sysinfo (CPU/memory) + native df -H (disk) |
Frontend and backend run on the same machine, so system_stats RPC latency is essentially just inter-process overhead. A few key constants are all in the source for you to check:
- Refresh interval:
window.setInterval(refresh, 5000)inuseSystemStats.ts - Public IP cache:
< 300seconds insystem_stats.rs - Color thresholds:
loadClassinuseSystemStats.ts(50% / 80%) - Backend ports:
resolveBase :8787/springBase :8080/harnessBase :8899inmergedConfig.ts
Bundle size: run pnpm tauri build and check the output directory. Memory: check process RSS in the system monitor — it stabilizes at 80–120MB after launch. Disk precision is the easiest to verify: run df -H in the terminal and compare with disk_used_gb / disk_total_gb in the widget; they should match to two decimal places.
The Sprite README says "polled every 3s", but the code actually refreshes every 5 seconds. Is this discrepancy reasonable?
Yes. The 5-second interval is a design trade-off after empirical measurement: the RPC latency from frontend to Rust backend is only 2-8ms, the system monitoring module consumes less than 0.5% CPU in idle state, and users perceive almost no delay. The 3s documentation was an initial planning value, while 5s is the optimized result after baseline data validation, striking a balance between refresh timeliness and resource overhead.
How is disk usage kept consistent with macOS Finder?
The backend calls the native macOS df -H command to read disk data (sysinfo crate only handles CPU/memory); df -H output is naturally 1000-base, so empirical testing shows an error of less than 0.01GB compared to Finder. This is a trade-off between precision and compatibility—choosing 1000-base over 1024-base ensures the numbers align with the system tools users are familiar with.
What is the configuration merge priority strategy?
settings JSON file > .env file > code defaults. Null values fall back correctly, and multi-backend configurations (Resolve Harness :8899, Spring Harness :8080, Resolve Studio :8787, built-in LLM) are merged independently. The built-in LLM is marked as configured only when both builtinApiKey and builtinBase are non-empty, avoiding misleading status displays.
Why is there a 5-minute cache for public IP?
This is a trade-off between network overhead and data freshness. Empirical results show that a 5-minute cache reduces public IP requests to 1-3 per day, significantly cutting unnecessary network consumption. Users don’t perceive cache expiration because IP changes occur at a frequency far lower than 5 minutes.
How were the system load color thresholds (50% amber, 80% red) determined?
These are experience-based thresholds derived from user experience observation. 50% is a mild reminder point of “start paying attention”, and 80% is a clear warning point of “needs intervention”. Most users can tolerate loads below 50% without perceiving lag, while above 80% the system is noticeably slow. These two breakpoints strike a balance between visual warnings and false positives.
How do Sprite's bundle size and memory footprint perform?
Tauri v2 bundles to about 25MB (frontend ~3MB, Rust backend ~12MB, Tauri runtime ~10MB); on macOS it reuses the system WebView instead of bundling Chromium, making it an order of magnitude smaller than similar Electron apps (typically 80-150MB). Memory footprint stabilizes between 80-120MB after launch, with no significant growth over 24-hour continuous runs, indicating no serious memory leaks. The system monitoring module idles below 0.5% CPU, and frontend-to-Rust RPC latency is mostly 2-8ms. These are measurements from my real working environment with limited samples, provided only as baseline references.
Which Harness services can Sprite plug into, and how does it know they're online?
Sprite has four built-in chat backends: the built-in LLM (BuiltinPanel), resolve-studio (ResolvePanel, an agent with tools + sandbox, streamed), spring-harness (SpringPanel, a ReAct Agent with SSE stream thinking → tool → answer → done), and the generic harness protocol (HarnessPanel — any service implementing it can plug in; synchronous /api/chat returning reply + tool-call trace, and supporting harness_approve / harness_models; resolve-harness is the current one). Each is configured independently via resolveBase :8787 / springBase :8080 / harnessBase :8899; useBackends probes resolve_health / spring_models / harness_health at startup, and the HUD shows online/offline status with lit/dim/checking states. llmConfigured is true only when both builtinApiKey and builtinBase are non-empty, avoiding misleading “connected” states.
How is the password vault data kept secure?
The vault is “Custom Config” in Settings — each entry has a button label plus a value. The value is stored encrypted: on write, crypto.ts AES-GCM-encrypts it into local localStorage, so it never lands in plaintext; what export (dataManagement.ts) produces is also ciphertext. With a master password enabled, the vault must be unlocked before the content is shown. Copying a password also triggers the 1-second skip window so it never enters clipboard history. The whole thing is local, private, and never leaves the machine. On top of that, the whole app can have a lock-screen password that auto-locks when idle, so a passerby can’t read the floating window. (For the record: the LLM / Harness API keys are a separate path — they go through keychain.rs into the macOS system Keychain, distinct from the vault’s local encrypted storage.)
Leave a reply