Your First Hook
This chapter walks through accept-all, the minimal starter Hook: it
traces a short message, then unconditionally accepts the transaction that
triggered it. No loops, no state, no emitted transactions — a good template
to copy for a new Hook and a good way to see every required piece in one
small file.
Before the code, two concepts this whole book builds on:
- The struct is a declaration vessel, not a runtime instance. A
#[hooks]struct never gets constructed and never holds real data at runtime — it exists so a name, its fields, and the Hook entries that use them can all be declared in one place. Every entry receives this chain declaration by shared reference (&self), evenAcceptAll, which declares no fields — later chapters, once a chain has declared fields worth reading, show how an entry reaches them asself.<field>. - Every Hook entry has an explicit index. The index is this crate’s
Hook’s position in the account’s
Hooksarray (the one aSetHooktransaction installs into) — position0,1, and so on, up to9. Even a crate with exactly one Hook must say which position it occupies; there’s no implicit “the only one.”
The source
Create src/lib.rs in the crate you set up in Installation:
#![no_std]
use rshooks::*;
#[hooks(description = "Accepts every transaction selected by HookOn.")]
pub struct AcceptAll;
#[hooks]
impl AcceptAll {
/// Accepts every triggering transaction.
#[hook(0, name = "accept", on = [Invoke])]
fn main(&self) -> exit::HookResult {
trace!(b"accept-all: accepting transaction");
accept!()
}
}
This first hook exits through accept!() — the simplest thing that works
before Ok/Accept are introduced. The idiomatic typed exit
(Ok(Accept::from_code(0)) and ?-propagated errors) is covered in
Accept, Rollback, and Errors;
both compile to the same wasm here.
To see the trace! line actually run, enable the trace feature in
Cargo.toml alongside rshooks:
[dependencies]
rshooks = { version = "0.2.3", features = ["trace", "host-panic-handler"] }
#![no_std]
Every Hook crate is #![no_std]: there’s no allocator and no std on the
Hook host, and rshooks itself is no_std so it can be linked into one.
use rshooks::*;
This glob import brings in everything declared at rshooks’s crate root:
the #[hooks] attribute macro, the XFL!/account_id! macros, the
accept!/rollback!/trace!/guard! macro family, and every top-level
module (api, types, xfl, …) by name. It does not bring the
functions inside those modules into scope — a Hook that calls typed API
functions like otxn_field or state also needs use rshooks::prelude::*;,
which this minimal example doesn’t, since it never reads the transaction or
touches state. Later chapters that do add that import.
#[hooks(description = "...")] on the struct
#[hooks] on struct AcceptAll; declares this crate’s Hook chain: a
container for shared state/parameter fields (none here — AcceptAll is a
unit struct, so
there’s nothing to declare) plus the optional description, free-form text
carried into the build’s generated sidecar. The struct name (AcceptAll)
is yours to choose; it plays no on-ledger role.
#[hooks] on the impl block, and #[hook(0, ...)]
The second #[hooks] attribute, on impl AcceptAll, marks this as the
chain’s entry-point block — exactly one such impl is required per
#[hooks] struct. Inside it, #[hook(0, name = "accept", on = [Invoke])]
declares one Hook entry:
0— the required, positional first argument: this Hook occupies position0in theHooksarray. Explained further in Hook Chains.name = "accept"— the on-ledgerHookNamethis Hook installs with (optional; omit it for an unnamed Hook).on = [Invoke]— this Hook fires only forInvoketransactions. Omittingonentirely is also legal — see Per-Hook Attributes for what that means.
The annotated function itself is an associated function taking &self and
returning exit::HookResult — reached here as exit::HookResult because
use rshooks::*; already brought the exit module in by name (see
above). AcceptAll declares no fields, so main never reaches through
self here, but the receiver is still required — #[hooks] expands it
into the wasm export the Hook host requires:
#[unsafe(no_mangle)]
pub extern "C" fn hook(_reserved: u32) -> i64 {
::rshooks::exit::EntryReturn::finish(AcceptAll::main(&AcceptAll))
}
The function’s own name (main here) is just a convention; what matters is
the hook export it produces for this entry’s build. &self is the one
receiver form #[hooks] accepts on an entry — no lifetime, not mut — and
it’s how an entry reaches a chain’s declared fields once there are any to
reach, covered in Hook State. Any other receiver shape
(self, mut self, &mut self, self: T), a missing receiver, or a
return type that doesn’t implement the sealed EntryReturn trait (in
practice, anything but rshooks::exit::HookResult) is rejected at compile
time with a pointed error rather than a malformed export. Use #[cbak(0)]
the same way, on the same index, to declare the optional settlement
callback for this entry.
accept!()
accept!() calls the host’s accept function and never returns — its
return type is !. accept!(msg, code) additionally carries a trace
message and a caller-chosen result code; the bare form used here accepts
with no message and code 0. Its counterpart, rollback!(msg, code),
rejects the transaction instead. Both are covered in more depth in Accept,
Rollback, and Errors.
Building it
From the crate’s own directory:
rshooks build --out out
or from elsewhere, pointing at its manifest:
rshooks build --manifest-path my-hook/Cargo.toml --out my-hook/out
--out out picks where the build publishes its output; omit it and
rshooks build publishes under <target>/rshooks/<crate-name> instead
(see “What lands in out/” below). This compiles your crate once to
discover its declared Hook(s), then once more per declared index, and
post-processes each result — see Building a Hook for
exactly what that pipeline does. A successful build prints something like:
discovery build (my-hook)
building entry 0 (`main`)
wrote out/current/0.main.wasm (171 bytes, estimated SetHook fee 855000 drops)
wrote out/current/0.main.metadata.json
wrote out/current/sethook.template.json
wrote out/current/sethook.template.meta.json
The guard checker’s/validator’s own numbers (worst-case instructions, max
nesting depth) aren’t printed here — they land in the metadata sidecar
below instead, or print on the terminal via rshooks check out/current/0.main.wasm;
see Building a Hook for that
report’s exact shape.
What lands in out/
rshooks build writes into a generation directory under its output root
— out/ above, since --out out was passed; the default, if --out is
omitted, is <target>/rshooks/<crate-name>, where <target> is cargo’s
own target directory — with <root>/current symlinked to the latest
generation (see Hook Chains for why generations
exist). For AcceptAll, out/current/ contains:
0.main.wasm— the cleaned, SetHook-valid binary for index0: cargo’s rawcdyliboutput with thememoryexport stripped and every Hook API rule (§ singlehook/cbakexport, guarded loops, MVP-only instructions) validated. The file name is<index>.<fn>.wasm— one file per declared entry, so a multi-Hook chain gets one independent binary per index.0.main.metadata.json— this entry’s metadata sidecar:
{
"index": 0,
"hook_fn": "main",
"cbak_fn": null,
"name": "main",
"description": null,
"HookOn": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFFFF",
"HookCanEmit": null,
"HookName": "616363657074",
"HookHash": "68A7D6BD77FB98A7E05E0BFD12180352903F131B2780F848267C37DC0837707C",
"WCE": {
"hook": 14,
"cbak": 0
},
"builder": {
"name": "rshooks-build",
"version": "0.2.3",
"rustc": "rustc 1.89.0 (29483883e 2025-08-04)",
"cargo_args": ["rustc", "--release", "--locked", "--target", "wasm32v1-none", "--crate-type", "cdylib"],
"rustc_args": ["--cfg", "rshooks_entry=\"0\"", "--check-cfg", "cfg(rshooks_entry,values(\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\"))", "-C", "link-arg=-zstack-size=131072"],
"wasm_opt": true
},
"human": {
"on": {
"form": "list",
"HookOn": ["Invoke"],
"HookOnIncoming": null,
"HookOnOutgoing": null
},
"HookCanEmit": null,
"HookName": "accept"
},
"chain": {
"struct": "AcceptAll",
"description": "Accepts every transaction selected by HookOn.",
"decls": { "state": [], "hook_params": [], "otxn_params": [] }
}
}
builder records the toolchain provenance for this build. A crate built
with the unstable-param-sig-interface feature additionally carries a
sig_params field (the entry’s declared typed signature parameters);
on a stable build like this one the key is absent entirely. The full
grammar for every field here is covered in
Per-Hook Attributes.
sethook.template.json/sethook.template.meta.json— a ready-to-editSetHooktransaction template covering every index this crate declares, plus a sidecar recording how it was generated. Covered in full in Hook Chains and Per-Hook Attributes.
The WCE (worst-case execution) numbers are the static, guard-derived
upper bound on instructions the host will ever execute for this entry’s
hook/cbak — the same figures the pipeline printed to the terminal. The
HookHash is Xahau’s hash of the deployed binary: the uppercase hex of
the first 32 bytes of the wasm’s SHA-512 digest — this is what identifies
the exact Hook code on-ledger, independent of which account installed it.
The chain object transcribes this crate’s shared struct-level schema
(empty here, since AcceptAll declares no fields) — every entry’s sidecar
carries the same chain object, since the schema is shared across the
whole crate, not owned by any one entry.
From here, Building a Hook explains what each pipeline
stage actually does, Hook Chains covers the
multi-Hook model this build pipeline exists for, and The rshooks
CLI is the complete flag reference for every subcommand.