Applies to: Rust workspaces across the ecosystem β CLIs and daemons
(fuz, fuzd), WASM bindings (blake3), web servers (zzz_server).
All use Rust edition 2024, resolver 2.
Each project's CLAUDE.md is authoritative for project-specific conventions.
This covers shared patterns.
- No backwards compatibility: Pre-1.0 means breaking changes. Delete old
code, don't shim.
- Code quality: unsafe_code = "forbid", pedantic lints, tests expected.
See Β§Lints.
- Performance: If it's slow, it's a bug. See ./rust-perf.
- Copious // TODO: comments: Mark known future work. todo!() is
warn workspace-wide β use #[allow(clippy::todo)] with justification
when needed.
Strict lint configuration in Cargo.toml:
[workspace.lints.rust]
unsafe_code = "forbid"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_lifetimes = "warn"
unused_qualifications = "warn"
[workspace.lints.clippy]
# Enable lint groups (priority -1 so individual lints can override)
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
# Common pedantic allows
module_name_repetitions = "allow"
must_use_candidate = "allow"
similar_names = "allow"
too_many_lines = "allow"
# Nursery overrides
significant_drop_tightening = "allow"
# Cargo allows (private repos)
cargo_common_metadata = "allow"
multiple_crate_versions = "allow"
# Restriction lints (panic points need explicit #[allow] with justification)
clone_on_ref_ptr = "warn"
dbg_macro = "warn"
expect_used = "warn"
panic = "warn"
todo = "warn"
unwrap_used = "warn"- missing_debug_implementations: "warn" in fuz and zzz; "allow" in
tsv (public types hold Chars, RefCell<Interner>, etc.); not set
in blake3.
- Crate-level overrides: FFI and binding crates (fuz_pty,
blake3_component, and any N-API/C-FFI/wit-bindgen layer) override
unsafe_code = "allow" because Cargo doesn't allow partial overrides β
they duplicate workspace lints. blake3_component also allows
same_length_and_capacity and use_self (false positives from
wit-bindgen generated code).
Each crate opts in with [lints] workspace = true.
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = trueSlower builds (~2x), no symbol names in backtraces. Worth it for binary size and performance.
blake3 exception: opt-level = "s" for smaller WASM. Individual builds
override via RUSTFLAGS.
thiserroruse thiserror::Error;
#[derive(Debug, Error)]
pub enum MyError {
#[error("file not found: {0}")]
FileNotFound(String),
#[error("invalid input: {0}")]
InvalidInput(String),
}Libraries export typed errors. Binaries wrap them with top-level error for display and exit codes:
// Library crate - typed errors
pub async fn call_tool(name: &str) -> Result<Value, ClientError> { ... }
// Binary crate - wraps library errors via #[from]
#[derive(Debug, Error)]
pub enum CliError {
#[error(transparent)]
Client(#[from] ClientError),
#[error(transparent)]
Artifact(#[from] ArtifactError),
}
// Central error handling in main()
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
if let Some(hint) = e.hint() {
eprintln!("{hint}");
}
std::process::exit(e.exit_code());
}
}impl CliError {
pub fn hint(&self) -> Option<HintMessage> { ... } // User-facing fix suggestion
pub fn exit_code(&self) -> i32 { ... } // Process exit code
}
impl ClientError {
pub fn hint(&self) -> &'static str { ... } // User-facing fix suggestion
pub fn is_transient(&self) -> bool { ... } // Retry might succeed
}
impl SidecarError {
pub fn is_recoverable(&self) -> bool { ... } // Should trigger restart
}Conventions (observed across fuz crates):
- Helpers belong on the binary's top-level error, not on every library
error. fuz_client::ClientError exposes .hint() because the message is
user-actionable. Library-level error types (parser errors, IO errors, etc.)
stay thin β variants only, no .hint() / .exit_code() β and the binary
wrapper adds the helpers when it composes them.
- .hint() returns Option<HintMessage> when most variants lack a hint
(CliError), or &'static str with "" for absent when all do
(ClientError). HintMessage (Static | Owned) handles the mixed case
where some hints interpolate runtime data (a PID, a path) and others are
pure constants.
- .exit_code() returns i32; reserve 1 for generic failure, 2+ for
category-specific (e.g., auth/token errors). Match arms over variants.
- .is_transient() / .is_recoverable() answer "should the caller retry or
restart?". Pure inspection, no side effects.
- Use #[source] on thiserror variants to chain causes; the Display impl
shows only the variant's own message, while the source chain surfaces via
e.source() for structured logging. Real example from fuz_client:
#[derive(Debug, Error)]
pub enum ClientError {
#[error("failed to parse RPC result: {context}")]
ResultParse {
context: &'static str,
#[source]
source: serde_json::Error,
},
#[error("failed to parse response")]
ResponseParse(#[source] reqwest::Error),
}No thiserror. Use JsError directly for wasm-bindgen:
pub fn keyed_hash(key: &[u8], data: &[u8]) -> Result<Vec<u8>, JsError> {
let key: [u8; 32] = key
.try_into()
.map_err(|_| JsError::new("key must be exactly 32 bytes"))?;
Ok(blake3::keyed_hash(&key, data).as_bytes().to_vec())
}For component model errors, see ./wasm-patterns.
Server/daemon crates use tokio with tokio-util's CancellationToken
for shutdown coordination. The pattern is consistent across fuz_server,
fuzd, and zzz_server.
A single CancellationToken owned at the top level. Clone it into every task
or component that needs to know about shutdown:
// fuzd/src/main.rs
let shutdown = CancellationToken::new();
// Spawn signal handler β flips the token on SIGINT/SIGTERM
let signal_token = shutdown.clone();
tokio::spawn(async move {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).expect("install SIGTERM");
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = sigterm.recv() => {}
}
signal_token.cancel();
});
// Pass into the server, which threads it into request handlers
let server = Server::new(addr, shutdown.clone(), /* ... */);
tokio::select! {
res = server.serve() => res,
() = shutdown.cancelled() => Ok(()),
}with_graceful_shutdownaxum integrates with CancellationToken directly. The server stops accepting
new connections when the token fires, but lets in-flight requests finish:
// fuz_server/src/lib.rs
let serve = axum::serve(listener, app).with_graceful_shutdown(async move {
shutdown.cancelled().await;
});
// Drain timeout: bound how long we wait for in-flight requests
tokio::select! {
res = serve => res?,
() = tokio::time::sleep(DRAIN_TIMEOUT) => {
tracing::warn!("drain timeout exceeded; forcing shutdown");
}
}Drain timeout is essential β without it, a hung handler keeps the process alive indefinitely.
select! for cooperative cancellationLong-running tasks (log flusher, background workers) check the shutdown
token via tokio::select!. The fuz_server log flusher uses Notify for
event-driven wakeups rather than a fixed interval β flushes are debounced
behind the most recent log call, so an idle daemon doesn't tick uselessly:
// fuz_server/src/logging.rs β notify-driven flush task
loop {
tokio::select! {
() = logger.notify.notified() => {}
() = shutdown.cancelled() => {
logger.flush(); // final flush before exit
return;
}
}
// ... debounce/throttle inner loop, also selects on shutdown ...
logger.flush();
}The two takeaways: every select! arm includes shutdown.cancelled(), and
every shutdown branch flushes pending work before returning.
TaskTrackertokio_util::task::TaskTracker is for waiting on a known set of spawned
tasks to finish. Use it when shutdown needs to verify "all worker tasks
exited cleanly" before the process exits. Skip it when the task is
short-lived or owned by an Arc-shared component that drops naturally.
- std::process::exit() from inside async code β bypasses Drop, leaks
file descriptors, skips graceful shutdown.
- Bare tokio::spawn with no shutdown awareness for anything that holds
resources (sockets, child processes, file handles).
- tokio::sync::broadcast as a poor-man's cancellation token. Use
CancellationToken β it's purpose-built and composes via .clone().
Standard Rust (snake_case / PascalCase / SCREAMING_SNAKE_CASE). Free
functions use natural Rust naming β not the domain-first domain_action
style of this stack's TypeScript code (see SKILL.md). fn parse(...),
fn create_artifact(...) β not fn artifact_create(...).
Style guidance the lint config encodes (clone_on_ref_ptr, panic,
unwrap_used warn). Ecosystem-specific bits are called out with examples.
Fixed variant sets β enum, not bool or sentinel string. The exhaustiveness
check turns every match into a contract that fires when variants change.
// Yes β exhaustive, named
pub enum AuditOutcome { Success, Failure }
// No β `bool` carries no name for what `true` means here
let success: bool = ...;
let kind: &str = "failure"; // typo-prone, no compiler helpThree patterns recur across the ecosystem:
Function pointers over trait objects for statically-known dispatch.
fuz_sidecar::SpawnConfig holds `build_command: fn(&Path, Option<&Path>)
-> Command instead of Box<dyn Fn(...)>` β no allocation, inlinable.
Callback resolution over allocating accessors in hot paths. For interned data or pooled resources, expose both an allocating accessor for one-off lookups and a callback form for tight loops:
let owned: String = printer.resolve_symbol(sym); // allocates
printer.with_resolved_symbol(sym, |s| out.push_str(s)); // zero-allocCow-shaped wrappers when some returns are pure constants and others
need interpolation. HintMessage (Static(&'static str) | Owned(String))
keeps the constant case allocation-free β same idea as Cow<'static, str>,
spelled out where API clarity matters more than terseness.
The clone_on_ref_ptr lint warns on arc.clone() β the workspace policy
is to use Arc::clone(&arc) instead, so the call site signals a refcount
bump rather than a deep copy. Other guidance:
- Don't clone to satisfy a borrow. Take &T or &str. A function that
takes String when it only needs &str forces every caller to allocate.
- Don't clone large collections just to pass them β take &[T] or
&HashMap<K, V>. Small, short-lived collections aren't worth Arc-wrapping
to dodge a single clone.
Rough preference for inputs: &str >> String. Same shape for collections
(&[T] >> Vec<T>). Reach for Cow<'_, str> only when callers genuinely
have mixed-ownership data and the borrowed case is common β otherwise the
cleverness isn't worth it.
project/
βββ Cargo.toml # Workspace config with shared deps, lints, profile
βββ crates/
β βββ {proj}_common/ # Foundation utilities (shared types, errors)
β βββ {proj}_*/ # Feature-specific crates
β βββ {proj}_cli/ # Production binary (or just {proj}/ β see below)
β βββ {proj}_debug/ # Dev binary (may use Deno sidecar)
β βββ {proj}_wasm/ # Interface crates: WASM, FFI, N-API
βββ tests/ # Integration tests (where applicable; fuz uses unit tests)
β βββ fixtures/ # Test fixtures (if applicable)
βββ docs/ # Architecture and reference documentationCrate naming: generally {project}_{crate} (fuz_common,
blake3_wasm_core). Exceptions: fuz's CLI is just fuz (not fuz_cli) and
its daemon is fuzd (not fuz_daemon) β short names for frequently-typed
commands.
- Foundation crate: Shared types (Span, errors, config) with minimal deps
(fuz_common, blake3_wasm_core)
- Feature crates: Domain logic with lib.rs public API
- Debug crate: Dev tooling, may embed external runtimes (Deno sidecars)
- Interface crates: Binding layers (CLI, C FFI, N-API, WASM)
- xtask crate: Dev automation (cargo xtask install), used by fuz
- Git version embedding (fuz, fuzd): cargo::rustc-env=FUZ_GIT_INFO={hash}
- Compile-time data (fuz_crypto): Parse/validate public keys, generate
constants
- Target triple (fuz_release): cargo:rustc-env=TARGET={triple}
cargo xtask install # Build, install to ~/.fuz/, restart daemon
cargo xtask install --new-token # Regenerate auth token
cargo xtask clean # Remove ~/.fuz/, stop daemonfuz separates dev config from prod config by source:
| Source | Sets | Read by | Notes |
| ----------------------- | ------------------- | ------------------------ | ---------------------------------- |
| .cargo/config.toml | FUZ_PORT | cargo run / cargo test | Checked in. Dev port (3621) only |
| ~/.fuz/config/env | FUZ_PORT, FUZ_AUTH_TOKEN | User shells (sourced) | Generated by cargo xtask install |
| systemd / Docker / etc. | FUZ_AUTH_TOKEN | prod daemon | Never sourced from ~/.fuz/config/env in prod |
# .cargo/config.toml
[env]
FUZ_PORT = "3621" # Dev port override (avoids conflict with prod port 3620)
[alias]
xtask = "run --package xtask --"Rule: .cargo/config.toml does NOT set FUZ_AUTH_TOKEN. Tokens are
secrets β they don't belong in a checked-in file, and they shouldn't be
silently inherited by every cargo run invocation. Dev tokens live in
~/.fuz/config/env (gitignored, mode 0600); prod tokens come from
systemd/Docker/secrets infrastructure.
This pattern generalizes: anything in .cargo/config.toml should be a
non-secret dev override. Anything secret or environment-dependent
goes in a generated, gitignored config file.
cargo test --workspace. Unit tests in #[cfg(test)] mod tests,
integration tests in tests/ where applicable. fuz uses unit tests in
modules; blake3 tests correctness in TypeScript (WASM vs native) and uses
Wasmtime for the component. See each project's CLAUDE.md for specifics.
Manual arg parsing (no clap) is the default β keeps binary size and compile
times down. The fuz shape is a simple match on the first arg in main.rs:
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
if let Some(hint) = e.hint() {
eprintln!("{hint}");
}
std::process::exit(e.exit_code());
}
}
#[tokio::main]
async fn run() -> Result<(), CliError> {
let args: Vec<String> = std::env::args().collect();
match args[1].as_str() {
"build" => build::cmd_build(&args[2..]).await,
"status" => status::cmd_status(&args[2..]).await,
_ => { print_usage(); std::process::exit(1); }
}
}Shared input modes: file path, --content <string>, --stdin.
Minimal dependency philosophy. Prefer workspace-level sharing. No new deps without explicit request.
| Crate | Purpose | Used by |
| ---------------------------------- | ------------------ | ---------------------------------- |
| serde, serde_json | Serialization | fuz, zzz_server (blake3 bench crate only) |
| thiserror | Error derivation | fuz, zzz_server |
| tracing, tracing-subscriber | Structured logging | fuz, zzz_server |
Async / networking (fuz, zzz_server):
| Crate | Purpose |
| ------------- | -------------------------------- |
| tokio | Async runtime |
| axum | HTTP server (built on hyper) |
| reqwest | HTTP client (fuz_client, fuz_storage, zzz_server) |
| tokio-util | CancellationToken, TaskTracker |
| parking_lot | Default for Mutex/RwLock (sync, no poisoning) |
Use std::sync::* only when you need poisoning semantics, and
tokio::sync::* only when the critical section needs to .await.
Never hold a parking_lot or std::sync guard across .await β it
blocks the executor thread and risks deadlock. Drop the guard before the
await, or switch to tokio::sync::*. See ./rust-perf Β§Async lock hygiene.
Database (zzz_server):
| Crate | Purpose |
| -------------------- | ------------------------------ |
| tokio-postgres | Async PostgreSQL client |
| deadpool-postgres | Connection pooling |
Auth / crypto:
| Crate | Purpose | Used by |
| --------------- | ------------------------------------------------------ | ---------- |
| blake3 | Content-addressed artifacts, session-token hashing | fuz, zzz |
| ed25519-dalek | Signing/verification | fuz |
| subtle | Constant-time comparison | fuz |
| zeroize | Secure memory clearing | fuz |
| argon2 | Password hashing (with rand feature) | zzz_server |
| hmac, sha2 | HMAC-SHA256 for signed cookies / keyring | zzz_server |
| base64 | URL-safe base64 for tokens | zzz_server |
Filesystem / OS (zzz_server):
| Crate | Purpose |
| -------- | ------------------------------------------------------------ |
| notify | File system watching (FSEvents on macOS, inotify on Linux) |
WASM / JS bindings:
| Crate | Purpose |
| --------------------- | -------------------------------------------------- |
| wasm-bindgen | JS interop (wasm-pack) |
| serde-wasm-bindgen | Serde bridging at the JS boundary |
| wit-bindgen | Component model (blake3) |
| napi, napi-derive | Node.js/Bun N-API bindings |
See ./wasm-patterns for build targets, WIT design, and optimization profiles.
fuz_sidecar hosts the pattern for managing long-running subprocesses that
multiplex many concurrent requests. The shape:
// Generic controller, configured per runtime
pub struct SpawnConfig {
pub build_command: fn(script: &Path, config: Option<&Path>) -> Command,
pub script: &'static str, // embedded via include_str!
pub tools: &'static [&'static str], // tool names this runtime exposes
}
// Per-request flow inside SidecarController:
// 1. Allocate a request ID
// 2. Park a oneshot::Sender<Result<Value, _>> in a HashMap keyed by ID
// 3. Send a WireRequest over the child's stdin
// 4. Reader task parses WireResponses line-by-line, looks up ID, fires oneshot
// 5. Caller awaits the oneshot::Receiver
pub struct SidecarController {
pending: HashMap<u64, oneshot::Sender<Result<Value, SidecarError>>>,
// tx for outbound requests, child process handle, etc.
}Key design choices worth lifting to similar systems:
- Function pointers in SpawnConfig instead of trait objects β zero-cost,
no allocation per spawn. Works because runtime configs are statically known
at compile time.
- JSON-lines framing: one JSON object per line over stdin/stdout. Simple
to parse with tokio::io::AsyncBufReadExt::lines(), no length prefixes,
trivially debuggable with tee.
- mpsc command channel + oneshot response channel: callers don't share
the child's stdin directly; they send ControllerCommands to a serializer
task that owns the stdin handle. Responses route back via per-request
oneshot::channel().
- Embedded script via include_str!: single-binary distribution. The
script is written to a tempfile at spawn (held alive via
NamedTempFile for the controller's lifetime) and passed to the child as a
path argument.
When to use this pattern:
- A long-running subprocess that handles many requests (vs. spawn-per-request) - The protocol is naturally request/response with correlation IDs - You want to keep the daemon's address space lean (no embedded runtime)
When to skip it:
- One-shot subprocess invocations β just use tokio::process::Command
- Pure in-process work β use a regular Arc<Mutex<...>> pool
- Constant-time token comparison via subtle::ConstantTimeEq
- TOCTOU-safe file operations: Open with O_NOFOLLOW, check permissions
on fd not path
- Secure file permissions: 0o600 for files, 0o700 for directories
- Environment isolation: Strip sensitive env vars before spawning sidecars
When a value progresses through a sequence of states β parse β validate β authorize β dispatch, or unauthenticated β authenticated β closed β encode the state as a type parameter rather than a runtime field. Each transition consumes the value and returns it under a new state type, so calling a method in the wrong phase is a compile error, not a runtime check.
use std::marker::PhantomData;
pub struct Unauthenticated;
pub struct Authenticated;
pub struct Session<S> {
inner: SessionInner,
_state: PhantomData<S>,
}
impl Session<Unauthenticated> {
pub fn authenticate(self, token: &str) -> Result<Session<Authenticated>, AuthError> {
// verify token, then:
// Ok(Session { inner: self.inner, _state: PhantomData })
}
}
impl Session<Authenticated> {
pub fn send(&self, msg: Message) -> Result<(), SendError> { /* ... */ }
}Calling send() on Session<Unauthenticated> fails to compile β the
method doesn't exist for that state. PhantomData<S> is zero-sized;
the compiled binary is identical to a hand-written single-state API.
When it fits:
- ActionSpec-shaped dispatch pipelines (parse β auth β validate β
rate-limit β dispatch β respond): each phase produces a typed handle
the next phase consumes.
- Builder APIs where .build() before required fields are set should
fail to compile, not at runtime.
- Connection / socket lifecycles: Closed β Connecting β
Handshaking β Established β Closing.
- Filesystem transaction phases: Staged β Committed / RolledBack.
When to skip it:
- The set of states is dynamic or driven by data β a runtime state machine (enum) is clearer and supports collections of mixed states. - Only one transition exists β the ceremony outweighs the win; a plain method that returns the next type is enough. - The API is meant to be ergonomic for casual callers β type-state shows up in every signature and in compiler error messages.
Type-state is primarily a correctness pattern, not a performance
pattern. The runtime check it removes (an if authenticated branch)
is usually well-predicted and not a hot-path cost. The real win is
that invalid sequences become unrepresentable. Performance wins, when
they exist, are downstream of the optimizer seeing dead branches at
compile time.
Servers (zzz_server): tracing with tracing-subscriber for structured
logging. axum integrates with tracing natively. Use tracing::info!,
tracing::error!, etc.
CLIs / daemons (fuz, fuzd): eprintln! β simple, no framework.
Batched request logging for performance. --json for machine-readable
output.
Doc comments (///) for public API; inline comments (//) for
implementation notes. // TODO: is the standard marker for known future
work β see Β§Core Values. Each project's CLAUDE.md has detailed
conventions.