fude (筆)
The brush for AI-native document editors. A Rust shell that pre-wires the three things every "human + agent + document" app needs — a sandboxed filesystem, safe PTY spawning for CLI agents, and an ACP client — and ships nothing you didn't ask for.
~3,000 lines of Rust in src/. ~1 MB binary with everything turned on.
Webview + sandboxed FS + PTY agents + ACP client. That is the whole scope.
use App;
That's a working webview app. Every other feature is one .with_*() away.
Why pick fude
Tauri and Electron are general-purpose: their runtime covers every kind of desktop app. Fude takes the opposite bet — pick one shape of app, ship only its primitives, stay small enough to audit.
| Tauri 2 | Electron | fude | |
|---|---|---|---|
| Binary (this app) | ~4.5 MB | ~80 MB | ~1.0 MB |
| Shell core LOC | ~100k+ (workspace) | N/A (JS) | ~3k |
| Scope | general | general | AI × docs only |
| Plugins | yes | yes | compile-time only |
| Agent primitives | via plugins | via IPC | built-in |
| Config surface | tauri.conf.json + capabilities |
main.js |
Rust builder |
| Maintainers | 200+ contributors | 1000+ | 1 (be aware) |
The last row is honest: fude is a one-person library, pre-1.0. The small surface is its strength and its fragility. If you need hundreds of eyes on your shell code today, use Tauri. If you'd rather read the shell code yourself before shipping it, read on.
What fude wires for you
Three things an AI-native editor needs that every team builds wrong at least once. Each is already in the box, tested, and gated by an opt-in builder.
Without fude With fude
───────────── ─────────
Write an FS allow-list: .with_fs_sandbox()
canonicalize every path,
block-list ~/.ssh, ~/.aws, …,
symlink-escape detection,
refuse write to existing symlinks (230 LOC you don't write)
Spawn a CLI agent safely: .with_pty(&["claude", "codex"])
allow-list by name,
resolve inside trusted dirs only,
scrub PATH before exec,
stream PTY output back to UI (350 LOC you don't write)
Talk to an ACP agent: .with_acp(…)
JSON-RPC over stdio,
route fs/read, fs/write through sandbox,
permission prompts from agent,
session-update streaming to UI (900 LOC you don't write)
If any of those three rows describes a problem you're actively solving, fude is pitched at you. If none of them do, fude is probably not the right tool.
Read it yourself
Not a black box. The "narrow on purpose" claim is verifiable in minutes:
| File | LOC | What it's responsible for |
|---|---|---|
src/sandbox.rs |
228 | Allow-list, block-list, canonicalization, symlink guard |
src/fs.rs |
225 | FS command handlers layered on sandbox |
src/pty.rs |
350 | Allow-listed PTY spawning + streaming |
src/acp.rs |
614 | ACP client (JSON-RPC, stdio, session bookkeeping) |
src/acp_commands.rs |
287 | ACP command handlers that bridge to the sandbox |
src/assets.rs |
394 | asset:// protocol serving ./dist or sandboxed files |
src/lib.rs |
530 | Builder, IPC bridge, event loop |
| (plus 5 smaller) | 463 | dialogs, shell, settings, events, … |
| Total | 3,091 |
If you plan to ship an app that touches a user's filesystem, being able to
audit the 228 lines that decide what write_file will and won't accept is
the feature.
5-minute quickstart
From empty directory to a running, sandboxed editor.
1. A new crate
2. The frontend (one file)
3. The backend (seven lines)
// src/main.rs
use App;
4. Run
You now have a desktop app where: the user picks a directory through a
native dialog, anything under that directory (and only that directory) is
readable and writable, and the sandbox refuses symlinks that escape, refuses
paths under ~/.ssh, ~/.aws, .git, and so on — regardless of what the
frontend asks for.
Layering on a CLI agent is one more line: .with_pty(&["claude"]).
Layering on an ACP agent is one more: .with_acp(…). Full examples in
examples/.
Philosophy
- Narrow on purpose. Features that don't serve human + AI + document stay out, even if they'd be easy.
- Sandboxed by default. File I/O is allow-list only. No path touches
disk until the user selects it through a native dialog. System dirs and
credential stores (
~/.ssh,~/.aws,.git, …) are blocked regardless of allow-list. - Agents are first-class. PTY for CLI agents and ACP clients are core primitives, not plugins.
- Web frontend, unopinionated. Ship a
dist/. Fude serves it overasset://and exposeswindow.__shell_ipc/window.__shell_listen. Pick any framework. - Builder, not DSL. One
App::new().with_x().with_y().command(…).run()chain. No config files. No macros.
What's in the box
Built on wry +
tao — the same webview/window pair
Tauri uses, but wired directly.
| Module | What it provides | Opt-in via |
|---|---|---|
| Core shell | Single window, asset:// protocol, window.__shell_ipc JSON bridge, server-push events |
App::new |
| Sandbox | Allow-list, atomic writes, blocked system/credential paths, symlink-escape checks | with_fs_sandbox() |
| Dialogs | File / folder pickers, message / question boxes (via rfd) |
with_dialogs() |
| PTY agents | Spawn allow-listed CLI tools from trusted install dirs; stream output as base64 | with_pty(&[…]) |
| ACP client | JSON-RPC over stdio to ACP servers, with sandboxed fs/* handlers and permission prompts |
with_acp(…) |
Each one is opt-in. App::new() alone is a webview and an IPC channel,
period.
Security model
Fude's sandbox has three layers:
- Absolute block-list — even with explicit user consent, Fude refuses
to read or write paths under system directories (
/etc,/var,/usr,/Library,/private/*, …) or credential stores (.ssh,.gnupg,.aws,.kube,.docker,.git,.netrc,.npmrc,Keychains). Comparison is case-insensitive and component-wise;/tmp/etcetera/notes.mdis fine,/Users/alice/.SSH/id_rsais not. - Symlink-escape detection — every path is canonicalized before checks.
A symlink inside an allowed dir that resolves outside is rejected.
write_file_binaryadditionally refuses to write to an existing symlink at all. - Allow-list only for everything else — nothing reads or writes until the user selects a file or folder through a native dialog, and even then only the chosen path (or its children, for directories) is accessible.
PTY spawning has its own layer: only tool names in your with_pty(&[…])
allow-list may be spawned, they must resolve to a binary in a trusted
install directory (/opt/homebrew/bin, ~/.cargo/bin, etc.), and the
child's PATH is overwritten — a compromised frontend cannot inject a
malicious binary.
ACP wraps the same sandbox: when an ACP agent asks to fs/read_text_file
or fs/write_text_file, Fude handles it on the agent's behalf, applying
the allow-list before touching disk.
Docs
docs/frontend-bridge.md— JS API and IPC command referencedocs/sandbox.md— sandbox threat model and exact guaranteesdocs/cookbook.md— state sharing, image rendering, Tauri migration, custom AI integration, …docs/ROADMAP.md— where fude is headed and what it won't grow intoexamples/—minimal,ipc-hello,pty-terminal,cowrite,acp-chat
Is this for you?
Yes if:
- You're building a note / markdown / prose / notebook editor where an AI agent touches the user's files.
- A ~1 MB binary and ~3k LOC of shell you can read matter to you.
- You'd rather write Rust than fight a config file.
No if:
- You need a general-purpose app shell — Tauri is the right call.
- You need tabs, multiple windows, tray icons, background services, plugin marketplaces. None of those are coming.
- You need many contributors, auditors, or an SLA today. Pre-1.0, one-maintainer — vendor it or wait.
Non-goals (explicit)
- Multi-window / tab management beyond one root window.
- Tray icons, global shortcuts, OS integration beyond dialogs.
- Runtime plugin loader. Add things in Rust at compile time.
- Hiding the webview. You're shipping web code; fude won't pretend otherwise.
- Replacing Tauri for general apps.
Naming
Fude (筆) — the brush. In Japanese calligraphy, the tool that touches the paper. Tauri named itself after the gate (鳥居); fude names itself after the implement. Narrower, more intimate, more honest about scope.
Status & MSRV
Pre-1.0. Extracted from a production app; the public API may still shift
before 1.0 — see docs/ROADMAP.md. Rust 1.93+
(edition2024 required by transitive deps). MSRV bumps are treated as a
minor-version change.
License
Licensed under either of Apache License, Version 2.0 (LICENSE-APACHE) or MIT (LICENSE-MIT) at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.
See CONTRIBUTING.md for the dev loop,
CODE_OF_CONDUCT.md for community expectations,
and SECURITY.md for reporting vulnerabilities.