| name | rust-strict |
| description | Rust security, strictness, and vulnerability prevention rules. Use when writing, reviewing, or auditing Rust code. Complements rust-skills (179 general rules) with security-focused rules: unsafe audit, unwrap/expect bans, error handling hierarchy, secret handling, concurrency safety, input validation for Tauri commands, and release profile hardening. Derived from production Rust projects.
|
Rust Strict Standard
Security and strictness rules complementing the existing rust-skills (179 rules).
These rules are derived from 5 production Rust projects.
CRITICAL: Workspace Lint Configuration
Every Rust project must configure workspace lints. Baseline:
[workspace.lints.rust]
unsafe_code = "deny"
unused_qualifications = "deny"
[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
Per-crate opt-in:
[lints]
workspace = true
Exceptions (feature-gated, never blanket)
#![cfg_attr(feature = "local-llm", allow(unsafe_code))]
static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"...").expect("regex must compile")
});
CRITICAL: Error Handling Hierarchy
Rule: Library crates use thiserror, app crates use anyhow
#[derive(Debug, thiserror::Error)]
pub enum GatewayError {
#[error("authentication required")]
AuthRequired,
#[error("rate limited: retry after {retry_after_ms}ms")]
RateLimited { retry_after_ms: u64 },
#[error("payload too large: {size} exceeds {max} bytes")]
PayloadTooLarge { size: usize, max: usize },
}
impl GatewayError {
pub fn status_code(&self) -> StatusCode { }
pub fn is_retryable(&self) -> bool { matches!(self, Self::RateLimited { .. }) }
}
fn main() -> anyhow::Result<()> {
let config = load_config().context("failed to load configuration")?;
Ok(())
}
Rule: Tauri commands return Result<T, String>
#[tauri::command]
pub async fn my_command(
state: tauri::State<'_, AppState>,
) -> Result<ResponseData, String> {
let data = do_work().map_err(|e| format!("operation failed: {e}"))?;
Ok(data)
}
Tauri serializes errors as strings: this is the framework constraint, not a hack.
Rule: Never Box<dyn Error>: use domain-specific enums
fn process() -> Result<(), Box<dyn std::error::Error>> { ... }
fn process() -> Result<(), GatewayError> { ... }
CRITICAL: Unsafe Audit Rules
When unsafe is acceptable (with mandatory SAFETY comment)
#[target_feature(enable = "avx2,fma")]
unsafe fn cosine_similarity_avx2(a: &[f32], b: &[f32]) -> f64 {
let va = unsafe { _mm256_loadu_ps(a_ptr.add(offset)) };
}
#[cfg(test)]
fn test_env_var() {
let _guard = env_lock();
unsafe { std::env::set_var("KEY", "value") };
}
When unsafe is NEVER acceptable
- String/buffer manipulation (use safe APIs)
- Pointer arithmetic without bounds proof
transmute (almost always wrong: use From/Into)
- Implementing
Send/Sync manually (unless wrapping C FFI)
unsafe impl without audited invariants
HIGH: Concurrency Safety
Rule: Never hold RwLock/Mutex across .await
let mut data = state.data.write().await;
data.insert(key, value);
let json = serde_json::to_string(&*data)?;
tokio::fs::write(path, json).await;
let json = {
let mut data = state.data.write().await;
data.insert(key, value);
serde_json::to_string(&*data)?
};
tokio::fs::write(path, json).await;
Rule: Never hold RwLock across disk I/O
let mut cookies = state.cookies.write().await;
cookies.insert(name, result);
std::fs::write(&path, serde_json::to_string(&*cookies)?);
let json = {
let mut cookies = state.cookies.write().await;
cookies.insert(name, result);
serde_json::to_string(&*cookies)?
};
tokio::fs::write(&path, json).await;
Rule: Use atomics for flags/counters, RwLock for data
pub struct AppState {
pub is_running: AtomicBool,
pub connection_count: AtomicUsize,
pub sessions: RwLock<HashMap<String, Session>>,
pub shutdown: watch::Sender<bool>,
}
Rule: BoundedMap for all caches (prevent memory leaks)
let cache: HashMap<String, CachedValue> = HashMap::new();
let cache = BoundedMap::new(1000, Duration::from_secs(300));
HIGH: Secret Handling
Rule: Secrets in Keychain, never plain files
#[cfg(target_os = "macos")]
fn keychain_get(account: &str) -> Result<Option<String>, String> {
let output = Command::new("security")
.args(["find-generic-password", "-s", "AppName", "-a", account, "-w"])
.output()
.map_err(|e| format!("keychain read failed: {e}"))?;
}
#[cfg(not(target_os = "macos"))]
fn save_fallback(app: &AppHandle, name: &str, value: &str) -> Result<(), String> { ... }
Rule: Use obfstr!() for hardcoded strings in binary
let api_url = "https://api.internal.example.com/v2";
let api_url = obfstr::obfstr!("https://api.internal.example.com/v2");
Rule: Validate empty strings on secret storage
pub fn save_secret(name: &str, value: &str) -> Result<(), String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err("Secret value cannot be empty".to_string());
}
}
HIGH: Input Validation (Tauri Commands)
Rule: Enum-constrain all command inputs
#[tauri::command]
fn spawn_agent(agent: String) -> Result<(), String> {
Command::new(&agent).spawn();
}
#[derive(Debug, Clone, Deserialize)]
pub enum AgentType { Claude, Codex, Opencode }
#[tauri::command]
fn spawn_agent(agent: AgentType) -> Result<(), String> {
let binary = match agent {
AgentType::Claude => "claude",
AgentType::Codex => "codex",
AgentType::Opencode => "opencode",
};
}
Rule: Use tempfile crate for temp files
std::fs::write("/tmp/prompt.txt", prompt)?;
let mut tmp = tempfile::NamedTempFile::new()
.map_err(|e| format!("temp file failed: {e}"))?;
writeln!(tmp, "{}", prompt)?;
let path = tmp.path().to_string_lossy().to_string();
MEDIUM: Dependency Rules
Rule: Explicit feature control
reqwest = "0.12"
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
Rule: rustls-tls over native-tls
Pure Rust TLS stack: no OpenSSL dependency, auditable, consistent behavior.
Rule: Workspace dependency inheritance
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }
HIGH: Type Safety Patterns
Rule: Newtype for IDs and units
pub fn ban_user(uid: Uuid, by: Uuid) { ... }
pub struct UserId(Uuid);
pub struct AdminId(Uuid);
pub fn ban_user(uid: UserId, by: AdminId) { ... }
Same pattern for units: Bytes(u64), Millis(u64), RetryCount(u32).
Rule: #[must_use] on results that should not be silently dropped
#[must_use = "builder is incomplete until .build() is called"]
pub struct RequestBuilder { ... }
#[must_use]
pub fn try_acquire(&self) -> Option<Lease> { ... }
Result<T, E> already has #[must_use]. Add it to builders, locks, and validation outcomes.
Rule: Explicit overflow handling on arithmetic
let next = current + delta;
let next = current.checked_add(delta).ok_or(Error::Overflow)?;
let next = current.saturating_add(delta);
let next = current.wrapping_add(delta);
Default to checked_* for anything user-influenced (sizes, counts, indices).
HIGH: Edition 2024
Edition 2024 ships with Rust 1.85 (Feb 2025). Current stable is 1.95.0. New crates start at edition 2024 with current stable. Migration changes that affect strict-mode rules:
Rule: Wrap external symbols in unsafe extern "C" blocks
extern "C" { fn external_fn(); }
unsafe extern "C" {
fn external_fn();
}
Forces the FFI declaration site to acknowledge unsafety.
Rule: unsafe_op_in_unsafe_fn is warn-by-default
Inside unsafe fn, every individual unsafe operation now needs its own unsafe { ... } block. This makes the audit surface explicit, do not bypass with #[allow].
Rule: Capture lifetimes explicitly with use<...> on RPIT
fn parse(s: &str) -> impl Iterator<Item = &str> + use<'_> { ... }
The use<> syntax is the escape hatch for the new capture rules. Reach for it when the inferred capture set is wrong.
Rule: gen is a reserved keyword
If you have variables, functions, or modules named gen, rename them before migrating.
Rule: tracing for structured logs, not log or env_logger
use tracing::{info, instrument, warn};
#[instrument(skip(state), fields(user_id = %req.user_id))]
async fn handler(state: AppState, req: Request) -> Result<Response, Error> {
info!("processing request");
...
}
tracing is the de-facto standard. Pair with tracing-subscriber for output and tracing-opentelemetry for distributed tracing.
MEDIUM: Release Profile
Desktop apps (smallest binary)
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = 3
panic = "abort"
Server/gateway (balanced)
[profile.release]
strip = true
lto = "thin"
codegen-units = 1
Dev profile (fast compile)
[profile.dev.package."*"]
opt-level = 3
Vulnerability Checklist