agentleFS
xmtpxmtp/libxmtp/.agents/skills/writing-rust/SKILL.md

writing-rust

Skill127 starsChanged 11 days agoMITView on GitHub

Use when writing or reviewing Rust in crates/, apps/backend, or bindings/ - covers the shared helpers to reach for (errors, retry, time, spawn, spans, newtypes, transactions), the crate and module conventions, and the anti-patterns lint does not catch

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 2 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Search for an existing helper before adding one.

    line 8
  2. Distinctive:

    Never copy a function between crates.

    line 11
  3. Very specific:

    Error control flow: use typed error variants or stable error codes.

    line 19
  4. Very specific:

    Never branch on Display, Debug, or error message text.

    line 20
  5. Very specific:

    Never std::time::Instant or tokio::time in shared code.

    line 23
  6. Very specific:

    Never bare tokio::spawn. See async.md.

    line 25
  7. Very specific:

    Constants: every number has a name. A value shared by more than one crate goes in xmtp_configuration.

    line 34
  8. Distinctive:

    Read one when you start that kind of work.

    line 102

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Writing Rust in libxmtp

Search for an existing helper before adding one. Shared code lives in a shared crate (xmtp_common, xmtp_proto, xmtp_configuration, xmtp_mls_group_state, xmtp_mls_group_keys, xmtp_mls_tls_collections). Never copy a function between crates; move it and import it. Read the package's AGENTS.md before working in it.

Essentials

  • Errors: thiserror enums, #[from] on sub-errors, #[derive(ErrorCode)] when the code crosses FFI, a hand-written RetryableError. Never stringify an inner error. See errors.md.
  • Error control flow: use typed error variants or stable error codes. Never branch on Display, Debug, or error message text. Preserve the typed cause and code across FFI and transport boundaries. Messages are for diagnostics only.
  • Time: xmtp_common::time::{now_ns, sleep, timeout, Duration, Instant}. Never std::time::Instant or tokio::time in shared code.
  • Tasks: xmtp_common::spawn(ready, fut) and keep the StreamHandle. Never bare tokio::spawn. See async.md.
  • Send bounds: MaybeSend + MaybeSync, BoxDynError, #[xmtp_common::async_trait]. Wasm has no Send.
  • Logging: tracing only. #[xmtp_common::rpc_span] / db_span / mls_span / err_span; log_event! for product events. Never log keys, credentials, payloads, or full ids, except full inbox and installation ids in client logs; backend telemetry follows OPS-014. See logging.md.
  • Ids: GroupId, InstallationId, Topic, Cursor from xmtp_proto::types, not Vec<u8>. See types.md.
  • Constants: every number has a name. A value shared by more than one crate goes in xmtp_configuration.
  • Client database: XmtpMlsStorageProvider::transaction returning TransactionOutcome; impl_store! / impl_fetch! for plain tables. See database.md.
  • Tests: #[xmtp_common::test(unwrap_try = true)]. See the writing-rust-tests skill.

Reach for the helper

NeedUsePath
Retry with backoffretry_async!(Retry::default(), (async { .. }))xmtp_common
Is this error retryableretryable!(err)xmtp_common
Wall clocktime::now_ns() -> i64, now_secs() -> i64xmtp_common::time
Bounded waittime::timeout(d, fut) -> Result<_, Expired>xmtp_common::time
Periodic workertime::jittered_interval_stream(base, jitter)xmtp_common::time
Spawn a taskspawn(None, fut) -> impl StreamHandlexmtp_common
Platform splitif_native! {} / if_wasm! {} / wasm_or_native! {}xmtp_common
Test-only itemif_test! {} (also test-utils), if_only_test! {}xmtp_common
HTTP clienthttp::client() / http::client_builder()xmtp_common::http
Token bucketrate_limit::Bucket::new(rate, burst)xmtp_common::rate_limit
Log an error, keep goingoptify!(result, "msg") -> Option<T>xmtp_common
Short id in a log lineid.short_hex()xmtp_proto::ShortHex
SHA-256sha256_array(&[u8]) -> [u8; 32]xmtp_common
Normalize a hex ids.normalize_hex()xmtp_common::hex::NormalizeHex
Nanosecond constantsNS_IN_SEC, NS_IN_HOUR, NS_IN_DAYxmtp_common
Group or installation idGroupId::try_from(&[u8]), InstallationId::from([u8; 32])xmtp_proto::types
Topic bytesTopic::new_group_message(id), Topic::parse(bytes)xmtp_proto::types
MLS message idutils::id::calculate_message_id(..)xmtp_mls
Own secret bytes (field, return, binding)Secret::new(bytes), .as_slice()xmtp_cryptography::secret
Transient secret bufferZeroizing::new(buf) where the buffer is madexmtp_cryptography::secret
Database column that holds a secretStoredSecret (BLOB, serde as a byte string)xmtp_db
Byte field of a stored libxmtp type#[serde(with = "xmtp_db::byte_string")] (byte_strings, optional_byte_strings)xmtp_db
Hand-written secret holderZeroize, ZeroizeOnDropxmtp_cryptography::secret
Prove a wipe in a testinstall_heap_canary!(), Canary, scan / scan_asyncxmtp_cryptography::secret::heap_canary

xmtp_common re-exports retry, wasm, stream_handles, const, event_logging, hash, and rand at its root. time, fmt, hex, http, snippet, types, and rate_limit need the module path.

Anti-patterns

Do notUse instead
loop { .. sleep .. } retryretry_async!(Retry::default(), (async { .. }))
reqwest::Client::new() / Client::builder()xmtp_common::http::client() / client_builder()
std::time::SystemTime::now() / Instant::now() in shared codexmtp_common::time::{now_ns, Instant}
tokio::spawn in a shared cratexmtp_common::spawn(None, fut); keep the handle
#[xmtp_common::timeout] in production (it panics)xmtp_common::time::timeout(..); handle Expired
Bare #[tracing::instrument] on an API, DB, MLS, or FFI fnrpc_span / db_span / mls_span / err_span
Logging a full id, key, or payloadomit keys and payloads; short_hex() ids, except client inbox and installation ids; skip_all
Vec<u8> for a group or installation idGroupId / InstallationId
Raw conn.transaction(..) in a client crateXmtpMlsStorageProvider::transaction(|tx| ..)
Hand-written Store / Fetch for a plain tableimpl_store! / impl_fetch!
TLS client before provider install (native)install_crypto_provider() at the entry point
Re-deriving an MLS message idutils::id::calculate_message_id(..)
Renaming an error variant silently#[error_code("Type::OldName")]
Editing schema_gen.rs, dist/, or src/gen/**the regeneration command in the owning AGENTS.md
println! in a library cratetracing; println! only in apps/* binaries
.ok() that hides an erroroptify!(result, "why")
Secret bytes in a bare Vec<u8>, [u8; N] or StringSecret, or Zeroizing<T> for a transient buffer. See types.md
#[derive(Debug)] on a type with a secret field whose Debug prints it (bare bytes, Zeroizing<T>, an OpenMLS HPKE key)a hand-written Debug that shows only the length, or a redacting field type (Secret, StoredSecret)
Formatting HpkePrivateKey, HpkeKeyPair, KeyPackageBundle, SecretVLBytes or Zeroizing<T> ({:?}, ?field)log a public value or a length; these Debug impls print the key
SigningKey::to_bytes() and other key-copying accessorsborrow (as_bytes()) or move (from_signing_key); clippy bans them

Reference

Read one when you start that kind of work.

  • errors.md — ErrorCode derive forms, RetryableError, the glossary, binding error wrappers
  • async.md — time, spawn and StreamHandle, retry, Send bounds, platform macros, locks, HTTP
  • logging.md — span macros, log_event!, the never-log list, id truncation
  • types.md — newtypes, configuration layout, cryptography, secrets and identity entry points
  • database.md — client transactions and table macros, backend storage rules
  • crates.md — crate layout, features, workspace deps, hakari, RustDoc, bindings

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.