diff --git a/PLAN.md b/PLAN.md
index 3fa02bc0..a4ccd34c 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -90,7 +90,7 @@ The leverage point: every `foo.c` already ships `foo_ext.h` (its public `extern`
 | **P8 — Delete the C + retire the oracle harness ✅ DONE** (per-sub-phase: [`PLAN-P8-progress.md`](PLAN-P8-progress.md) + [`docs/progress-log/p8.md`](docs/progress-log/p8.md)) | remaining C (the ~25 variadic sender trampolines + residual data globals) + `ircd-sys` C build + `ircd-golden` (L2) + `ircd-testkit`/`cref_*` archives (L1) + the frozen reference‑C tree | With all C _logic_ already Rust (P7 exit), delete the last C: replace the variadic sender trampolines with a non‑variadic Rust sender API at every call site (faithful `wire!`/`reply_one!`/`format!` first; leveva typed messages adopted alongside the IRC senders), drop the residual data globals via the data‑symbol seam (bindgen `extern` decls resolve to `#[no_mangle]` Rust defs at link), then drop **all** C compilation from `ircd-sys` (no more `cc::Build`), retire the differential harness (`cref_*`/L1 `ircd-testkit`/`ircd-golden` L2 can no longer be built once the oracle is gone), and **migrate the load‑bearing tests into `ircd-common`** as self‑contained Rust tests (own fixtures, no `cref_` oracle). **Progress (P8a–P8r — full entries in the progress logs):** **every variadic sender trampoline is now Rust** (P8a–P8m, incl. the keystones `sendto_one`/`sendto_flag`/`esendto_*`), and the five data‑global `.o`s are dropped outright (`s_auth.o` iauth globals P8n, `send.o` P8o, `s_bsd.o` `local[]`/`highest_fd`/… P8p, `ircd.o` `me`/`client`/timers/… P8q). **P8r dropped the last C‑logic TU `support.o`** — `dgets`/`make_isupport` ported to Rust + the `ipv6string`/`minus_one` data globals defined as `#[no_mangle]` statics (`snprintf_append` is dead in Rust — WHOX is field‑by‑field — so it died with the `.o`; `irc_sprintf` is deleted not ported, in P11). **Every ircd C TU is now Rust:** the link set holds only the generated `version.o` + the L1 harness's `ctruth.o`. **P8s migrated all 115 L1 `cref_` differentials into self‑contained `ircd-common` `insta` snapshot tests** (drive only the Rust port via `link_anchor()`, no oracle; soundness via the capture chain) — the suite is green + 0 warnings. **P8t ported the last *generated* C TU `version.c` to Rust** (`ircd-common/src/version.rs`): the data globals `generation`/`creation`/`pass_version`/`infotext`/`isupport` are now `#[no_mangle]` statics via the data‑symbol seam — `generation`/`creation` sourced from crate metadata at build time (`CARGO_PKG_VERSION` + an `ircd-common/build.rs` UTC build stamp), `pass_version`/`infotext` copied verbatim. `version.c.SH` generation + the `version.o` compile/archive are gone from `ircd-sys/build.rs`; `libircd_c.a` now holds **only** the L1 harness `ctruth.o`. (`creation` is now genuinely build‑time volatile, so the golden canonicalizer masks RPL_CREATED 003, matching the existing 371 Birth‑Date rule.) **P8u (FINAL) retired the oracle.** Ran the differential suite a last time green — L1 `ircd-testkit` all pass; L2 `ircd-golden` 86 pass + only the documented reference‑C‑garbage `s_serv_stats` flake (where Rust is the *correct* side) — then **dropped all C compilation** from `ircd-sys/build.rs`: the `make` object build, the `cref_*`/`ctruth.o`/`res.o` recompiles, the `libircd_c.a` archive, and the whole‑archive link + `-lz`/`-lm`/`-lcrypt` (none needed by Rust — `pow` resolves from glibc 2.29+). `build.rs` now only runs `configure` + bindgen over the C *headers* (the struct view `ircd-common` still uses until P9–P12) and expands the install‑path Makefile vars. `ircd-testkit` (L1) + `ircd-golden` (L2) are `exclude`d from the workspace (mothballed, git‑recoverable); the `ctruth.c`/`layout.rs` drift‑net is deleted. **The workspace is now 100% Rust — zero C TUs compiled.** **P8v (literal end of C) deleted the C *source tree* itself + retired `ircd-sys`:** froze the generated `bindings.rs` into a committed `ircd-common/src/bindings.rs` (a self‑alias `extern crate self as ircd_sys;` keeps every `ircd_sys::bindings::*` path resolving, zero src churn) + the install‑path consts as literals, then deleted `ircd-sys`, the mothballed `ircd-testkit`/`ircd-golden` oracle crates, the `iauth-rs` C‑iauth differential, and **all** C source (`common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* clangformat.yml`). `ircd-rs` calls `ircd_common::ircd::c_ircd_main` directly. **Not one line of C remains in the repo.** | **MET. Final differential run green; oracle mothballed; `cargo build --workspace` 0 warnings links pure Rust; `cargo test --workspace` 695 pass / 0 fail (P8v; was 698 before deleting the C‑oracle tests); the `ircd-common` snapshot tests carry the reference‑C correctness forward. C source tree deleted — repo is 100% Rust at the file level.** |
 | **P9 — std‑library cleanup ❌ RETIRED (2026‑06‑08)**     | — (was: whole Rust tree)                                                                                                                                                   | **Skipped by decision — superseded by the `leveva` greenfield.** P9 assumed an *in‑place* idiomatic transformation of the mechanical port (`MyMalloc`/`MyFree`→`Box`/`Vec`, `dbuf` freelist→owned, raw `libc`→`std`/`nix`, `[c_char;N]`→`[u8;N]`). Because the idiomatic end‑state is now a separate greenfield crate (`leveva`), `ircd-common` is throwaway code (the oracle, deleted at parity), so polishing it earns nothing. Nothing was implemented; the row is kept for provenance. | n/a — retired. |
 | **P10 — `leveva` greenfield foundations 🔶 IN PROGRESS** ([`docs/progress-log/p10.md`](docs/progress-log/p10.md)) | new `leveva` crate (lib) + `leveva-integration` differential tests                                                                                                        | The idiomatic‑Rust product, built from scratch (not transformed from the port). **Done so far:** RFC 1459 case‑folding (`casemap`); typed identifier strings (`IrcStr`/`IrcString` + `Nick`/`ChanName`/`Uid`/`Sid`/`Cid`/`ServerName`/`UserName`/`HostName` validating newtypes via `ident_newtype!`); `Message`/`MessageBuilder`; the full `Numeric` reply/error enum (184 codes, discriminant = wire code); a generic safe `patricia` trie; glob `matching`; `mode`; KDL `config` (model/parse/password/privilege); `rustls`/`tokio` `tls` endpoint with hot reload; `ident`. The async boot path (`main.rs`) binds listeners + watches certs but currently drops accepted connections — no protocol layer yet. Idiomatic Rust allowed everywhere; pulls real crates (tokio/rustls/kdl/nom/clap). | `cargo test -p leveva` green + clippy clean per module; `leveva-integration` pins each greenfield reimplementation against `ircd-common` (the oracle) within their shared domain (documented divergences asserted explicitly). |
-| **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 254 slices.** The slices cover the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash across every block, a channel-mode rework, CertFP, the **IRCv3 track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track), elemental channel modes + extbans, KNOCK, command-rate flood protection (fakelag), the leveva-native channel modes `+C`/`+c`/`+S`/`+j`/`+z`/`+T`/`+g`/`+r` (the `+z` op-moderation track 235–238 — local + network-wide S2S op-redirect for PRIVMSG/NOTICE/TAGMSG/REDACT via the statusmsg `@<chan>` seam — now complete; `+T` no-NOTICE, charybdis `MODE_NONOTICE`, slice 239; `+g` free-invite, charybdis `MODE_FREEINVITE`, slice 240; `+r` registered-only join, charybdis `MODE_REGONLY`, slice 241; `+f` channel forwarding, charybdis `MODE_FORWARD`, slice 242 — a forwardable JOIN reject redirects to the `+f` target with `470`; `+Q`/`+F` forward-target control, charybdis `MODE_DISFORWARD`/`MODE_FREETARGET`, slice 243 — `+F` lifts the op-on-target requirement for setting `+f`, `+Q` makes a channel refuse incoming forwards; `+P` permanent channel, charybdis `MODE_PERMANENT`/`chm_staff`, slice 244 — an oper-only flag that stops an empty channel being destroyed, so its topic/modes persist with zero members; `+L` large ban list, charybdis `MODE_EXLIMIT`/`chm_staff`, slice 245 — an oper-only flag that raises the per-list size cap on `+b`/`+e`/`+I` from 100 to 500, `478 ERR_BANLISTFULL` when a normal list is full — **the last missing standard charybdis channel mode**; the `max-bans`/`max-bans-large` caps `+L` toggles between are promoted to REHASH-able `options { max-bans / max-bans-large }` knobs, slice 246 — the const→knob trajectory `MAX_PENALTY` took); the user mode `+D` deaf, charybdis `UMODE_DEAF`, slice 247 — a client-settable, purely-local umode whose holder receives no channel-addressed messages (the channel fan-out filter runs on the deaf user's home server, so it is not in `SEND_UMODES`); the user mode `+R` registered-only messages, charybdis `UMODE_REGONLYMSG`, slice 248 — a client-settable, purely-local umode whose holder refuses a **private** `PRIVMSG`/`NOTICE` from an unidentified sender (`486 ERR_NONONREG`), bypassed by an identified/operator/accepted sender; the local-sender plane (slice 248); the `+R` remote-sender S2S plane, slice 249 — the inbound `relay::inbound_message` re-runs the same `callerid::regonly_msg_blocks` gate on the target's home server for a *remote* sender (neither `+R` nor `+g`/`+G` is in `SEND_UMODES`), bouncing one `486 ERR_NONONREG` to the sender's uplink for a blocked PRIVMSG (no notify pair, unlike caller-id) and silently dropping a blocked NOTICE — the caller-id 216→217 split repeated, closing the `+R` family; the user mode `+Q` no-forward, charybdis `UMODE_NOFORWARD`, slice 250 — the *user*-side counterpart of the channel `+f` forwarding feature: a client-settable, purely-local umode whose holder is never redirected by a `+f` channel (a forwardable JOIN reject falls through to the source's original numeric instead of `470` + redirect; the gate runs on the joiner's home server, so not in `SEND_UMODES`; the disjoint channel `+Q` `MODE_DISFORWARD` reuses the same letter), closing the `+f` family; the user mode `+G` soft caller-id MODE echo fix, slice 251 — `command::mode::umode_diff` omitted `SoftCallerId`, so a bare `±G` set the bit but echoed an empty trailing (local-echo repair to the existing slice-218 mode; not in `SEND_UMODES`, no snapshot/004 change); the user mode `+z` operwall + the `OPERWALL` command, charybdis `UMODE_OPERWALL`, slice 252 — the oper-only sibling of `+w`/WALLOPS: an operator broadcasts to every `+z` operator network-wide (`OPERWALL` command propagates, each server fans to its own local `+z`), `+z` is oper-gated to set like `+s` and purely-local (not in `SEND_UMODES`), a near-exact mirror of the slice-178 WALLOPS machinery (`command/operwall.rs` + `s2s/operwall.rs`); the user mode `+S` service, charybdis `UMODE_SERVICE`, slice 253 — a **server-set-only** umode (settable only on an S2S burst, a client/oper `MODE +S` silently ignored like `+Z`) whose holder is **immune to an operator's `KILL`** (`481` "Cannot kill a network service"; an inbound *server* KILL is still honoured, matching charybdis's `MyClient`-gated `no_kill_services` hook); unlike the recent local-only umodes it **propagates** (`SEND_UMODES`) since a service is a service network-wide; the user mode `+l` locops + the `LOCOPS` command, charybdis `UMODE_LOCOPS`, slice 254 — the local-server sibling of `+z`/OPERWALL (an oper broadcasts to `+l` users on the originating server only; the command never crosses a link, the point of "LOCal OPerator Wall"), the last named standard charybdis umode candidate, shipped alongside a product decision that **leveva has no local-only user modes**: `SEND_UMODES` became *every* umode (so `+O`/`+x`/`+s`/`+g`/`+G`/`+D`/`+R`/`+Q`/`+z`/`+l` all propagate network-wide and a remote `WHOIS` shows them), while behavioural enforcement (caller-id/`+R` accept-list gating) stays on the target's home server; and the native cross-daemon command-skeleton differential harness. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** |  Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. |
+| **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 255 slices.** The slices cover the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash across every block, a channel-mode rework, CertFP, the **IRCv3 track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track), elemental channel modes + extbans, KNOCK, command-rate flood protection (fakelag), the leveva-native channel modes `+C`/`+c`/`+S`/`+j`/`+z`/`+T`/`+g`/`+r` (the `+z` op-moderation track 235–238 — local + network-wide S2S op-redirect for PRIVMSG/NOTICE/TAGMSG/REDACT via the statusmsg `@<chan>` seam — now complete; `+T` no-NOTICE, charybdis `MODE_NONOTICE`, slice 239; `+g` free-invite, charybdis `MODE_FREEINVITE`, slice 240; `+r` registered-only join, charybdis `MODE_REGONLY`, slice 241; `+f` channel forwarding, charybdis `MODE_FORWARD`, slice 242 — a forwardable JOIN reject redirects to the `+f` target with `470`; `+Q`/`+F` forward-target control, charybdis `MODE_DISFORWARD`/`MODE_FREETARGET`, slice 243 — `+F` lifts the op-on-target requirement for setting `+f`, `+Q` makes a channel refuse incoming forwards; `+P` permanent channel, charybdis `MODE_PERMANENT`/`chm_staff`, slice 244 — an oper-only flag that stops an empty channel being destroyed, so its topic/modes persist with zero members; `+L` large ban list, charybdis `MODE_EXLIMIT`/`chm_staff`, slice 245 — an oper-only flag that raises the per-list size cap on `+b`/`+e`/`+I` from 100 to 500, `478 ERR_BANLISTFULL` when a normal list is full — **the last missing standard charybdis channel mode**; the `max-bans`/`max-bans-large` caps `+L` toggles between are promoted to REHASH-able `options { max-bans / max-bans-large }` knobs, slice 246 — the const→knob trajectory `MAX_PENALTY` took); the user mode `+D` deaf, charybdis `UMODE_DEAF`, slice 247 — a client-settable, purely-local umode whose holder receives no channel-addressed messages (the channel fan-out filter runs on the deaf user's home server, so it is not in `SEND_UMODES`); the user mode `+R` registered-only messages, charybdis `UMODE_REGONLYMSG`, slice 248 — a client-settable, purely-local umode whose holder refuses a **private** `PRIVMSG`/`NOTICE` from an unidentified sender (`486 ERR_NONONREG`), bypassed by an identified/operator/accepted sender; the local-sender plane (slice 248); the `+R` remote-sender S2S plane, slice 249 — the inbound `relay::inbound_message` re-runs the same `callerid::regonly_msg_blocks` gate on the target's home server for a *remote* sender (neither `+R` nor `+g`/`+G` is in `SEND_UMODES`), bouncing one `486 ERR_NONONREG` to the sender's uplink for a blocked PRIVMSG (no notify pair, unlike caller-id) and silently dropping a blocked NOTICE — the caller-id 216→217 split repeated, closing the `+R` family; the user mode `+Q` no-forward, charybdis `UMODE_NOFORWARD`, slice 250 — the *user*-side counterpart of the channel `+f` forwarding feature: a client-settable, purely-local umode whose holder is never redirected by a `+f` channel (a forwardable JOIN reject falls through to the source's original numeric instead of `470` + redirect; the gate runs on the joiner's home server, so not in `SEND_UMODES`; the disjoint channel `+Q` `MODE_DISFORWARD` reuses the same letter), closing the `+f` family; the user mode `+G` soft caller-id MODE echo fix, slice 251 — `command::mode::umode_diff` omitted `SoftCallerId`, so a bare `±G` set the bit but echoed an empty trailing (local-echo repair to the existing slice-218 mode; not in `SEND_UMODES`, no snapshot/004 change); the user mode `+z` operwall + the `OPERWALL` command, charybdis `UMODE_OPERWALL`, slice 252 — the oper-only sibling of `+w`/WALLOPS: an operator broadcasts to every `+z` operator network-wide (`OPERWALL` command propagates, each server fans to its own local `+z`), `+z` is oper-gated to set like `+s` and purely-local (not in `SEND_UMODES`), a near-exact mirror of the slice-178 WALLOPS machinery (`command/operwall.rs` + `s2s/operwall.rs`); the user mode `+S` service, charybdis `UMODE_SERVICE`, slice 253 — a **server-set-only** umode (settable only on an S2S burst, a client/oper `MODE +S` silently ignored like `+Z`) whose holder is **immune to an operator's `KILL`** (`481` "Cannot kill a network service"; an inbound *server* KILL is still honoured, matching charybdis's `MyClient`-gated `no_kill_services` hook); unlike the recent local-only umodes it **propagates** (`SEND_UMODES`) since a service is a service network-wide; the user mode `+l` locops + the `LOCOPS` command, charybdis `UMODE_LOCOPS`, slice 254 — the local-server sibling of `+z`/OPERWALL (an oper broadcasts to `+l` users on the originating server only; the command never crosses a link, the point of "LOCal OPerator Wall"), the last named standard charybdis umode candidate, shipped alongside a product decision that **leveva has no local-only user modes**: `SEND_UMODES` became *every* umode (so `+O`/`+x`/`+s`/`+g`/`+G`/`+D`/`+R`/`+Q`/`+z`/`+l` all propagate network-wide and a remote `WHOIS` shows them), while behavioural enforcement (caller-id/`+R` accept-list gating) stays on the target's home server; the WHOIS service display, slice 255 — a `+S` target reads `313` "is a Network Service" (precedence over the operator string) and its channels are hidden (no `319`), closing the `+S` track; and the native cross-daemon command-skeleton differential harness. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** |  Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. |
 | **P12 — Retire the mechanical port; `leveva` is the product ✅ DONE (2026‑06‑13)** ([`docs/progress-log/p12.md`](docs/progress-log/p12.md)) | deleted `ircd-common`/`ircd-rs` + C‑era `iauth-rs` + `leveva-integration` + the mechanical port's deployment rigging; workspace = `leveva` + `leveva-iauth` | Ran the differential suite a **final time green** (`cargo test -p leveva-integration`, skeleton‑identical to the oracle), then **deleted the mechanical port** (`ircd-common`, `ircd-rs`), its C‑era auth scaffolding (`iauth-rs`, long since subsumed by the native `leveva-iauth`), and the oracle differential crate (`leveva-integration`). The oracle tests retired with their oracle — the load‑bearing behavior was already carried by leveva's self‑contained 212‑file golden + proptest suite (verified: leveva/leveva-iauth have zero Cargo/`use` dep on the deleted crates). Also stripped the mechanical port's **deployment rigging** (`docker/Dockerfile.ircd`, the `ircd` bake target, the entire `docs/k8s/` example network) and updated the README to describe the finished strangler. This is the literal end of the strangler — same lifecycle the C tree had at P8. | **MET. Final differential run green; `ircd-common`/`ircd-rs`/`iauth-rs`/`leveva-integration` deleted; `cargo build --workspace` 0 warnings; `cargo clippy --workspace --tests` clean; `cargo test -p leveva -p leveva-iauth` green — the behavioral guarantees carry forward. The workspace is now `leveva` + `leveva-iauth`.** |
 
 ---
diff --git a/leveva/help/WHOIS.md b/leveva/help/WHOIS.md
index 6455c042..90fed12d 100644
--- a/leveva/help/WHOIS.md
+++ b/leveva/help/WHOIS.md
@@ -7,7 +7,9 @@ real name), then 319 RPL_WHOISCHANNELS listing the target's visible channels
 are hidden, and the 319 is omitted entirely when none are visible), then 312
 RPL_WHOISSERVER. If the target is away you also
 get 301 RPL_AWAY, and if they are an operator you get 313 RPL_WHOISOPERATOR.
-The whole reply closes with a single 318 RPL_ENDOFWHOIS.
+A network service (+S) reads "is a Network Service" on that 313 line instead
+of the operator string, and its channels are hidden entirely (no 319). The
+whole reply closes with a single 318 RPL_ENDOFWHOIS.
 
 A nick that does not exist yields 401 ERR_NOSUCHNICK (the closing 318 is
 still sent). With no parameter at all you get 431 ERR_NONICKNAMEGIVEN.
diff --git a/leveva/src/command/mod.rs b/leveva/src/command/mod.rs
index 4acc22f6..ee7167d3 100644
--- a/leveva/src/command/mod.rs
+++ b/leveva/src/command/mod.rs
@@ -626,7 +626,9 @@ mod locops;
 mod operwall;
 mod wallops;
 mod who;
-mod whois;
+// `pub` so the pure WHOIS `313` decision seam (`whois::whois_oper_line`) is reachable from the
+// `whois_service_proptest` integration fuzz target (slice 255); the handler fns stay `pub(crate)`.
+pub mod whois;
 mod whowas;
 
 #[cfg(test)]
diff --git a/leveva/src/command/whois.rs b/leveva/src/command/whois.rs
index b9dd68d0..82a51736 100644
--- a/leveva/src/command/whois.rs
+++ b/leveva/src/command/whois.rs
@@ -59,10 +59,15 @@ pub(crate) fn whois(client: &Registered, msg: &Message, ctx: &ServerContext) ->
                         .trailing(&rec.realname)
                         .build(),
                 );
-                // 319 RPL_WHOISCHANNELS, only when there is at least one visible channel.
-                let chans =
+                // 319 RPL_WHOISCHANNELS, only when there is at least one visible channel —
+                // and never for a network service (`+S`): a service's channel memberships are
+                // not leaked in WHOIS (slice 255, charybdis hides them), for any requester.
+                let chans = if crate::mode::is_service(rec.modes) {
+                    Vec::new()
+                } else {
                     ctx.channels
-                        .whois_channels(&rec.uid, &client.uid, client.caps.multi_prefix);
+                        .whois_channels(&rec.uid, &client.uid, client.caps.multi_prefix)
+                };
                 if !chans.is_empty() {
                     replies.push(
                         Message::builder(Numeric::RplWhoischannels)
@@ -99,16 +104,18 @@ pub(crate) fn whois(client: &Registered, msg: &Message, ctx: &ServerContext) ->
                             .build(),
                     );
                 }
-                // 313 RPL_WHOISOPERATOR, when the target carries +o/+O — after the 301
-                // away block (send_whois order: 311 [319] 312 [301] [313] … [317] 318).
-                let oper_bits = crate::UserMode::Oper.bit() | crate::UserMode::LocalOp.bit();
-                if rec.modes & oper_bits != 0 {
+                // 313 RPL_WHOISOPERATOR — after the 301 away block (send_whois order:
+                // 311 [319] 312 [301] [313] … [317] 318). A network service (`+S`) reads "is a
+                // Network Service" and takes precedence over operator status (slice 255,
+                // charybdis single_whois `if(IsService) … else if(SeesOper) …`); otherwise an
+                // operator (+o/+O) reads "is an IRC Operator".
+                if let Some(text) = whois_oper_line(rec.modes) {
                     replies.push(
                         Message::builder(Numeric::RplWhoisoperator)
                             .prefix(&ctx.name)
                             .param(&client.nick)
                             .param(&rec.nick)
-                            .trailing("is an IRC Operator")
+                            .trailing(text)
                             .build(),
                     );
                 }
@@ -192,7 +199,9 @@ pub(crate) fn whois(client: &Registered, msg: &Message, ctx: &ServerContext) ->
                 // +<modes>`. leveva-native (the IRCnet 2.11 oracle has no 379). User modes
                 // leak `+i`/oper/cloak state, so this is gated: only the target themselves
                 // or an operator may see it. Last extra-info line before the 317/318 tail.
-                let requester_is_oper = client.modes & oper_bits != 0;
+                let requester_is_oper = client.modes
+                    & (crate::UserMode::Oper.bit() | crate::UserMode::LocalOp.bit())
+                    != 0;
                 if client.uid == rec.uid || requester_is_oper {
                     replies.push(
                         Message::builder(Numeric::RplWhoismodes)
@@ -268,6 +277,22 @@ pub(crate) fn whois(client: &Registered, msg: &Message, ctx: &ServerContext) ->
     replies
 }
 
+/// The `313 RPL_WHOISOPERATOR` trailing text for a target with user modes `modes`, or `None`
+/// when no `313` line fires. A network service (`+S`, [`crate::UserMode::Service`]) reads "is a
+/// Network Service" and takes **precedence** over operator status (charybdis `single_whois`:
+/// `if(IsService) … else if(SeesOper) …`), so a `+oS` user reads as a service; otherwise an
+/// operator (`+o`/`+O`) reads "is an IRC Operator". Pure — the fuzz seam for the WHOIS `313`
+/// decision (slice 255).
+pub fn whois_oper_line(modes: u32) -> Option<&'static str> {
+    if crate::mode::is_service(modes) {
+        Some("is a Network Service")
+    } else if modes & (crate::UserMode::Oper.bit() | crate::UserMode::LocalOp.bit()) != 0 {
+        Some("is an IRC Operator")
+    } else {
+        None
+    }
+}
+
 /// The `(server-name, server-info)` to report on `312` for `rec` — the target's *own*
 /// server. A local user (UID prefix = our SID) is us / our description; a remote user is
 /// resolved from the [`Network`](crate::s2s::network::Network) mirror (its real burst
@@ -406,6 +431,70 @@ mod tests {
         assert_eq!(r[2].trailing(), Some("is using modes +"));
     }
 
+    #[test]
+    fn whois_service_target_shows_network_service_not_oper() {
+        let ctx = ctx();
+        let _alice = claim_observed(&ctx, "alice");
+        let _bob = claim_observed(&ctx, "bob");
+        // A +S (network-service) target reads "is a Network Service" on 313, not "is an IRC
+        // Operator" (slice 255, charybdis servicestring). Fires for any requester.
+        ctx.registry
+            .set_modes(&uid_for("bob"), crate::UserMode::Service.bit());
+        let r = run(&ctx, "WHOIS bob");
+        let line = r.iter().find(|m| m.command() == "313").expect("313 present");
+        assert_eq!(line.trailing(), Some("is a Network Service"));
+        // Inverse: a plain (non-service, non-oper) target gets no 313 at all.
+        ctx.registry.set_modes(&uid_for("bob"), 0);
+        let r = run(&ctx, "WHOIS bob");
+        assert!(!codes(&r).contains(&"313"), "non-service non-oper has no 313");
+    }
+
+    #[test]
+    fn whois_service_and_oper_shows_network_service() {
+        let ctx = ctx();
+        let _alice = claim_observed(&ctx, "alice");
+        let _bob = claim_observed(&ctx, "bob");
+        // A +oS target: service status takes precedence over operator status (charybdis
+        // single_whois `if(IsService) … else if(SeesOper) …`).
+        ctx.registry.set_modes(
+            &uid_for("bob"),
+            crate::UserMode::Service.bit() | crate::UserMode::Oper.bit(),
+        );
+        let r = run(&ctx, "WHOIS bob");
+        let line = r.iter().find(|m| m.command() == "313").expect("313 present");
+        assert_eq!(line.trailing(), Some("is a Network Service"));
+        // Inverse: drop +S, keep +o → the line reverts to the operator string.
+        ctx.registry
+            .set_modes(&uid_for("bob"), crate::UserMode::Oper.bit());
+        let r = run(&ctx, "WHOIS bob");
+        let line = r.iter().find(|m| m.command() == "313").expect("313 present");
+        assert_eq!(line.trailing(), Some("is an IRC Operator"));
+    }
+
+    #[test]
+    fn whois_service_target_hides_channels() {
+        let ctx = ctx();
+        claim_and_join(&ctx, "alice", "#rust"); // requester, shares the channel
+        claim_and_join(&ctx, "bob", "#rust");
+        // A +S service in a shared channel: its channel memberships are hidden — no 319.
+        ctx.registry
+            .set_modes(&uid_for("bob"), crate::UserMode::Service.bit());
+        let r = run(&ctx, "WHOIS bob");
+        assert!(
+            !codes(&r).contains(&"319"),
+            "a service must not leak its channels via 319: {:?}",
+            codes(&r)
+        );
+        // Inverse: clear +S (bob still in #rust) → the shared channel reappears on 319.
+        ctx.registry.set_modes(&uid_for("bob"), 0);
+        let r = run(&ctx, "WHOIS bob");
+        assert!(
+            codes(&r).contains(&"319"),
+            "a non-service in a shared channel shows 319: {:?}",
+            codes(&r)
+        );
+    }
+
     #[test]
     fn whois_local_user_emits_317_before_318() {
         let ctx = ctx();
