diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,35 @@
+# Changelog for synapse-cc
+
+## 0.4.0
+
+### Breaking
+
+- Built on `plexus-synapse >= 4.0 && < 5` and `plexus-protocol >= 0.6 && < 0.7`
+  (was `plexus-synapse >= 3.14.3 && < 3.18`). The IR synapse-cc builds and
+  hands to the generator is synapse 4.0's IR 3.0 (a method's `returns`,
+  `error`, `progress` and `cardinality` read from the plexus 0.7 schema),
+  so the generator must read IR 3.0. Backends must serve plexus 0.7 or the
+  published 0.7.0 line.
+- `watch`: a failed hash call reports the server's typed error, rendered as
+  plexus-protocol 0.6's `renderRemoteError` line.
+
+### Changed
+
+- `synapse-cc-test`, which needs a live substrate on `127.0.0.1:4444`, is
+  now opt-in: `cabal test -f live-tests`. The hermetic suites
+  (`safe6-authcheck`, `naming`) run by default.
+- `cabal.project` lists only `.`; the plexus packages come from Hackage.
+
+## Since 0.3.3 (unpublished 0.3.4 - 0.3.6)
+
+- The generator is `service-codegen`, found by that name first;
+  `--service-codegen` replaces the generator's old flag (hidden, still
+  read). `synapse.lock` writes `serviceCodegen` and reads either key.
+- Warns when the generator found is older than 0.5.0, which does not leave
+  server-injected parameters out of generated methods.
+- `synapse-cc init` takes or infers the backend; a `cargo build` step for
+  generated Rust clients; the registry endpoint is read from
+  `PLEXUS_REGISTRY_URL`.
+- Auth: `--cookie` and `--header`, credentials scanned from the
+  environment, token resolution through synapse's `_self` credential
+  store, and a warning when cookie auth does not match the backend.
diff --git a/PLAN.md b/PLAN.md
--- a/PLAN.md
+++ b/PLAN.md
@@ -21,7 +21,7 @@
     ↓
     ├─→ synapse (Haskell) → IR (JSON)
     ↓
-    ├─→ hub-codegen (Rust) → Generated Code
+    ├─→ service-codegen (Rust) → Generated Code
     ↓
     └─→ Language Tools (npm, tsc, etc.) → Compiled Artifact
 ```
@@ -64,7 +64,7 @@
    - Fail with helpful error
 4. ✅ Pipeline orchestration:
    - Run synapse to generate IR
-   - Run hub-codegen on IR
+   - Run service-codegen on IR
    - Write output to directory
 5. ✅ Basic error handling and logging
 6. ✅ End-to-end test with substrate
@@ -119,7 +119,7 @@
 3. Parallel generation for multiple targets
 4. Progress indicators and spinners
 5. Colorized output
-6. Auto-install missing tools (synapse, hub-codegen)
+6. Auto-install missing tools (synapse, service-codegen)
 
 **Deliverable**: Production-ready developer tool
 
@@ -136,7 +136,7 @@
     └── SynapseCC/
         ├── Types.hs             # Core types (Config, Target, Backend, etc.)
         ├── CLI.hs               # Command-line argument parsing
-        ├── Discover.hs          # Tool discovery (find synapse, hub-codegen)
+        ├── Discover.hs          # Tool discovery (find synapse, service-codegen)
         ├── Pipeline.hs          # Pipeline orchestration
         ├── Process.hs           # Subprocess execution helpers
         ├── Cache.hs             # IR/code caching by hash
@@ -148,7 +148,7 @@
 
 ### 1. Subprocess Execution (Not FFI)
 
-**Decision**: Call synapse and hub-codegen as separate processes
+**Decision**: Call synapse and service-codegen as separate processes
 
 **Rationale**:
 - Clean separation of concerns
@@ -160,7 +160,7 @@
 **Implementation**:
 ```haskell
 runSynapse :: Backend -> URL -> FilePath -> IO (Either Error IRPath)
-runHubCodegen :: IRPath -> Target -> Options -> FilePath -> IO (Either Error GeneratedPath)
+runServiceCodegen :: IRPath -> Target -> Options -> FilePath -> IO (Either Error GeneratedPath)
 ```
 
 ### 2. Tool Discovery Strategy
@@ -168,12 +168,12 @@
 **Priority Order**:
 1. Local development paths (for contributors)
    - `../synapse/dist-newstyle/build/.../synapse`
-   - `../hub-codegen/target/release/hub-codegen`
+   - `../service-codegen/target/release/service-codegen`
 2. System PATH (for installed versions)
-   - `synapse` and `hub-codegen` in $PATH
+   - `synapse` and `service-codegen` in $PATH
 3. Plexus bin directory (for managed installs)
    - `~/.plexus/bin/synapse`
-   - `~/.plexus/bin/hub-codegen`
+   - `~/.plexus/bin/service-codegen`
 4. Fail with helpful installation instructions
 
 **Future**: Auto-install via cabal/cargo
@@ -251,7 +251,7 @@
 # Output:
 # 🔍 Discovering tools...
 # ✓ Found synapse at ../synapse/dist-newstyle/.../synapse
-# ✓ Found hub-codegen at ../hub-codegen/target/release/hub-codegen
+# ✓ Found service-codegen at ../service-codegen/target/release/service-codegen
 #
 # 📡 Connecting to substrate at ws://localhost:4444...
 # ✓ Connected
@@ -309,7 +309,7 @@
 
 ### External Tools (Runtime)
 - `synapse` - Schema discovery and IR generation
-- `hub-codegen` - Code generation from IR
+- `service-codegen` - Code generation from IR
 - Language tools (npm, tsc, cargo, pip, etc.) - Language-specific builds
 
 ## Future Extensions
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -8,9 +8,10 @@
 
 - 🔧 **Unified Interface**: Single command to generate clients from any backend
 - 🔍 **Smart Tool Discovery**: Finds local development builds or installed versions
-- ⚡ **Fast**: Smart caching avoids regeneration when schemas haven't changed (coming in Phase 3)
-- 🎯 **Multi-Language**: TypeScript, Python, Rust support (TypeScript in MVP)
-- 🛠️ **Complete Pipeline**: From schema → IR → generated code → compiled artifacts
+- ⚡ **Fast**: Incremental caching skips regeneration when schemas haven't changed (IR cache at `~/.cache/plexus-codegen/synapse/ir/`, per-file code cache via service-codegen)
+- 🎯 **Multi-Language**: TypeScript and Rust today (`rust` requires a service-codegen built with `--features rust`); `python` parses but has no generator yet
+- 🛠️ **Complete Pipeline**: schema → IR → generated code → installed deps → compiled artifact → smoke tests
+- 🔐 **Credential lifecycle**: Per-backend defaults store shared with `synapse` — set, inspect, and rotate JWTs / cookies / headers via `synapse-cc _self <backend> …`. Values held as reference URIs (`literal:`, `env://`, `file://`, `keychain://`) so secrets live in the OS keychain or env vars, not plaintext. JWT decoding flags expired tokens at a glance.
 
 ## Installation
 
@@ -26,71 +27,175 @@
 
 `synapse-cc` requires these tools to be available:
 - **synapse** - Schema discovery and IR generation
-- **hub-codegen** - Code generation from IR
+- **service-codegen** ≥ 0.5.0 - Code generation from IR. Earlier releases write a
+  parameter the server injects (`pdSource`: auth, header, derived, …) into the
+  generated methods as an optional `unknown` argument; synapse-cc says so when
+  it finds one (SYNAPSE-CC-2)
 
 The tool will automatically find them if they're:
 - In your `$PATH`
-- In local development directories (`../synapse`, `../hub-codegen`)
+- In local development directories (`../synapse`, `../service-codegen`)
 - In `~/.plexus/bin/`
 
+**The generator is `service-codegen`** (0.5.1 on; NAMING-10 — a Plexus backend
+is a *service*). synapse-cc 0.3.6 looks for it under that name first — on
+`$PATH`, then `../service-codegen/target/…`, `~/.plexus/bin`, `~/.cargo/bin` —
+and only then under its old name, saying the old name is deprecated. Its path
+is `--service-codegen PATH` (the old flag still works for one release, hidden,
+with a note). `synapse.lock` is written with `serviceCodegen`, the generator's
+JSON is read as `serviceCodegenVersion`, a code cache's toolchain as
+`tvServiceCodegen`; each old key is still read. NAMING-18 removes the old
+names; `scripts/naming-gate.sh` (the `naming` test suite) fails on a new one.
+synapse-cc 0.3.6 builds against `plexus-synapse` 3.14.3 up to 3.17.
+
 ## Usage
 
-### Basic TypeScript Client
+The CLI is subcommand-shaped:
 
 ```bash
-synapse-cc typescript substrate ws://localhost:4444
+synapse-cc init [BACKEND]                  # scaffold synapse.config.json
+synapse-cc build                           # build every target in synapse.config.json
+synapse-cc build TARGET BACKEND [OPTIONS]  # one-off build, no config file
+synapse-cc watch BACKEND [PLUGIN...]       # rebuild on schema changes
+synapse-cc wait [BACKEND]                  # block until backend(s) reachable
+synapse-cc _self BACKEND VERB ...          # credentials store (shared with synapse)
 ```
 
-This will:
-1. Connect to substrate at `ws://localhost:4444`
-2. Discover the schema
-3. Generate IR
-4. Generate TypeScript client
-5. Output to `./generated/`
+### Quickstart (config-driven)
 
-### Options
+A service scaffolded by `axon new` ships a `synapse.config.json` pre-pointed
+at itself — `synapse-cc build` works unedited from the service directory
+(server must be running). Real run:
 
-```bash
-synapse-cc <target> <backend> <url> [OPTIONS]
+```
+$ synapse-cc build
+==> Discovering tools...
+[+] Found all required tools
+[i] Building "client" → src/lib/plexus
 
-Arguments:
-  target    Target language (typescript, python, rust)
-  backend   Backend identifier (substrate, plexus, synapse, etc.)
-  url       Backend WebSocket URL (e.g., ws://localhost:4444)
+==> Reading schema...
+[+] Schema ready (2 plugins)
 
-Options:
-  -o, --output DIR           Output directory (default: ./generated)
-  --bundle-transport BOOL    Bundle transport code (default: true)
-  --no-install              Skip dependency installation
-  --no-build                Skip compilation step
-  --cache-dir DIR           Cache directory (default: ~/.plexus/cache)
-  --force                   Force regeneration (ignore cache)
-  --watch                   Watch backend and regenerate on changes
-  --debug                   Enable debug logging
-  -h, --help                Show help message
+==> Generating code...
+[+] Code generated (11 files)
+
+==> Installing dependencies...
+[+] Dependencies installed
+
+==> Building...
+[+] Build passed
+
+==> Running tests...
+[+] Tests passed
+[+] client → src/lib/plexus/
 ```
 
-### Examples
+Without a config file, `synapse-cc init <backend>` scaffolds one; with the
+backend argument omitted it is **inferred** — from a co-located Plexus
+service crate, then from the local registry — and the inference is
+announced. There is no silent default backend:
 
-**External transport mode** (uses `@plexus/rpc-client` package):
-```bash
-synapse-cc typescript substrate ws://localhost:4444 \
-  --bundle-transport=false \
-  --output ./packages/substrate-client
 ```
+$ synapse-cc init
+[!] missing BACKEND argument — no co-located Plexus service crate found and
+    no backend discoverable on the registry at 127.0.0.1:4444.
+    Run: synapse-cc init <backend>
+```
 
-**Skip build steps** (just generate code):
+### Quickstart (one-off, no config)
+
 ```bash
-synapse-cc typescript substrate ws://localhost:4444 \
-  --no-install \
-  --no-build
+synapse-cc build typescript substrate          # TS client → ./generated/
+synapse-cc build rust substrate -o ./client    # Rust client, ends in cargo build
 ```
 
-**Debug mode**:
+The backend is resolved **by name via the registry** (endpoint from
+`PLEXUS_REGISTRY_URL`, default `ws://127.0.0.1:4444`; `-H`/`-P` set the
+fallback). A real `rust` run:
+
+```
+$ synapse-cc build rust zechodev -o ./rust-client
+==> Discovering tools...
+[+] Found all required tools
+
+==> Resolving zechodev via registry at ws://127.0.0.1:4444...
+
+==> Reading schema...
+[+] Schema ready (2 plugins)
+
+==> Generating code...
+[+] Code generated (5 files)
+
+==> Building...
+[+] Build passed
+
+==> Running tests...
+[+] Tests passed
+
+[+] Client → ./rust-client/
+```
+
+`-t rust` ends in a real `cargo build` of the generated crate (Z2H-7) — the
+artifact compiles or the command fails.
+
+### Options (build / watch)
+
+```
+  -o, --output DIR        Output directory (default: ./generated)
+  --transport ws|browser  ws (Node.js, default) or browser (native WebSocket,
+                          for Tauri/WebView)
+  --no-install            Skip dependency installation
+  --no-build              Skip compilation step
+  --no-tests              Skip running smoke tests
+  --cache-dir DIR         Cache directory (default: ~/.cache/plexus-codegen)
+  --force                 Force regeneration (ignore cache)
+  --debug                 Enable debug logging
+  --synapse PATH          synapse binary path (overrides discovery)
+  --service-codegen PATH      service-codegen binary path (overrides discovery)
+  -t, --token JWT         JWT auth token (see Credentials below)
+  --token-file PATH       Read JWT from file
+  -H, --host HOST         Registry/discovery host (default: 127.0.0.1)
+  -P, --port PORT         Registry/discovery port (default: 4444)
+```
+
+### Rust target prerequisite
+
+`build rust` requires a service-codegen binary compiled with the `rust` feature
+(`cargo build --release --features all` in the service-codegen repo). A
+typescript-only binary fails with:
+
+```
+[!] Error: service-codegen failed (exit code 1)
+Error: Rust codegen not enabled. Rebuild with --features rust
+```
+
+Point `--service-codegen` at a rust-enabled build if the discovered one is not.
+
+### Credentials & Headers
+
+synapse-cc shares a **single defaults store** with synapse at `~/.plexus/<backend>/defaults.json`. Any JWT, cookie, or header you set via either CLI is immediately visible to the other. The file holds credential-reference URIs (`literal:`, `env://`, `file://`, `keychain://`), resolved at request time.
+
 ```bash
-synapse-cc typescript substrate ws://localhost:4444 --debug
+# Inspect what's stored + decode any JWTs + flag expired tokens
+synapse-cc _self <backend> show
+
+# Store a JWT (auto-wrapped as literal:)
+synapse-cc _self <backend> set cookie access_token "eyJ..."
+
+# Store a reference to an env var (preferred in CI)
+synapse-cc _self <backend> set cookie access_token "env://MY_JWT"
+
+# Import a JWT file
+synapse-cc _self <backend> import-token ~/Downloads/jwt.txt
+
+# Clear all defaults for a backend
+synapse-cc _self <backend> clear --yes
 ```
 
+The `_self` subcommand tree is identical to `synapse _self <backend> …` — they share the same library implementation. See synapse's README for the full verb list, URI scheme reference, and legacy `~/.plexus/tokens/<backend>` migration behavior.
+
+Per-invocation overrides: `--token <jwt>`, `SYNAPSE_TOKEN` env, `--token-file <path>`, `--cookie KEY=VALUE`, `--header KEY=VALUE` — all flow through the same priority chain as synapse, with CLI values winning over stored defaults per key.
+
 ## Architecture
 
 ```
@@ -98,7 +203,7 @@
     ↓
     ├─→ synapse (Haskell) → IR (JSON)
     ↓
-    ├─→ hub-codegen (Rust) → Generated Code
+    ├─→ service-codegen (Rust) → Generated Code
     ↓
     └─→ Language Tools (npm, tsc, etc.) → Compiled Artifact
 ```
@@ -110,19 +215,26 @@
 ```
 synapse-cc/
 ├── synapse-cc.cabal         # Cabal project file
-├── PLAN.md                  # Detailed implementation plan
 ├── app/
-│   └── Main.hs              # Entry point
+│   └── Main.hs              # Entry point, subcommand dispatch
 └── src/
     └── SynapseCC/
-        ├── Types.hs         # Core types
-        ├── CLI.hs           # Command-line parsing
-        ├── Discover.hs      # Tool discovery
-        ├── Pipeline.hs      # Pipeline orchestration
-        ├── Process.hs       # Subprocess helpers
-        ├── Cache.hs         # Caching (Phase 3)
-        ├── Language.hs      # Language integration (Phase 2)
-        └── Logging.hs       # Pretty output
+        ├── Types.hs           # Core types, Options, synapse.config.json types
+        ├── CLI.hs             # Subcommand + option parsing
+        ├── Config.hs          # synapse.config.json load/init
+        ├── Detect.hs          # Project detection for `init` inference
+        ├── Discover.hs        # Tool discovery (synapse, service-codegen)
+        ├── RegistryResolve.hs # Backend-name resolution via the registry
+        ├── Pipeline.hs        # Pipeline orchestration
+        ├── Process.hs         # Subprocess helpers
+        ├── Cache.hs           # Incremental IR caching
+        ├── Merge.hs           # Three-way merge bookkeeping
+        ├── Dependency.hs      # Plugin dependency resolution
+        ├── Language.hs        # Per-language install/build/test steps
+        ├── Auth.hs            # Token priority chain (wraps Synapse.Self)
+        ├── Watch.hs           # watch subcommand
+        ├── Wait.hs            # wait subcommand
+        └── Logging.hs         # Pretty output
 ```
 
 ### Building
@@ -134,45 +246,30 @@
 ### Running from source
 
 ```bash
-cabal run synapse-cc -- typescript substrate ws://localhost:4444
+cabal run synapse-cc -- build typescript substrate --debug
 ```
 
 ### Testing
 
 ```bash
-# Ensure substrate is running
-cd ../plexus-substrate
-cargo run -- --port 4444
+# Scaffold and start a throwaway backend (any free port)
+axon new mysvc --port 4452 && cd mysvc && cargo run &
 
-# In another terminal, test synapse-cc
-cd ../synapse-cc
-cabal run synapse-cc -- typescript substrate ws://localhost:4444 --debug
+# Generate, compile, and smoke-test a client against it — the scaffold's
+# synapse.config.json is already pointed at ws://127.0.0.1:4452
+synapse-cc build
 ```
 
-## Roadmap
-
-### Phase 1: MVP (Current)
-- ✅ Project scaffolding
-- ✅ Tool discovery
-- ✅ Pipeline orchestration
-- ✅ Basic error handling
-- [ ] End-to-end testing
-
-### Phase 2: Language Integration
-- [ ] Dependency installation (npm, pip, cargo)
-- [ ] Build automation
-- [ ] Multi-package manager support
+## Status
 
-### Phase 3: Smart Caching
-- [ ] Hash-based caching
-- [ ] Cache management
-- [ ] Fast iteration
+Shipped: tool discovery, pipeline orchestration, config-driven multi-target
+builds (`synapse.config.json`), incremental IR + code caching with three-way
+merge, dependency installation + compile + smoke-test steps (bun/npm for
+TypeScript, cargo for Rust), watch mode, `wait`, registry-based backend
+resolution (`PLEXUS_REGISTRY_URL`), shared credentials store (`_self`).
 
-### Phase 4: Production Features
-- [ ] Watch mode
-- [ ] Progress indicators
-- [ ] Auto-install tools
-- [ ] Configuration files
+Not shipped: a `python` generator (the target parses; service-codegen has no
+emitter), remote/distributed caching.
 
 ## Contributing
 
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -3,15 +3,19 @@
 import Control.Monad (when, forM_)
 import qualified Data.Text as T
 import qualified Data.Text.IO as TIO
-import System.Exit (exitFailure, exitSuccess)
+import System.Environment (getArgs)
+import System.Exit (ExitCode(..), exitFailure, exitSuccess, exitWith)
 import System.IO (stdout, stderr, hSetEncoding, hSetBuffering, utf8, BufferMode(..))
 
+import qualified Synapse.Self.Command as SelfCmd
+
 import SynapseCC.CLI
-import SynapseCC.Config (initSynapseConfig, loadSynapseConfig, synapseConfigPath)
+import SynapseCC.Config (initSynapseConfig, InitResult(..), loadSynapseConfig, synapseConfigPath)
 import SynapseCC.Detect (ProjectHint(..), defaultDetectors)
 import SynapseCC.Discover
 import SynapseCC.Logging
 import SynapseCC.Pipeline
+import qualified SynapseCC.RegistryResolve as Registry
 import SynapseCC.Types
 import SynapseCC.Wait (runWait)
 import SynapseCC.Watch (runWatch)
@@ -28,23 +32,31 @@
   hSetBuffering stdout LineBuffering
   hSetBuffering stderr LineBuffering
 
+  getArgs >>= mapM_ logInfo . deprecatedFlagNotes
   cmd <- parseCommand
 
   case cmd of
     -- ------------------------------------------------------------------
     -- synapse-cc init
     -- ------------------------------------------------------------------
-    CmdInit -> do
-      result <- initSynapseConfig synapseConfigPath defaultDetectors
+    CmdInit mBackend -> do
+      -- Z2H-5: explicit backend wins; otherwise infer (co-located service
+      -- crate, then local registry at 127.0.0.1:4444). Never a silent default.
+      result <- initSynapseConfig synapseConfigPath defaultDetectors mBackend "127.0.0.1" 4444
       case result of
         Left err -> do
           TIO.putStrLn $ "[!] " <> err
           exitFailure
-        Right hint -> do
+        Right initResult -> do
           logSuccess $ "Created " <> T.pack synapseConfigPath
+                    <> " (backend: " <> irBackend initResult <> ")"
+          -- If the backend was inferred (not given explicitly), SAY SO.
+          case irBackendNote initResult of
+            Just note -> logInfo note
+            Nothing   -> pure ()
           -- If a detector gave a reason, surface it so the user knows
           -- why the generated config has a particular transport.
-          case phReason hint of
+          case phReason (irHint initResult) of
             Just reason -> logInfo reason
             Nothing     -> pure ()
           TIO.putStrLn "Edit it to configure your targets, then run:"
@@ -67,8 +79,11 @@
 
         Right tools -> do
           logSuccess "Found all required tools"
-          logStep $ "Connecting to " <> backendName (cfgBackend config)
-                 <> " at ws://" <> cfgHost config <> ":" <> cfgPort config <> "..."
+          -- Z2H-6: report the EFFECTIVE registry endpoint (PLEXUS_REGISTRY_URL
+          -- may override the default --host/--port).
+          (effHost, effPort) <- Registry.resolveRegistryEndpoint (cfgHost config) (cfgPort config)
+          logStep $ "Resolving " <> backendName (cfgBackend config)
+                 <> " via registry at ws://" <> effHost <> ":" <> T.pack (show effPort) <> "..."
 
           pipelineResult <- runPipeline config tools
           case pipelineResult of
@@ -146,3 +161,15 @@
         Right tools -> do
           logSuccess "Found all required tools"
           runWatch watchArgs tools
+
+    -- ------------------------------------------------------------------
+    -- synapse-cc _self <backend> <verb> ...   (SELF-6)
+    -- ------------------------------------------------------------------
+    -- Delegates straight to the shared handler that @synapse _self@ uses.
+    -- Keeping the dispatch thin here (one line of real work) is the
+    -- point of SELF-6: both CLIs must stay behavior-identical.
+    CmdSelf selfCmd -> do
+      code <- SelfCmd.runSelfCommand selfCmd
+      case code of
+        ExitSuccess     -> exitSuccess
+        ExitFailure _   -> exitWith code
diff --git a/src/SynapseCC/Auth.hs b/src/SynapseCC/Auth.hs
new file mode 100644
--- /dev/null
+++ b/src/SynapseCC/Auth.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Token resolution for synapse-cc (SAFE-2; deduplicated in SELF-6).
+--
+-- This module is the CLI-priority chain that sits in front of the shared
+-- 'Synapse.Self.resolveToken' helper. The chain (highest first):
+--
+--   1. @--token \<jwt\>@                       (explicit flag)
+--   2. @SYNAPSE_TOKEN@ env var
+--   3. @--token-file \<path\>@                 (explicit file)
+--   4. 'Synapse.Self.resolveToken' — reads
+--      @~\/.plexus\/\<backend\>\/defaults.json@ via 'defaultRegistry'
+--
+-- Step 4 delegates through the shared library so synapse and synapse-cc
+-- stay byte-identical on persistent defaults. Prior to SELF-6 this
+-- module carried a hand-rolled copy of the defaults.json read; SELF-6
+-- collapses that onto 'Synapse.Self.resolveToken'.
+--
+-- 'SynapseCC.Auth.resolveToken' itself stays as a synapse-cc-local
+-- orchestrator because 'Options' is a synapse-cc type — the Options
+-- record and the CLI flag priority belong here, not in the library.
+-- The /library-visible/ fallback is what SELF-6 dedups.
+module SynapseCC.Auth
+  ( resolveToken
+  ) where
+
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as TIO
+import System.Environment (lookupEnv)
+
+import qualified Synapse.Self as Self
+
+import SynapseCC.Types (Options(..))
+
+-- | Resolve the auth token for a given backend.
+-- Returns 'Nothing' if no source provides one.
+resolveToken :: Options -> Text -> IO (Maybe Text)
+resolveToken opts backend =
+  case optToken opts of
+    Just tok | not (T.null tok) -> pure (Just tok)
+    _ -> do
+      mEnv <- lookupEnv "SYNAPSE_TOKEN"
+      case mEnv of
+        Just envTok | not (null envTok) -> pure (Just (T.pack envTok))
+        _ -> case optTokenFile opts of
+          Just p -> do
+            contents <- TIO.readFile p
+            let tok = T.strip contents
+            pure $ if T.null tok then Nothing else Just tok
+          Nothing -> Self.resolveToken backend
diff --git a/src/SynapseCC/AuthCheck.hs b/src/SynapseCC/AuthCheck.hs
new file mode 100644
--- /dev/null
+++ b/src/SynapseCC/AuthCheck.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | SAFE-6: detect backends that require cookie-based WebSocket auth
+-- and warn when the target's generated @transport.ts@ lacks the
+-- SAFE-7 cookie-auth marker.
+--
+-- Detection signal: the IR contains at least one method whose params
+-- schema has a property annotated @x-plexus-source.from = "auth"@.
+-- That annotation is emitted by REQ-6 from @#[from_auth(...)]@ method
+-- params. Any presence means the backend is using cookie-driven auth
+-- at dispatch time; clients without cookie support will fail silently.
+--
+-- The SAFE-7 marker is the string @SAFE-7-cookie-auth-marker@ embedded
+-- in the transport template by @service-codegen@'s TS generator. Its
+-- presence in an existing @transport.ts@ indicates the file was
+-- produced by a SAFE-7-era service-codegen (i.e. cookie-capable).
+module SynapseCC.AuthCheck
+  ( detectCookieAuthMismatch
+  , backendRequiresCookieAuth
+  ) where
+
+import Data.Aeson (Value(..))
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.KeyMap as KM
+import qualified Data.ByteString as BS
+import qualified Data.Text as T
+import Data.Text (Text)
+import qualified Data.Text.IO as TIO
+import System.Directory (doesFileExist)
+import System.FilePath ((</>))
+
+-- | The marker string embedded by SAFE-7 in @transport.ts@. Kept in
+-- sync with @service-codegen/src/generator/typescript/transport.rs@.
+safe7Marker :: Text
+safe7Marker = "SAFE-7-cookie-auth-marker"
+
+-- | Inspect the IR JSON at @irFile@ and the target's output directory
+-- @outputDir@. Returns @Just warningText@ when the backend requires
+-- cookie auth (any method has @x-plexus-source.from = "auth"@) AND an
+-- existing @transport.ts@ in the output directory lacks the SAFE-7
+-- marker. Returns @Nothing@ otherwise (including first builds where
+-- no transport.ts exists — nothing to mismatch with yet).
+detectCookieAuthMismatch
+  :: FilePath  -- ^ IR JSON file (e.g. cache's ir.json)
+  -> FilePath  -- ^ Target output directory (will inspect transport.ts inside)
+  -> IO (Maybe Text)
+detectCookieAuthMismatch irFile outputDir = do
+  irBytes <- BS.readFile irFile
+  case Aeson.eitherDecodeStrict irBytes of
+    Left _    -> pure Nothing  -- unreadable IR is not this module's problem
+    Right (val :: Value) ->
+      if backendRequiresCookieAuth val
+        then inspectTarget outputDir
+        else pure Nothing
+
+-- | Returns 'True' when the IR describes at least one method that uses
+-- @#[from_auth(...)]@ (i.e. a param with @x-plexus-source.from = "auth"@).
+backendRequiresCookieAuth :: Value -> Bool
+backendRequiresCookieAuth val = case val of
+  Object top -> case KM.lookup "irMethods" top of
+    Just (Object ms) -> any hasAuthParam (KM.elems ms)
+    _                -> False
+  _ -> False
+  where
+    hasAuthParam :: Value -> Bool
+    hasAuthParam (Object m) = case KM.lookup "mdParams" m of
+      Just (Array params) -> any paramIsAuth (foldr (:) [] params)
+      _                   -> False
+    hasAuthParam _ = False
+
+    paramIsAuth :: Value -> Bool
+    paramIsAuth (Object p) = case KM.lookup "pdSource" p of
+      Just (Object src) -> case KM.lookup "from" src of
+        Just (String "auth") -> True
+        _                    -> False
+      _ -> False
+    paramIsAuth _ = False
+
+-- | Inspect the target directory for a SAFE-7-marked transport.ts.
+-- Returns a warning when transport.ts exists but lacks the marker.
+-- Returns 'Nothing' when transport.ts doesn't exist (first build) or
+-- does contain the marker (correctly configured).
+inspectTarget :: FilePath -> IO (Maybe Text)
+inspectTarget outputDir = do
+  let tsFile = outputDir </> "transport.ts"
+  exists <- doesFileExist tsFile
+  if not exists
+    then pure Nothing  -- first build; nothing to compare
+    else do
+      contents <- TIO.readFile tsFile
+      if safe7Marker `T.isInfixOf` contents
+        then pure Nothing
+        else pure . Just $
+          "[!] SAFE-6: backend requires cookie auth but target's transport.ts "
+          <> "does not look like a SAFE-7-era service-codegen output (missing marker '"
+          <> safe7Marker
+          <> "'). Cookie-based WS upgrade may not work. "
+          <> "Regenerate with an up-to-date service-codegen or pass --force."
diff --git a/src/SynapseCC/CLI.hs b/src/SynapseCC/CLI.hs
--- a/src/SynapseCC/CLI.hs
+++ b/src/SynapseCC/CLI.hs
@@ -10,12 +10,18 @@
 
     -- * Version
   , versionInfo
+
+    -- * Deprecated spellings (NAMING-10)
+  , deprecatedFlagNotes
   ) where
 
+import Data.List (isPrefixOf)
 import Data.Text (Text)
 import qualified Data.Text as T
 import Options.Applicative
 
+import qualified Synapse.Self.Command as SelfCmd
+
 import SynapseCC.Types
 
 -- ============================================================================
@@ -70,6 +76,8 @@
       (progDesc "Block until all configured backends are reachable"))
  <> command "init"  (info initCommandParser
       (progDesc "Scaffold a synapse.config.json in the current directory"))
+ <> command "_self" (info (CmdSelf <$> SelfCmd.parseSelfCommand)
+      (progDesc "Manage per-backend defaults (cookies, headers, credentials)"))
   )
 
 buildCommandParser :: Parser Command
@@ -137,13 +145,20 @@
         )
   <*> switch (long "debug" <> help "Enable debug logging")
 
+-- | Z2H-5: @init@ takes an optional BACKEND argument. When omitted, the
+-- backend is inferred (co-located service crate, then local registry) —
+-- and the inference is announced. There is no hardcoded default.
 initCommandParser :: Parser Command
-initCommandParser = pure CmdInit
+initCommandParser = CmdInit
+  <$> optional (T.pack <$> argument str
+        ( metavar "BACKEND"
+       <> help "Backend identifier to scaffold for (omit to infer from a co-located service crate or the local registry)"
+        ))
 
 -- | Options relevant to watch mode.
 -- host/port come from synapse.config.json (not CLI flags for watch).
 watchOptionsParser :: Parser Options
-watchOptionsParser = (\cacheDir debug synapse hub ->
+watchOptionsParser = (\cacheDir debug synapse service token tokenFile ->
   defaultOptions
     { optInstallDeps    = False
     , optBuild          = False
@@ -151,7 +166,9 @@
     , optCacheDir       = cacheDir
     , optDebug          = debug
     , optSynapsePath    = synapse
-    , optHubCodegenPath = hub
+    , optServiceCodegenPath = service
+    , optToken          = token
+    , optTokenFile      = tokenFile
     })
   <$> option str
       ( long "cache-dir"
@@ -162,12 +179,33 @@
       )
   <*> switch (long "debug" <> help "Enable debug logging")
   <*> optional (option str (long "synapse"     <> metavar "PATH" <> help "synapse binary path"))
-  <*> optional (option str (long "hub-codegen" <> metavar "PATH" <> help "hub-codegen binary path"))
+  <*> serviceCodegenPathParser "service-codegen binary path"
+  <*> optional (T.pack <$> option str (long "token" <> short 't' <> metavar "JWT" <> help "JWT auth token (overrides SYNAPSE_TOKEN, --token-file, and any access_token in ~/.plexus/<backend>/defaults.json)"))
+  <*> optional (option str (long "token-file" <> metavar "PATH" <> help "Read JWT token from file"))
 
 -- ============================================================================
 -- Shared Parsers
 -- ============================================================================
 
+-- | @--service-codegen PATH@. NAMING-10: the flag's old spelling still works
+-- for one release, hidden from help; 'deprecatedFlagNotes' says so.
+serviceCodegenPathParser :: String -> Parser (Maybe FilePath)
+serviceCodegenPathParser helpText =
+  (<|>) <$> optional (option str (long "service-codegen" <> metavar "PATH" <> help helpText))
+        <*> optional (option str (long oldCodegenFlag <> metavar "PATH" <> hidden))
+
+-- | The generator flag's spelling before NAMING-10.
+oldCodegenFlag :: String
+oldCodegenFlag = "hub-codegen" -- naming-alias: a hidden spelling of --service-codegen; removed by NAMING-18
+
+-- | A note for each deprecated flag among the arguments, to print before
+-- running (NAMING-10).
+deprecatedFlagNotes :: [String] -> [Text]
+deprecatedFlagNotes args =
+  [ "--" <> T.pack oldCodegenFlag <> " is deprecated: use --service-codegen (NAMING-10)"
+  | any (\a -> a == "--" <> oldCodegenFlag || ("--" <> oldCodegenFlag <> "=") `isPrefixOf` a) args
+  ]
+
 -- | Parse target language
 targetParser :: Parser Target
 targetParser = argument readTarget
@@ -267,10 +305,17 @@
      <> metavar "PATH"
      <> help "Path to synapse binary (overrides discovery)"
       ))
+  <*> serviceCodegenPathParser "Path to service-codegen binary (overrides discovery)"
+  <*> optional (T.pack <$> option str
+      ( long "token"
+     <> short 't'
+     <> metavar "JWT"
+     <> help "JWT auth token (overrides SYNAPSE_TOKEN env, --token-file, and any access_token in ~/.plexus/<backend>/defaults.json)"
+      ))
   <*> optional (option str
-      ( long "hub-codegen"
+      ( long "token-file"
      <> metavar "PATH"
-     <> help "Path to hub-codegen binary (overrides discovery)"
+     <> help "Read JWT token from file (overrides any access_token in ~/.plexus/<backend>/defaults.json)"
       ))
 
 -- ============================================================================
diff --git a/src/SynapseCC/Cache.hs b/src/SynapseCC/Cache.hs
--- a/src/SynapseCC/Cache.hs
+++ b/src/SynapseCC/Cache.hs
@@ -51,7 +51,7 @@
 -- │       ├── manifest.json # IR cache manifest
 -- │       ├── cone.json     # Generated IR fragments
 -- │       └── arbor.json
--- └── hub-codegen/
+-- └── service-codegen/
 --     └── typescript/       # Level 3: Code cache
 --         ├── manifest.json # Code cache manifest
 --         ├── cone/         # Generated code per plugin
@@ -124,7 +124,7 @@
       target = cfgTarget config
       debug = optDebug opts
       synapseVer    = toolSynapseVersion tools
-      hubCodegenVer = toolHubCodegenVersion tools
+      serviceCodegenVer = toolServiceCodegenVersion tools
 
   -- If --force flag is set, skip cache entirely
   if optForce opts
@@ -161,7 +161,7 @@
                   let codeToolchain = ccmToolchain codeManifest
                   if tvSynapseCC codeToolchain /= synapseCCVersion ||
                      tvSynapse codeToolchain /= synapseVer ||
-                     isJust (tvHubCodegen codeToolchain) && tvHubCodegen codeToolchain /= Just hubCodegenVer
+                     isJust (tvServiceCodegen codeToolchain) && tvServiceCodegen codeToolchain /= Just serviceCodegenVer
                     then do
                       logDebug debug "Tool versions changed, invalidating code cache"
                       pure $ CacheMiss ToolVersionChanged
@@ -338,7 +338,8 @@
         , ircmToolchain = ToolchainVersions
             { tvSynapseCC = synapseCCVersion
             , tvSynapse = synapseVer
-            , tvHubCodegen = Nothing
+            , tvServiceCodegen = Nothing
+            , tvPlexusCore = Nothing  -- SAFE-4 degraded mode: backend doesn't expose version yet (SAFE-S02)
             }
         , ircmUpdatedAt = timestamp
         , ircmPlugins = plugins
@@ -349,7 +350,7 @@
 
 -- | Write code cache manifest
 writeCodeCacheManifest :: Options -> Backend -> Target -> Map Text CodePluginCache -> Text -> Text -> IO ()
-writeCodeCacheManifest opts backend target plugins synapseVer hubCodegenVer = do
+writeCodeCacheManifest opts backend target plugins synapseVer serviceCodegenVer = do
   cacheDir <- getCodeCacheDir opts backend target
   createDirectoryIfMissing True cacheDir
 
@@ -362,7 +363,8 @@
         , ccmToolchain = ToolchainVersions
             { tvSynapseCC = synapseCCVersion
             , tvSynapse = synapseVer
-            , tvHubCodegen = Just hubCodegenVer
+            , tvServiceCodegen = Just serviceCodegenVer
+            , tvPlexusCore = Nothing  -- SAFE-4 degraded mode (SAFE-S02)
             }
         , ccmUpdatedAt = timestamp
         , ccmPlugins = plugins
diff --git a/src/SynapseCC/Config.hs b/src/SynapseCC/Config.hs
--- a/src/SynapseCC/Config.hs
+++ b/src/SynapseCC/Config.hs
@@ -5,6 +5,7 @@
   ( loadSynapseConfig
   , validateSynapseConfig
   , initSynapseConfig
+  , InitResult(..)
   , synapseConfigPath
 
     -- * Shared config-to-runtime conversions
@@ -22,7 +23,10 @@
 import qualified Data.Text as T
 import System.Directory (doesFileExist)
 
-import SynapseCC.Detect (Detector, ProjectHint(..), runDetectors)
+import SynapseCC.Detect
+  ( Detector, ProjectHint(..), runDetectors
+  , BackendInference(..), InferenceOutcome(..), inferBackend
+  )
 import SynapseCC.Types
 
 -- | Standard config file name (looked up from CWD)
@@ -147,25 +151,66 @@
        , cfgPlugins = tcPlugins tc
        }
 
+-- | Result of a successful @synapse-cc init@.
+data InitResult = InitResult
+  { irHint        :: !ProjectHint   -- ^ Transport detection result ('phReason' explains)
+  , irBackend     :: !Text          -- ^ Backend name written into the config
+  , irBackendNote :: !(Maybe Text)  -- ^ Announcement when the backend was INFERRED
+                                    --   (Nothing when given explicitly)
+  } deriving (Show, Eq)
+
 -- | Scaffold a synapse.config.json, using 'detectors' to infer the right
 -- transport and other settings for the current project.
 --
--- Returns @Left@ if the file already exists.
--- Returns @Right hint@ on success — the caller can inspect 'phReason' to
--- print a message explaining what was detected.
+-- Z2H-5: the backend name is either given explicitly (@Just backend@) or
+-- inferred via 'inferBackend' (co-located service crate, then local
+-- registry at @regHost:regPort@). There is no hardcoded fallback — when
+-- nothing is inferable the result is a @Left@ naming the missing argument.
 --
+-- Returns @Left@ if the file already exists or no backend could be
+-- determined. Returns @Right InitResult@ on success — 'irBackendNote'
+-- carries the inference announcement the caller MUST surface.
+--
 -- Pass 'SynapseCC.Detect.defaultDetectors' for the standard set, or prepend
 -- additional detectors for project-specific overrides.
-initSynapseConfig :: FilePath -> [Detector] -> IO (Either Text ProjectHint)
-initSynapseConfig path detectors = do
+initSynapseConfig
+  :: FilePath      -- ^ Config path (usually 'synapseConfigPath')
+  -> [Detector]    -- ^ Transport detectors
+  -> Maybe Text    -- ^ Explicit backend name (Nothing = infer)
+  -> Text          -- ^ Registry host for inference (default "127.0.0.1")
+  -> Int           -- ^ Registry port for inference (default 4444)
+  -> IO (Either Text InitResult)
+initSynapseConfig path detectors mBackend regHost regPort = do
   exists <- doesFileExist path
   if exists
     then pure $ Left $ T.pack path <> " already exists"
     else do
-      hint <- runDetectors detectors
-      let config = applyHint hint defaultSynapseConfig
-      BL.writeFile path (Pretty.encodePretty' prettyConfig config)
-      pure $ Right hint
+      backendResult <- case mBackend of
+        Just b  -> pure $ Right (b, Nothing)
+        Nothing -> do
+          outcome <- inferBackend regHost regPort
+          pure $ case outcome of
+            Inferred bi -> Right (biBackend bi, Just (biReason bi))
+            AmbiguousRegistry known -> Left $
+              "missing BACKEND argument — multiple backends are registered ("
+              <> T.intercalate ", " known
+              <> "). Pick one: synapse-cc init <backend>"
+            NoInference -> Left $
+              "missing BACKEND argument — no co-located Plexus service crate found"
+              <> " and no backend discoverable on the registry at "
+              <> regHost <> ":" <> T.pack (show regPort)
+              <> ". Run: synapse-cc init <backend>"
+      case backendResult of
+        Left err -> pure $ Left err
+        Right (backend, note) -> do
+          hint <- runDetectors detectors
+          let config = applyHint hint (defaultSynapseConfig backend)
+          BL.writeFile path (Pretty.encodePretty' prettyConfig config)
+          pure $ Right InitResult
+            { irHint        = hint
+            , irBackend     = backend
+            , irBackendNote = note
+            }
 
 -- | Apply detected hints to the default config.
 applyHint :: ProjectHint -> SynapseConfig -> SynapseConfig
diff --git a/src/SynapseCC/Detect.hs b/src/SynapseCC/Detect.hs
--- a/src/SynapseCC/Detect.hs
+++ b/src/SynapseCC/Detect.hs
@@ -25,13 +25,27 @@
   , detectVite
   , detectNextJS
   , detectNodeProject
+
+    -- * Backend inference (Z2H-5)
+  , BackendInference(..)
+  , InferenceOutcome(..)
+  , inferBackend
+  , inferBackendFromCrate
+  , inferBackendFromRegistry
+  , parseServiceCrateName
   ) where
 
 import Control.Applicative ((<|>))
+import Control.Exception (try, SomeException)
 import Control.Monad (foldM)
+import Data.Maybe (listToMaybe, mapMaybe)
 import qualified Data.Text as T
-import System.Directory (doesDirectoryExist, doesFileExist)
+import qualified Data.Text.IO as TIO
+import System.Directory (doesDirectoryExist, doesFileExist, getCurrentDirectory)
+import System.FilePath ((</>), takeDirectory)
 
+import qualified Synapse.Backend.Discovery as D
+
 import SynapseCC.Types (TransportType(..))
 
 -- ============================================================================
@@ -192,3 +206,133 @@
   where
     step True  _    = pure True
     step False path = doesFileExist path
+
+-- ============================================================================
+-- Backend inference (Z2H-5)
+-- ============================================================================
+
+-- | A successfully inferred backend name plus a human-readable explanation
+-- of where it came from. The explanation is ALWAYS surfaced to the user —
+-- bare @synapse-cc init@ must never silently pick a backend.
+data BackendInference = BackendInference
+  { biBackend :: !T.Text  -- ^ Inferred backend identifier
+  , biReason  :: !T.Text  -- ^ Why (announced to the user)
+  } deriving (Show, Eq)
+
+-- | Outcome of trying to infer a backend when none was given.
+data InferenceOutcome
+  = Inferred !BackendInference
+    -- ^ Exactly one candidate found
+  | AmbiguousRegistry ![T.Text]
+    -- ^ Registry reachable but lists multiple backends — user must choose
+  | NoInference
+    -- ^ Nothing inferable (no service crate, registry empty/unreachable)
+  deriving (Show, Eq)
+
+-- | Infer the backend for @synapse-cc init@ when no argument was given.
+--
+-- Sources, in priority order:
+--
+--  1. A co-located Plexus service crate: a @Cargo.toml@ in the current
+--     directory (or its parent) whose package depends on a plexus service
+--     crate (@plexus-rpc@, @plexus-core@, or @plexus-transport@). The crate
+--     name becomes the backend name.
+--  2. The local Plexus registry at the given host:port (default
+--     @127.0.0.1:4444@), reusing the existing
+--     'Synapse.Backend.Discovery' mechanism — but only when it lists
+--     exactly one backend. Multiple backends are ambiguous.
+inferBackend
+  :: T.Text  -- ^ registry host
+  -> Int     -- ^ registry port
+  -> IO InferenceOutcome
+inferBackend regHost regPort = do
+  cwd <- getCurrentDirectory
+  fromCrate <- inferBackendFromCrate cwd
+  case fromCrate of
+    Just bi -> pure (Inferred bi)
+    Nothing -> inferBackendFromRegistry regHost regPort
+
+-- | Look for a co-located Plexus service crate: @Cargo.toml@ in the given
+-- directory, then in its parent. Returns the crate's package name when the
+-- crate looks like a Plexus service (see 'parseServiceCrateName').
+inferBackendFromCrate :: FilePath -> IO (Maybe BackendInference)
+inferBackendFromCrate dir = go [dir, takeDirectory dir]
+  where
+    go [] = pure Nothing
+    go (d:rest) = do
+      let manifest = d </> "Cargo.toml"
+      exists <- doesFileExist manifest
+      if not exists
+        then go rest
+        else do
+          content <- TIO.readFile manifest
+          case parseServiceCrateName content of
+            Just name -> pure $ Just BackendInference
+              { biBackend = name
+              , biReason  = "Inferred backend '" <> name
+                         <> "' from co-located Plexus service crate ("
+                         <> T.pack manifest <> ")"
+              }
+            Nothing -> go rest
+
+-- | Extract the package name from Cargo.toml text — but only when the crate
+-- depends on a Plexus service crate (@plexus-rpc@, @plexus-core@, or
+-- @plexus-transport@). A random Rust crate is NOT a backend.
+--
+-- This is a line-oriented scan, not a full TOML parser: it understands
+-- @[package]@ section @name = "..."@ entries, @[dependencies]@-style section
+-- keys, and table-form deps like @[dependencies.plexus-rpc]@.
+parseServiceCrateName :: T.Text -> Maybe T.Text
+parseServiceCrateName toml =
+  let lns = map T.strip (T.lines toml)
+      -- Pair each line with its enclosing [section]
+      sectioned = go "" lns
+        where
+          go _ [] = []
+          go sec (l:rest)
+            | Just hdr <- sectionHeader l = (hdr, l) : go hdr rest
+            | otherwise                   = (sec, l) : go sec rest
+      sectionHeader l = do
+        inner <- T.stripPrefix "[" l
+        body  <- T.stripSuffix "]" inner
+        pure (T.strip body)
+      isDepSection sec =
+        sec == "dependencies" || "dependencies." `T.isPrefixOf` sec
+        || sec == "workspace.dependencies" || "workspace.dependencies." `T.isPrefixOf` sec
+      plexusCrates = ["plexus-rpc", "plexus-core", "plexus-transport"]
+      -- A dep either appears as a key in a deps section…
+      keyOf l = T.strip (fst (T.breakOn "=" l))
+      hasInlineDep = any (\(sec, l) -> isDepSection sec && keyOf l `elem` plexusCrates) sectioned
+      -- …or as a table-form section header [dependencies.plexus-rpc]
+      hasTableDep = any (\(sec, _) ->
+        any (\c -> sec == "dependencies." <> c
+                || sec == "workspace.dependencies." <> c) plexusCrates) sectioned
+      pkgName = listToMaybe $ mapMaybe
+        (\(sec, l) ->
+          if sec == "package" && keyOf l == "name"
+            then unquote (T.strip (T.drop 1 (snd (T.breakOn "=" l))))
+            else Nothing)
+        sectioned
+      unquote t = T.stripPrefix "\"" =<< T.stripSuffix "\"" t
+  in if hasInlineDep || hasTableDep then pkgName else Nothing
+
+-- | Query the local registry for registered backends, reusing the existing
+-- 'Synapse.Backend.Discovery' machinery (no new registry protocol — Z2H-6
+-- owns registration mechanics). Infers only when EXACTLY one backend is
+-- registered; multiple candidates are ambiguous and reported as such.
+-- Unreachable or empty registry → 'NoInference' (never an error here —
+-- the caller decides how to phrase the failure).
+inferBackendFromRegistry :: T.Text -> Int -> IO InferenceOutcome
+inferBackendFromRegistry regHost regPort = do
+  let discovery = D.registryDiscovery regHost regPort
+  result <- try (D.discoverBackends discovery) :: IO (Either SomeException [D.Backend])
+  case result of
+    Left _        -> pure NoInference
+    Right []      -> pure NoInference
+    Right [b]     -> pure $ Inferred BackendInference
+      { biBackend = D.backendName b
+      , biReason  = "Inferred backend '" <> D.backendName b
+                 <> "' — the only backend registered on the local registry at "
+                 <> regHost <> ":" <> T.pack (show regPort)
+      }
+    Right many    -> pure $ AmbiguousRegistry (map D.backendName many)
diff --git a/src/SynapseCC/Discover.hs b/src/SynapseCC/Discover.hs
--- a/src/SynapseCC/Discover.hs
+++ b/src/SynapseCC/Discover.hs
@@ -1,9 +1,13 @@
--- | Tool discovery - finding synapse and hub-codegen executables
+-- | Tool discovery - finding synapse and service-codegen executables
 module SynapseCC.Discover
   ( discoverTools
   , findTool
   , toolPathToFilePath
   , findInDistNewstyle
+  , serviceCodegenSendsInjectedParams
+  , findServiceCodegen
+  , serviceCodegenName
+  , oldCodegenName
   ) where
 
 import Control.Exception (catch, SomeException, try)
@@ -14,6 +18,7 @@
 import Data.Ord (comparing, Down(..))
 import Data.Text (Text)
 import qualified Data.Text as T
+import qualified Data.Text.Read as TR
 import System.Directory (doesFileExist, doesDirectoryExist, findExecutable, getHomeDirectory, listDirectory, getModificationTime)
 import System.FilePath ((</>))
 import System.Process (readProcessWithExitCode)
@@ -37,27 +42,47 @@
   case synapsePath of
     Nothing -> pure $ Left $ ToolNotFound "synapse" synapseSuggestions
     Just synapseToolPath -> do
-      hubCodegenPath <- case optHubCodegenPath opts of
-        Just explicit -> resolveExplicit "hub-codegen" explicit debug
-        Nothing       -> findTool debug "hub-codegen" hubCodegenFallbackPaths
+      serviceCodegenPath <- case optServiceCodegenPath opts of
+        Just explicit -> resolveExplicit serviceCodegenName explicit debug
+        Nothing       -> findServiceCodegen debug >>= \case
+          Nothing             -> pure Nothing
+          Just (path, mNote)  -> mapM_ logInfo mNote >> pure (Just path)
 
-      case hubCodegenPath of
-        Nothing -> pure $ Left $ ToolNotFound "hub-codegen" hubCodegenSuggestions
-        Just hubCodegenToolPath -> do
+      case serviceCodegenPath of
+        Nothing -> pure $ Left $ ToolNotFound (T.pack serviceCodegenName) serviceCodegenSuggestions
+        Just serviceCodegenToolPath -> do
           -- Run --version for each discovered tool
           synapseVer    <- getToolVersion (toolPathToFilePath synapseToolPath)
-          hubCodegenVer <- getToolVersion (toolPathToFilePath hubCodegenToolPath)
+          serviceCodegenVer <- getToolVersion (toolPathToFilePath serviceCodegenToolPath)
 
           logDebug debug $ "  synapse     " <> T.pack (toolPathToFilePath synapseToolPath) <> "  (" <> synapseVer <> ")"
-          logDebug debug $ "  hub-codegen " <> T.pack (toolPathToFilePath hubCodegenToolPath) <> "  (" <> hubCodegenVer <> ")"
+          logDebug debug $ "  service-codegen " <> T.pack (toolPathToFilePath serviceCodegenToolPath) <> "  (" <> serviceCodegenVer <> ")"
+          when (serviceCodegenSendsInjectedParams serviceCodegenVer) $
+            logInfo $ "the code generator " <> serviceCodegenVer <> " predates 0.5.0: a parameter the server injects "
+              <> "(pdSource: auth, header, derived, …) comes out as an optional `unknown` argument "
+              <> "of the generated methods. Use service-codegen 0.5.0 or later (SYNAPSE-CC-2)."
 
           pure $ Right $ ToolLocations
             { toolSynapse           = synapseToolPath
-            , toolHubCodegen        = hubCodegenToolPath
+            , toolServiceCodegen        = serviceCodegenToolPath
             , toolSynapseVersion    = synapseVer
-            , toolHubCodegenVersion = hubCodegenVer
+            , toolServiceCodegenVersion = serviceCodegenVer
             }
 
+-- | SYNAPSE-CC-2: whether a service-codegen of this version (as 'getToolVersion'
+-- reports it) still writes server-injected parameters into the generated
+-- methods — every release before 0.5.0. An unparseable version is given
+-- the benefit of the doubt.
+serviceCodegenSendsInjectedParams :: Text -> Bool
+serviceCodegenSendsInjectedParams ver =
+  case traverse readNat (T.splitOn "." (T.takeWhile (/= '-') ver)) of
+    Just (major : minor : _) -> (major, minor) < (0, 5)
+    _                        -> False
+  where
+    readNat t = case TR.decimal t of
+      Right (n, rest) | T.null rest -> Just (n :: Int)
+      _                             -> Nothing
+
 -- | Resolve an explicitly-provided path, failing clearly if it doesn't exist
 resolveExplicit :: String -> FilePath -> Bool -> IO (Maybe ToolPath)
 resolveExplicit name path debug = do
@@ -121,7 +146,7 @@
     Left _             -> pure "unknown"
     Right (_, out, _) ->
       let firstLine = T.strip $ T.pack $ head (lines out ++ [""])
-          -- Take last word (handles "hub-codegen 0.1.0" and "3.5.0")
+          -- Take last word (handles "service-codegen 0.1.0" and "3.5.0")
           lastWord  = case reverse (T.words firstLine) of
                         (w:_) -> w
                         []    -> ""
@@ -208,19 +233,47 @@
         ]
   pure $ devPaths ++ installPaths
 
--- | Fallback paths for hub-codegen (checked if not in PATH)
-hubCodegenFallbackPaths :: IO [FilePath]
-hubCodegenFallbackPaths = do
+-- | NAMING-10: the generator is service-codegen; before 0.5.1 it had another
+-- name. Discovery looks for the new name first — on PATH, then beside this
+-- checkout and in the install directories — and only then for the old one.
+serviceCodegenName :: String
+serviceCodegenName = "service-codegen"
+
+-- | The generator's name before NAMING-10, still found (second, with a note)
+-- for one release.
+oldCodegenName :: String
+oldCodegenName = "hub-codegen" -- naming-alias: found after service-codegen; removed by NAMING-18
+
+-- | Find the generator under its name, else under its old one. The second
+-- part is a note to log when only the old name was found.
+findServiceCodegen :: Bool -> IO (Maybe (ToolPath, Maybe Text))
+findServiceCodegen debug = do
+  new <- findTool debug serviceCodegenName (codegenFallbackPaths serviceCodegenName)
+  case new of
+    Just path -> pure (Just (path, Nothing))
+    Nothing -> do
+      old <- findTool debug oldCodegenName (codegenFallbackPaths oldCodegenName)
+      pure $ fmap (\path -> (path, Just (oldNameNote path))) old
+  where
+    oldNameNote path =
+      "found the code generator under its old name, " <> T.pack oldCodegenName <> " ("
+        <> T.pack (toolPathToFilePath path) <> "); the name is deprecated: install "
+        <> T.pack serviceCodegenName <> " (0.5.1 or later) — the old name goes in NAMING-18"
+
+-- | Fallback paths for the generator under a name (checked if not in PATH):
+-- the sibling checkout's build, then the install directories.
+codegenFallbackPaths :: String -> IO [FilePath]
+codegenFallbackPaths name = do
   home <- getHomeDirectory
   let devPaths =
-        [ "../hub-codegen/target/release/hub-codegen"
-        , "../hub-codegen/target/debug/hub-codegen"
-        , "../../hub-codegen/target/release/hub-codegen"
-        , "../../hub-codegen/target/debug/hub-codegen"
+        [ ".." </> name </> "target/release" </> name
+        , ".." </> name </> "target/debug" </> name
+        , "../.." </> name </> "target/release" </> name
+        , "../.." </> name </> "target/debug" </> name
         ]
       installPaths =
-        [ home </> ".plexus/bin/hub-codegen"
-        , home </> ".cargo/bin/hub-codegen"
+        [ home </> ".plexus/bin" </> name
+        , home </> ".cargo/bin" </> name
         ]
   pure $ devPaths ++ installPaths
 
@@ -235,9 +288,9 @@
   , "Add to PATH: export PATH=\"$HOME/.local/bin:$PATH\""
   ]
 
-hubCodegenSuggestions :: [Text]
-hubCodegenSuggestions =
-  [ "Install hub-codegen: cd ../hub-codegen && cargo install --path ."
-  , "Or specify path: --hub-codegen /path/to/hub-codegen"
+serviceCodegenSuggestions :: [Text]
+serviceCodegenSuggestions =
+  [ "Install service-codegen: cd ../service-codegen && cargo install --path ."
+  , "Or specify path: --service-codegen /path/to/service-codegen"
   , "Add to PATH: export PATH=\"$HOME/.cargo/bin:$PATH\""
   ]
diff --git a/src/SynapseCC/Language.hs b/src/SynapseCC/Language.hs
--- a/src/SynapseCC/Language.hs
+++ b/src/SynapseCC/Language.hs
@@ -165,7 +165,9 @@
   pure $ Right ()
 
 installDependencies Rust _ debug = do
-  when debug $ putStrLn "[*] Rust dependency installation not yet implemented (Phase 2 TODO)"
+  -- Z2H-7: no separate install step for Rust — the generated Cargo.toml
+  -- declares all dependencies and `cargo build` resolves them.
+  when debug $ putStrLn "[*] Rust dependencies are declared in the generated Cargo.toml — cargo build resolves them"
   pure $ Right ()
 
 -- | Add specific packages to the project using the detected package manager.
@@ -253,9 +255,29 @@
   when debug $ putStrLn "[*] Python build not yet implemented (Phase 2 TODO)"
   pure $ Right $ CompiledPath $ unGeneratedPath genPath
 
+-- Z2H-7: compile the generated Rust crate with `cargo build`.
+-- Incremental rebuilds are cargo's own concern (its target/ cache);
+-- synapse-cc's manifest/lock cache decides whether this step runs at all.
 buildProject Rust genPath debug = do
-  when debug $ putStrLn "[*] Rust build not yet implemented (Phase 2 TODO)"
-  pure $ Right $ CompiledPath $ unGeneratedPath genPath
+  when debug $ putStrLn $ "[*] Compiling Rust crate in " ++ unGeneratedPath genPath
+
+  mbCargo <- findExecutable "cargo"
+  case mbCargo of
+    Nothing -> pure $ Left $ ToolNotFound "cargo"
+      [ "Install Rust via rustup: https://rustup.rs"
+      , "Ensure cargo is on PATH: export PATH=\"$HOME/.cargo/bin:$PATH\""
+      ]
+    Just cargo -> do
+      result <- runProcess cargo ["build"] (Just $ unGeneratedPath genPath) debug
+      case prExitCode result of
+        ExitSuccess -> do
+          when debug $ putStrLn "  [+] cargo build passed"
+          pure $ Right $ CompiledPath $ unGeneratedPath genPath
+        ExitFailure code -> do
+          -- cargo writes diagnostics to stderr; keep stdout too in case of
+          -- build-script output. These must reach the user verbatim.
+          let combined = T.unlines $ filter (not . T.null . T.strip) [prStdout result, prStderr result]
+          pure $ Left $ LanguageToolError "cargo build" combined code
 
 -- ============================================================================
 -- Testing
diff --git a/src/SynapseCC/Lock.hs b/src/SynapseCC/Lock.hs
--- a/src/SynapseCC/Lock.hs
+++ b/src/SynapseCC/Lock.hs
@@ -9,6 +9,7 @@
   , emptySynapseLock
   ) where
 
+import Control.Applicative ((<|>))
 import Data.Aeson (FromJSON, ToJSON, eitherDecodeFileStrict)
 import qualified Data.Aeson as Aeson
 import qualified Data.Aeson.Encode.Pretty as Pretty
@@ -60,7 +61,7 @@
 data SynapseLock = SynapseLock
   { slVersion    :: !Text
   , slSynapseCC  :: !Text
-  , slHubCodegen :: !Text
+  , slServiceCodegen :: !Text
   , slUpdatedAt  :: !Text
   , slTargets    :: !(Map Text LockTarget)  -- ^ outputDir → LockTarget
   } deriving (Show, Eq)
@@ -69,7 +70,7 @@
   parseJSON = Aeson.withObject "SynapseLock" $ \o -> SynapseLock
     <$> o Aeson..: "version"
     <*> o Aeson..: "synapseCC"
-    <*> o Aeson..: "hubCodegen"
+    <*> (o Aeson..: "serviceCodegen" <|> o Aeson..: "hubCodegen") -- naming-alias: a lock written before NAMING-10 reads
     <*> o Aeson..: "updatedAt"
     <*> o Aeson..: "targets"
 
@@ -77,7 +78,7 @@
   toJSON sl = Aeson.object
     [ "version"    Aeson..= slVersion sl
     , "synapseCC"  Aeson..= slSynapseCC sl
-    , "hubCodegen" Aeson..= slHubCodegen sl
+    , "serviceCodegen" Aeson..= slServiceCodegen sl
     , "updatedAt"  Aeson..= slUpdatedAt sl
     , "targets"    Aeson..= slTargets sl
     ]
@@ -87,7 +88,7 @@
 emptySynapseLock = SynapseLock
   { slVersion    = "1"
   , slSynapseCC  = synapseCCVersion
-  , slHubCodegen = ""
+  , slServiceCodegen = ""
   , slUpdatedAt  = ""
   , slTargets    = Map.empty
   }
@@ -99,7 +100,7 @@
 lockPrettyConfig :: Pretty.Config
 lockPrettyConfig = Pretty.defConfig
   { Pretty.confCompare = Pretty.keyOrder
-      [ "version", "synapseCC", "hubCodegen", "updatedAt", "targets"
+      [ "version", "synapseCC", "serviceCodegen", "updatedAt", "targets"
       , "backend", "irHash", "transport", "files", "sharedTransport"
       ]
   }
diff --git a/src/SynapseCC/Merge.hs b/src/SynapseCC/Merge.hs
--- a/src/SynapseCC/Merge.hs
+++ b/src/SynapseCC/Merge.hs
@@ -27,7 +27,7 @@
 import System.FilePath ((</>), takeDirectory)
 
 -- | Compute SHA-256 hash of text content, returning first 16 hex characters.
--- Algorithm matches hub-codegen hash.rs: SHA-256(utf8_bytes)[..16] as lowercase hex.
+-- Algorithm matches service-codegen hash.rs: SHA-256(utf8_bytes)[..16] as lowercase hex.
 computeFileHash :: Text -> Text
 computeFileHash content =
   T.take 16 $ TE.decodeUtf8 $ Base16.encode $ SHA256.hash $ TE.encodeUtf8 content
diff --git a/src/SynapseCC/Pipeline.hs b/src/SynapseCC/Pipeline.hs
--- a/src/SynapseCC/Pipeline.hs
+++ b/src/SynapseCC/Pipeline.hs
@@ -26,7 +26,9 @@
 import System.Exit (ExitCode(..))
 import System.FilePath ((</>), splitPath)
 
-import Synapse.Monad (initEnv, runSynapseM, SynapseError(..))
+-- The constructors by name: synapse 3.16 adds its own CacheMiss (SYNAPSE-28),
+-- which would clash with SynapseCC.Types' (NAMING-10 moved the bound to 3.16).
+import Synapse.Monad (initEnv, runSynapseM, SynapseError(NavError, TransportError, TransportErrorContext, ParseError, ValidationError, BackendError))
 import Synapse.IR.Builder (buildIR)
 import qualified Synapse.Log as Log
 import qualified Katip
@@ -42,6 +44,9 @@
 import SynapseCC.Cache (getCacheDir)
 import qualified SynapseCC.Merge as Merge
 import qualified SynapseCC.Lock as Lock
+import qualified SynapseCC.Auth as Auth
+import qualified SynapseCC.RegistryResolve as Registry
+import qualified SynapseCC.AuthCheck as AuthCheck
 
 -- ============================================================================
 -- Pipeline Orchestration
@@ -52,6 +57,11 @@
 runPipeline config tools = do
   let debug = optDebug (cfgOptions config)
 
+  -- SAFE-4 (degraded mode): plexus-core does not yet expose its toolchain
+  -- version (see SAFE-S01 spike). Warn once per build that version-gating
+  -- is inactive until SAFE-S02 lands.
+  logDebug debug "  SAFE-4: version-gating inactive (plexus-core does not expose toolchain version; see SAFE-S02)"
+
   -- Step 0: Check cache (unless --force is set)
   cacheResult <- Cache.validateCache config tools
   case cacheResult of
@@ -122,7 +132,7 @@
 synapseCCMarker :: Text
 synapseCCMarker = "\"_generatedBy\": \"synapse-cc\""
 
--- | Marker present in package.json files generated by hub-codegen.
+-- | Marker present in package.json files generated by service-codegen.
 -- Used alongside synapseCCMarker to detect generated (standalone) output directories.
 generatedByMarker :: Text
 generatedByMarker = "\"_generatedBy\""
@@ -198,6 +208,15 @@
       logSuccess $ "Schema ready (" <> T.pack (show pluginCount) <> " plugins)"
       logDebug debug $ "  IR at " <> T.pack (unIRPath irPath)
 
+      -- SAFE-6: warn if backend requires cookie auth but the target's
+      -- existing transport.ts lacks the SAFE-7 marker. Runs BEFORE
+      -- regeneration so the inspection sees the stale (pre-SAFE-7) output.
+      let outputDir = optOutput (cfgOptions config)
+      mWarn <- AuthCheck.detectCookieAuthMismatch (unIRPath irPath) outputDir
+      case mWarn of
+        Just w  -> logInfo w
+        Nothing -> pure ()
+
       -- Step 2: Generate code
       logStep "Generating code..."
       (codeResult, codeMs) <- timeStep $ generateCode config tools irPath Nothing
@@ -209,7 +228,7 @@
           -- Detect standalone vs integration mode.
           --
           -- Check the OUTPUT directory first: if it already has a package.json with a
-          -- "_generatedBy" field, hub-codegen (or a prior synapse-cc run) wrote it
+          -- "_generatedBy" field, service-codegen (or a prior synapse-cc run) wrote it
           -- → standalone, regardless of what the CWD contains.
           --
           -- If outputDir has no package.json, fall back to checking the CWD: if the CWD
@@ -224,7 +243,12 @@
               outPkgJsonPath = outputDir </> "package.json"
           outPkgExists <- doesFileExist outPkgJsonPath
           isIntegration <-
-            if outPkgExists
+            if cfgTarget config /= TypeScript
+              -- Z2H-7: integration mode is a TypeScript (package.json) concept.
+              -- A Rust target emits a standalone crate; a package.json in the
+              -- CWD must not suppress the cargo build step.
+              then pure False
+            else if outPkgExists
               then do
                 -- outputDir has a package.json — if it has _generatedBy it's ours (standalone)
                 content <- TIO.readFile outPkgJsonPath
@@ -286,10 +310,12 @@
 
           let genPath = GeneratedPath outputDir
 
-          -- Write synapse-cc's tsconfig to the output dir.
+          -- Write synapse-cc's tsconfig to the output dir (TypeScript only —
+          -- a generated Rust crate has no business containing a tsconfig).
           -- Standalone mode: tight config (noEmit, transport-specific types).
           -- Integration mode: project-references config (composite, declaration).
-          do logDebug debug "  Writing tsconfig.json"
+          when (cfgTarget config == TypeScript) $ do
+             logDebug debug "  Writing tsconfig.json"
              let tsconfig = if isIntegration
                               then generateIntegrationTsconfig
                               else generateTsconfig (optTransport opts)
@@ -301,7 +327,7 @@
           let depsToAdd    = coDependencies out
               devDepsToAdd = if isIntegration then Map.empty else coDevDependencies out
               depPath      = if isIntegration then pmPath else genPath
-          (installResult, installMs) <- if optInstallDeps opts
+          (installResult, installMs) <- if optInstallDeps opts && cfgTarget config == TypeScript
             then do
               (addResult, addMs) <- timeStep $ Language.addDependencies
                 depPath
@@ -325,7 +351,11 @@
                         Left _   -> pure ()
                       pure (result, addMs + ms)
             else do
-              logDebug debug "Skipping dependency installation (--no-install)"
+              if cfgTarget config == TypeScript
+                then logDebug debug "Skipping dependency installation (--no-install)"
+                -- Z2H-7: no package-manager step for Rust — dependencies live
+                -- in the generated Cargo.toml and cargo build resolves them.
+                else logDebug debug "Dependencies declared in generated Cargo.toml (resolved by cargo build)"
               pure (Right (), 0)
 
           case installResult of
@@ -399,7 +429,7 @@
 writeCache config tools irPath _compiledPath codegenOutput = do
   let debug         = optDebug (cfgOptions config)
       synapseVer    = toolSynapseVersion tools
-      hubCodegenVer = coHubCodegenVersion codegenOutput
+      serviceCodegenVer = coServiceCodegenVersion codegenOutput
   logDebug debug "Writing cache manifests..."
 
   -- Read IR to extract plugin hashes
@@ -415,7 +445,7 @@
         , cpcCachedAt   = ""
         }
   Cache.writeCodeCacheManifest (cfgOptions config) (cfgBackend config) (cfgTarget config)
-    pluginCaches synapseVer hubCodegenVer
+    pluginCaches synapseVer serviceCodegenVer
 
   -- Write synapse.lock (project-level, committed to git)
   irBytes <- BS.readFile (unIRPath irPath)
@@ -444,7 +474,7 @@
   Lock.writeSynapseLock $ lock
     { Lock.slTargets    = Map.insert (T.pack outputDir) lt (Lock.slTargets lock)
     , Lock.slUpdatedAt  = T.pack (formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" now)
-    , Lock.slHubCodegen = hubCodegenVer
+    , Lock.slServiceCodegen = serviceCodegenVer
     }
 
   logDebug debug "  Cache manifests written"
@@ -531,7 +561,7 @@
   -- Build providers first
   providerResults <- mapM buildTarget providers
 
-  -- Build consumers: run hub-codegen for plugins only, then write shim files
+  -- Build consumers: run service-codegen for plugins only, then write shim files
   consumerResults <- forM consumers $ \(name, tc) -> do
     let config = buildConfigFromTarget opts sc tc
     logInfo $ "Building \"" <> name <> "\" → " <> T.pack (optOutput (cfgOptions config))
@@ -644,11 +674,11 @@
       logDebug debug "  No backend, deps, build, or tests for transport-only target"
       pure $ Right $ CompiledPath outputDir
 
--- | Call hub-codegen --generate transport (no IR argument needed).
+-- | Call service-codegen --generate transport (no IR argument needed).
 generateTransportCode :: Config -> ToolLocations -> IO (Either SynapseCCError CodegenOutput)
 generateTransportCode config tools = do
   let debug          = optDebug (cfgOptions config)
-      hubCodegenPath = toolPathToFilePath (toolHubCodegen tools)
+      serviceCodegenPath = toolPathToFilePath (toolServiceCodegen tools)
       opts           = cfgOptions config
       targetArg      = T.unpack (targetToText (cfgTarget config))
       args =
@@ -659,14 +689,14 @@
                                WsTransport      -> "ws"
                                BrowserTransport -> "browser"
         ]
-  result <- runProcess hubCodegenPath args Nothing debug
+  result <- runProcess serviceCodegenPath args Nothing debug
   case prExitCode result of
     ExitSuccess ->
       case eitherDecodeStrict (TE.encodeUtf8 (prStdout result)) of
-        Left parseErr -> pure $ Left $ HubCodegenError (T.pack parseErr) 0
+        Left parseErr -> pure $ Left $ ServiceCodegenError (T.pack parseErr) 0
         Right out     -> pure $ Right out
     ExitFailure code ->
-      pure $ Left $ HubCodegenError (prStderr result) code
+      pure $ Left $ ServiceCodegenError (prStderr result) code
 
 -- ============================================================================
 -- IR Generation
@@ -679,51 +709,77 @@
       Backend bkName = cfgBackend config
       outputDir = optOutput (cfgOptions config)
       opts = cfgOptions config
-      host = cfgHost config
-      port = read (T.unpack (cfgPort config)) :: Int
+      regHost = cfgHost config
+      regPort = read (T.unpack (cfgPort config)) :: Int
       generatorInfo = ["synapse-cc:" <> synapseCCVersion]
 
-  -- Compute IR file path in the cache directory:
-  --   <cacheDir>/synapse/ir/<backend>/ir.json
-  cacheDir <- getCacheDir opts
-  let irDir  = cacheDir </> "synapse" </> "ir" </> T.unpack bkName
-      irFile = irDir </> "ir.json"
+  -- SAFE-3 + Z2H-6: resolve backend host:port via the registry.
+  -- The registry endpoint comes from --host/--port; at their defaults,
+  -- PLEXUS_REGISTRY_URL may override (default ws://127.0.0.1:4444).
+  (effHost, effPort) <- Registry.resolveRegistryEndpoint regHost (cfgPort config)
+  resolution <- Registry.resolveBackendAddress effHost effPort bkName
+  hpResult <- case resolution of
+    Registry.ResolvedFromRegistry h p -> do
+      logDebug debug $ "  Registry resolved " <> bkName <> " -> " <> h <> ":" <> T.pack (show p)
+      pure $ Right (h, p)
+    Registry.ResolvedFallback h p -> do
+      logDebug debug $ "  Registry unreachable; using provided address " <> h <> ":" <> T.pack (show p)
+      pure $ Right (h, p)
+    Registry.NotInRegistry known ->
+      -- Z2H-6: actionable error naming the registration step.
+      pure $ Left $ BackendUnreachable bkName $
+        "Backend '" <> bkName <> "' is not registered. Known: "
+        <> (if null known then "<none>" else T.intercalate ", " known)
+        <> ". Backends self-register at startup — start '" <> bkName
+        <> "' (registry at $PLEXUS_REGISTRY_URL, default ws://127.0.0.1:4444),"
+        <> " or pass -H/-P to connect directly."
+  case hpResult of
+    Left e -> pure (Left e)
+    Right (host, port) -> do
+      -- Compute IR file path in the cache directory:
+      --   <cacheDir>/synapse/ir/<backend>/ir.json
+      cacheDir <- getCacheDir opts
+      let irDir  = cacheDir </> "synapse" </> "ir" </> T.unpack bkName
+          irFile = irDir </> "ir.json"
 
-  -- Ensure output and IR cache directories exist
-  createDirectoryIfMissing True outputDir
-  createDirectoryIfMissing True irDir
+      -- Ensure output and IR cache directories exist
+      createDirectoryIfMissing True outputDir
+      createDirectoryIfMissing True irDir
 
-  -- Call plexus-synapse library directly (no subprocess).
-  -- Pass [] as path: the backend is encoded in the env; [] = walk from root.
-  logDebug debug $ "  Connecting to " <> host <> ":" <> T.pack (show port)
-  logger <- Log.makeLogger Katip.ErrorS
-  env <- initEnv host port bkName logger
-  -- Wrap in try to catch hard exceptions from child-plugin fetch errors
-  rawResult <- try (runSynapseM env (buildIR generatorInfo []))
-  result <- case rawResult of
-    Left (ex :: SomeException) ->
-      let raw = T.pack (show ex)
-          msg = if "Connection refused" `T.isInfixOf` raw || "does not exist" `T.isInfixOf` raw
-                  then "Cannot connect to backend — is it running?"
-                  else T.takeWhile (/= '\n') raw
-      in pure $ Left msg
-    Right (Left synapseErr)    -> pure $ Left $ formatSynapseError synapseErr
-    Right (Right ir)           -> pure $ Right ir
+      -- Call plexus-synapse library directly (no subprocess).
+      -- Pass [] as path: the backend is encoded in the env; [] = walk from root.
+      logDebug debug $ "  Connecting to " <> host <> ":" <> T.pack (show port)
+      logger <- Log.makeLogger Katip.ErrorS
+      -- SAFE-2: resolve JWT token from --token / SYNAPSE_TOKEN / --token-file /
+      -- cookies.access_token in ~/.plexus/<backend>/defaults.json (SELF-3)
+      token <- Auth.resolveToken opts bkName
+      env <- initEnv host port bkName logger token
+      -- Wrap in try to catch hard exceptions from child-plugin fetch errors
+      rawResult <- try (runSynapseM env (buildIR generatorInfo []))
+      result <- case rawResult of
+        Left (ex :: SomeException) ->
+          let raw = T.pack (show ex)
+              msg = if "Connection refused" `T.isInfixOf` raw || "does not exist" `T.isInfixOf` raw
+                      then "Cannot connect to backend — is it running?"
+                      else T.takeWhile (/= '\n') raw
+          in pure $ Left msg
+        Right (Left synapseErr)    -> pure $ Left $ formatSynapseError synapseErr
+        Right (Right ir)           -> pure $ Right ir
 
-  case result of
-    Left msg -> do
-      logDebug debug $ "  Synapse error: " <> msg
-      pure $ Left $ SynapseError msg 1
+      case result of
+        Left msg -> do
+          logDebug debug $ "  Synapse error: " <> msg
+          pure $ Left $ SynapseError msg 1
 
-    Right ir -> do
-      -- Encode IR to JSON and write to cache path
-      let irBytes = BL.toStrict (Aeson.encode ir)
-      BS.writeFile irFile irBytes
+        Right ir -> do
+          -- Encode IR to JSON and write to cache path
+          let irBytes = BL.toStrict (Aeson.encode ir)
+          BS.writeFile irFile irBytes
 
-      -- Validate by round-tripping (ensures we can parse what we wrote)
-      case eitherDecodeStrict irBytes of
-        Left parseErr -> pure $ Left $ InvalidIR $ T.pack parseErr
-        Right (_ :: IRData) -> pure $ Right $ IRPath irFile
+          -- Validate by round-tripping (ensures we can parse what we wrote)
+          case eitherDecodeStrict irBytes of
+            Left parseErr -> pure $ Left $ InvalidIR $ T.pack parseErr
+            Right (_ :: IRData) -> pure $ Right $ IRPath irFile
 
 -- | Format a SynapseError (from plexus-synapse) for display
 formatSynapseError :: SynapseError -> Text
@@ -739,6 +795,7 @@
   ParseError t      -> "Parse error: " <> t
   ValidationError t -> "Validation error: " <> t
   BackendError bt _ -> "Backend error: " <> T.pack (show bt)
+  other             -> "synapse: " <> T.pack (show other)   -- a later synapse's own (3.16: StaleSchema, CacheMiss)
 
 -- | IR structure for reading plugin hashes
 -- We only need the fields relevant for caching
@@ -768,14 +825,14 @@
 -- Code Generation
 -- ============================================================================
 
--- | Generate code using hub-codegen, returning parsed JSON output.
+-- | Generate code using service-codegen, returning parsed JSON output.
 -- Pass @Just namespaces@ to restrict generation to specific plugin namespaces
 -- (equivalent to @--generate plugins --plugins ns1,ns2@).
 -- Pass @Nothing@ for a full generation pass.
 generateCode :: Config -> ToolLocations -> IRPath -> Maybe [Text] -> IO (Either SynapseCCError CodegenOutput)
 generateCode config tools irPath nsFilter = do
   let debug          = optDebug (cfgOptions config)
-      hubCodegenPath = toolPathToFilePath (toolHubCodegen tools)
+      serviceCodegenPath = toolPathToFilePath (toolServiceCodegen tools)
       opts           = cfgOptions config
       target         = cfgTarget config
       targetArg      = T.unpack (targetToText target)
@@ -800,13 +857,13 @@
         ++ pluginFilterArgs
         ++ [ unIRPath irPath ]
 
-  result <- runProcess hubCodegenPath args Nothing debug
+  result <- runProcess serviceCodegenPath args Nothing debug
 
   case prExitCode result of
     ExitSuccess -> do
       let stdoutBytes = TE.encodeUtf8 (prStdout result)
       case eitherDecodeStrict stdoutBytes of
-        Left parseErr -> pure $ Left $ HubCodegenError (T.pack parseErr) 0
+        Left parseErr -> pure $ Left $ ServiceCodegenError (T.pack parseErr) 0
         Right out     -> pure $ Right out
     ExitFailure code ->
-      pure $ Left $ HubCodegenError (prStderr result) code
+      pure $ Left $ ServiceCodegenError (prStderr result) code
diff --git a/src/SynapseCC/RegistryResolve.hs b/src/SynapseCC/RegistryResolve.hs
new file mode 100644
--- /dev/null
+++ b/src/SynapseCC/RegistryResolve.hs
@@ -0,0 +1,82 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Registry-based backend address resolution (SAFE-3).
+--
+-- synapse-cc previously assumed every backend lived at the registry's
+-- own host:port (typically @127.0.0.1:4444@), which broke for backends
+-- like @lforge@ that live on different ports. This module queries the
+-- Plexus registry to map a backend name to its actual host:port.
+--
+-- The registry's location defaults to @127.0.0.1:4444@; pass
+-- @--host@\/@--port@ to override.
+--
+-- Resolution flow:
+--
+--  1. Query @registry.get@ at the given registry address for the named backend.
+--  2. If found, return the registered host:port.
+--  3. If the backend is not registered, list known backends and fail.
+--  4. If the registry itself is unreachable, fall back to the registry
+--     address as the backend address (preserves the legacy "substrate IS
+--     the registry on 4444" path) and warn.
+module SynapseCC.RegistryResolve
+  ( ResolveResult(..)
+  , resolveBackendAddress
+  , resolveRegistryEndpoint
+  ) where
+
+import Control.Exception (try, SomeException)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Text.Read (readMaybe)
+
+import qualified Synapse.Backend.Discovery as D
+
+-- | Outcome of resolving a backend name against the registry.
+data ResolveResult
+  = ResolvedFromRegistry  !Text !Int  -- ^ Backend found in registry: returned host, port
+  | ResolvedFallback      !Text !Int  -- ^ Registry unreachable: falling back to provided address
+  | NotInRegistry         ![Text]     -- ^ Backend not in registry; suggestions = known backend names
+  deriving stock (Show, Eq)
+
+-- | Z2H-6: resolve the effective registry endpoint from the CLI flags.
+--
+-- When @--host@/@--port@ sit at their defaults (@127.0.0.1@/@4444@),
+-- the @PLEXUS_REGISTRY_URL@ env var (shared with plexus-transport's
+-- boot-time self-registration and synapse) may point the registry
+-- elsewhere. Explicit non-default flags always win.
+resolveRegistryEndpoint
+  :: Text  -- ^ cfg host (CLI --host, default "127.0.0.1")
+  -> Text  -- ^ cfg port (CLI --port, default "4444")
+  -> IO (Text, Int)
+resolveRegistryEndpoint cfgHost cfgPortText = do
+  let cfgPort = maybe D.defaultRegistryPort id (readMaybe (T.unpack cfgPortText))
+  if cfgHost == D.defaultRegistryHost && cfgPort == D.defaultRegistryPort
+    then D.registryEndpointFromEnv
+    else pure (cfgHost, cfgPort)
+
+-- | Resolve a backend name to a host:port using the registry at the
+-- provided address. Always returns SOMETHING — never throws.
+resolveBackendAddress
+  :: Text   -- ^ registry host (also fallback)
+  -> Int    -- ^ registry port (also fallback)
+  -> Text   -- ^ backend name to resolve
+  -> IO ResolveResult
+resolveBackendAddress regHost regPort bkName = do
+  let discovery = D.registryDiscovery regHost regPort
+  -- Try to look up the named backend.
+  result <- try (D.getBackendInfo discovery bkName) :: IO (Either SomeException (Maybe D.Backend))
+  case result of
+    Right (Just b) ->
+      pure $ ResolvedFromRegistry (D.backendHost b) (D.backendPort b)
+    Right Nothing -> do
+      -- Backend not in registry. Try to list registered ones for a helpful error.
+      knownE <- try (D.discoverBackends discovery) :: IO (Either SomeException [D.Backend])
+      let known = case knownE of
+            Right bs -> map D.backendName bs
+            Left _   -> []
+      pure $ NotInRegistry known
+    Left _ ->
+      -- Registry call failed (registry unreachable or RPC error).
+      -- Fall back to the registry address as the backend address —
+      -- preserves the "substrate is reachable on the same port" legacy path.
+      pure $ ResolvedFallback regHost regPort
diff --git a/src/SynapseCC/Types.hs b/src/SynapseCC/Types.hs
--- a/src/SynapseCC/Types.hs
+++ b/src/SynapseCC/Types.hs
@@ -56,7 +56,8 @@
   , summarizeStderr
   ) where
 
-import Data.Aeson (FromJSON, ToJSON, fieldLabelModifier)
+import Control.Applicative ((<|>))
+import Data.Aeson (FromJSON(..), ToJSON, fieldLabelModifier)
 import qualified Data.Aeson as Aeson
 import Data.Aeson.Types (Parser)
 import Data.Map.Strict (Map)
@@ -67,6 +68,7 @@
 import Data.Version (showVersion)
 import GHC.Generics (Generic)
 import Paths_synapse_cc (version)
+import Synapse.Self.Command (SelfCommand)
 import System.FilePath (FilePath)
 
 -- ============================================================================
@@ -134,7 +136,9 @@
   , optForce           :: !Bool
   , optDebug           :: !Bool
   , optSynapsePath     :: !(Maybe FilePath)  -- ^ Override synapse binary path
-  , optHubCodegenPath  :: !(Maybe FilePath)  -- ^ Override hub-codegen binary path
+  , optServiceCodegenPath  :: !(Maybe FilePath)  -- ^ Override service-codegen binary path
+  , optToken           :: !(Maybe Text)      -- ^ JWT token (--token; SAFE-2)
+  , optTokenFile       :: !(Maybe FilePath)  -- ^ Token file path (--token-file; SAFE-2)
   } deriving stock (Show, Eq, Generic)
 
 -- | Default options
@@ -149,7 +153,9 @@
   , optForce           = False
   , optDebug           = False
   , optSynapsePath     = Nothing
-  , optHubCodegenPath  = Nothing
+  , optServiceCodegenPath  = Nothing
+  , optToken           = Nothing
+  , optTokenFile       = Nothing
   }
 
 -- ============================================================================
@@ -262,11 +268,14 @@
     , Just $ "targets"        Aeson..= scTargets sc
     ]
 
-defaultSynapseConfig :: SynapseConfig
-defaultSynapseConfig = SynapseConfig
+-- | Scaffold template for synapse.config.json, parameterised by backend name.
+-- Z2H-5: there is deliberately NO zero-argument default — the backend must be
+-- supplied explicitly or inferred (see 'SynapseCC.Detect.inferBackend').
+defaultSynapseConfig :: Text -> SynapseConfig
+defaultSynapseConfig backend = SynapseConfig
   { scSchema         = "1.0"
   , scLanguage       = "typescript"
-  , scBackend        = Just "substrate"
+  , scBackend        = Just backend
   , scUrl            = Just "ws://127.0.0.1:4444"
   , scPackageManager = "bun"
   , scWatch          = defaultWatchConfig
@@ -291,13 +300,20 @@
 -- ============================================================================
 
 -- | Top-level CLI command
+--
+-- @CmdSelf@ (SELF-6) wraps the shared 'Synapse.Self.Command.SelfCommand'
+-- surface so @synapse-cc _self@ dispatches through the same handler as
+-- @synapse _self@. The payload is kept as an opaque dynamic here
+-- (@!SelfCommand@) rather than pattern-matched by synapse-cc itself —
+-- the dispatcher in @app\/Main.hs@ only calls 'runSelfCommand'.
 data Command
   = CmdBuild Config         -- ^ Build with explicit target/backend (CLI args)
   | CmdBuildFromConfig      -- ^ Build all targets from synapse.config.json
       Options               -- ^ CLI flags that override per-target config
   | CmdWatch WatchArgs      -- ^ Watch mode
   | CmdWait WaitArgs        -- ^ Wait for backends to become reachable
-  | CmdInit                 -- ^ Scaffold synapse.config.json
+  | CmdInit !(Maybe Text)   -- ^ Scaffold synapse.config.json (Z2H-5: optional explicit backend; Nothing = infer)
+  | CmdSelf SelfCommand     -- ^ @_self@ — see "Synapse.Self.Command"
   deriving stock (Show, Eq)
 
 -- | Arguments for the watch subcommand
@@ -325,9 +341,9 @@
 -- | Discovered locations of required tools
 data ToolLocations = ToolLocations
   { toolSynapse            :: !ToolPath
-  , toolHubCodegen         :: !ToolPath
+  , toolServiceCodegen         :: !ToolPath
   , toolSynapseVersion     :: !Text   -- ^ Result of @synapse --version@
-  , toolHubCodegenVersion  :: !Text   -- ^ Result of @hub-codegen --version@
+  , toolServiceCodegenVersion  :: !Text   -- ^ Result of @service-codegen --version@
   } deriving stock (Show, Eq)
 
 -- | Path to a discovered tool
@@ -361,10 +377,23 @@
 data ToolchainVersions = ToolchainVersions
   { tvSynapseCC   :: !Text
   , tvSynapse     :: !Text
-  , tvHubCodegen  :: !(Maybe Text)  -- Only known after codegen
+  , tvServiceCodegen  :: !(Maybe Text)  -- Only known after codegen
+  , tvPlexusCore  :: !(Maybe Text)  -- SAFE-4: backend-reported plexus-core version (Nothing until SAFE-S02)
   } deriving stock (Show, Eq, Generic)
-    deriving anyclass (FromJSON, ToJSON)
+    deriving anyclass (ToJSON)
 
+-- | NAMING-10: a cache manifest written before the generator's rename keeps
+-- its version under the field's old name; it is read, so the version still
+-- invalidates the cache when it moves.
+instance FromJSON ToolchainVersions where
+  parseJSON v = do
+    tv <- Aeson.genericParseJSON Aeson.defaultOptions v
+    case tvServiceCodegen tv of
+      Just _  -> pure tv
+      Nothing -> flip (Aeson.withObject "ToolchainVersions") v $ \o -> do
+        old <- o Aeson..:? "tvHubCodegen" -- naming-alias: removed by NAMING-18
+        pure tv { tvServiceCodegen = old }
+
 -- | Cache entry for a single plugin's IR
 data IRPluginCache = IRPluginCache
   { ipcIRHash       :: !Text         -- Hash of the generated IR for this plugin
@@ -394,7 +423,7 @@
   } deriving stock (Show, Eq, Generic)
     deriving anyclass (FromJSON, ToJSON)
 
--- | Cache manifest for generated code (hub-codegen/typescript/manifest.json)
+-- | Cache manifest for generated code (service-codegen/typescript/manifest.json)
 data CodeCacheManifest = CodeCacheManifest
   { ccmVersion    :: !Text
   , ccmTarget     :: !Text
@@ -425,30 +454,29 @@
   deriving stock (Show, Eq)
 
 -- ============================================================================
--- Codegen Output Types (JSON response from hub-codegen --output-format json)
+-- Codegen Output Types (JSON response from service-codegen --output-format json)
 -- ============================================================================
 
--- | JSON output from hub-codegen when invoked with --output-format json
+-- | JSON output from service-codegen when invoked with --output-format json
 data CodegenOutput = CodegenOutput
   { coFiles             :: !(Map Text Text)   -- ^ Generated file contents (relPath -> content)
   , coFileHashes        :: !(Map Text Text)   -- ^ Per-file content hashes (relPath -> hash)
   , coWarnings          :: ![CodegenWarning]  -- ^ Warnings from generation
-  , coHubCodegenVersion :: !Text             -- ^ hub-codegen version string
+  , coServiceCodegenVersion :: !Text             -- ^ service-codegen version string
   , coDependencies      :: !(Map Text Text)   -- ^ Runtime dependencies (name -> version)
   , coDevDependencies   :: !(Map Text Text)   -- ^ Dev dependencies (name -> version)
   } deriving stock (Show, Eq, Generic)
 
+-- | NAMING-10: the version is @serviceCodegenVersion@; a generator before
+-- 0.5.1 wrote it under its old name, which still reads.
 instance FromJSON CodegenOutput where
-  parseJSON = Aeson.genericParseJSON Aeson.defaultOptions
-    { fieldLabelModifier = \case
-        "coFiles"             -> "files"
-        "coFileHashes"        -> "fileHashes"
-        "coWarnings"          -> "warnings"
-        "coHubCodegenVersion" -> "hubCodegenVersion"
-        "coDependencies"      -> "dependencies"
-        "coDevDependencies"   -> "devDependencies"
-        other -> other
-    }
+  parseJSON = Aeson.withObject "CodegenOutput" $ \o -> CodegenOutput
+    <$> o Aeson..: "files"
+    <*> o Aeson..: "fileHashes"
+    <*> o Aeson..: "warnings"
+    <*> (o Aeson..: "serviceCodegenVersion" <|> o Aeson..: "hubCodegenVersion") -- naming-alias: removed by NAMING-18
+    <*> o Aeson..: "dependencies"
+    <*> o Aeson..: "devDependencies"
 
 -- | A warning emitted during code generation
 data CodegenWarning = CodegenWarning
@@ -474,8 +502,8 @@
     -- ^ Tool not found with suggestions
   | SynapseError !Text !Int
     -- ^ Synapse execution failed with stderr and exit code
-  | HubCodegenError !Text !Int
-    -- ^ hub-codegen execution failed with stderr and exit code
+  | ServiceCodegenError !Text !Int
+    -- ^ service-codegen execution failed with stderr and exit code
   | LanguageToolError !Text !Text !Int
     -- ^ Language tool (npm, tsc, etc.) failed
   | CacheError !Text
@@ -537,9 +565,9 @@
   SynapseError msg _ ->
     "[!] Error: " <> msg <> "\n"
 
-  HubCodegenError stderr exitCode ->
+  ServiceCodegenError stderr exitCode ->
     T.unlines
-      [ "[!] Error: hub-codegen failed (exit code " <> T.pack (show exitCode) <> ")"
+      [ "[!] Error: service-codegen failed (exit code " <> T.pack (show exitCode) <> ")"
       , ""
       , "Output:"
       , stderr
diff --git a/src/SynapseCC/Watch.hs b/src/SynapseCC/Watch.hs
--- a/src/SynapseCC/Watch.hs
+++ b/src/SynapseCC/Watch.hs
@@ -40,6 +40,7 @@
 import Plexus.Client (SubstrateConfig(..), defaultConfig)
 import qualified Plexus.Transport as Transport
 import Plexus.Types (PlexusStreamItem(..))
+import Plexus.RemoteError (renderRemoteError)
 import Synapse.Monad (initEnv, runSynapseM)
 import Synapse.IR.Builder (buildIR)
 import qualified Synapse.Log as Log
@@ -52,6 +53,7 @@
 import SynapseCC.Merge (applyMerge)
 import SynapseCC.Pipeline (formatSynapseError, generateCode, runPipeline)
 import SynapseCC.Types
+import qualified SynapseCC.Auth as Auth
 
 -- ============================================================================
 -- Public Entry Point
@@ -181,7 +183,7 @@
             _ -> Nothing
         _ -> Nothing
   | otherwise = Nothing
-extractHashValue (StreamError _ _ err _) = Just (Left err)
+extractHashValue (StreamError _ _ err _) = Just (Left (renderRemoteError err))
 extractHashValue _                        = Nothing
 
 -- ============================================================================
@@ -209,7 +211,9 @@
 
   -- Fetch fresh IR via plexus-synapse library
   logger <- Log.makeLogger Katip.ErrorS
-  env <- initEnv host port bkName logger
+  -- SAFE-2: resolve JWT token before connecting
+  token <- Auth.resolveToken opts bkName
+  env <- initEnv host port bkName logger token
   irResult <- runSynapseM env (buildIR generatorInfo [])
 
   case irResult of
@@ -277,7 +281,7 @@
                   Left err -> logInfo $ "  [!] " <> ns <> " (" <> targetName <> "): " <> err
                   Right () -> logSuccess $ ns <> " (" <> targetName <> ") rebuilt in " <> T.pack (show ms) <> "ms"
 
--- | Rebuild a single plugin namespace for a single target using hub-codegen.
+-- | Rebuild a single plugin namespace for a single target using service-codegen.
 rebuildPlugin
   :: WatchArgs
   -> SynapseConfig
@@ -385,7 +389,7 @@
 -- | Synthesize a SynapseConfig from CLI args alone (no config file)
 synapseConfigFromArgs :: WatchArgs -> SynapseConfig
 synapseConfigFromArgs watchArgs =
-  defaultSynapseConfig { scBackend = Just (waBackend watchArgs) }
+  defaultSynapseConfig (waBackend watchArgs)
 
 -- | Parse host and port from a WebSocket URL. Alias for 'Config.parseWsUrl'.
 parseUrl :: Text -> (Text, Text)
diff --git a/synapse-cc.cabal b/synapse-cc.cabal
--- a/synapse-cc.cabal
+++ b/synapse-cc.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               synapse-cc
-version:            0.3.3
+version:            0.4.0
 synopsis:           Unified compiler toolchain for Plexus backends
 description:
   synapse-cc orchestrates the complete pipeline from backend schema
@@ -10,14 +10,25 @@
 maintainer:         dev@hypermemetic.com
 category:           Development
 build-type:         Simple
-extra-doc-files:    PLAN.md, README.md
+extra-doc-files:    PLAN.md, README.md, CHANGELOG.md
 
+flag live-tests
+  description: Build synapse-cc-test, which needs a substrate backend
+               registered at 127.0.0.1:4444. Opt-in, so a default
+               `cabal test` needs nothing running.
+               Enable with: cabal test -f live-tests
+  default:     False
+  manual:      True
+
 common warnings
     ghc-options: -Wall
 
 library
     import:           warnings
     exposed-modules:  SynapseCC.Types
+                    , SynapseCC.Auth
+                    , SynapseCC.AuthCheck
+                    , SynapseCC.RegistryResolve
                     , SynapseCC.CLI
                     , SynapseCC.Config
                     , SynapseCC.Detect
@@ -53,8 +64,8 @@
                     , transformers >= 0.6
                     , unordered-containers >= 0.2
                     , fsnotify >= 0.4
-                    , plexus-protocol
-                    , plexus-synapse
+                    , plexus-protocol >= 0.6 && < 0.7
+                    , plexus-synapse >= 4.0 && < 5
                     , katip >= 0.8 && < 0.9
     autogen-modules:  Paths_synapse_cc
     other-modules:    Paths_synapse_cc
@@ -75,6 +86,7 @@
     build-depends:    base >= 4.14 && < 5
                     , synapse-cc
                     , text
+                    , plexus-synapse >= 4.0 && < 5
     hs-source-dirs:   app
     default-language: Haskell2010
     ghc-options:      -threaded -rtsopts -with-rtsopts=-N
@@ -99,8 +111,48 @@
                     , bytestring >= 0.11
                     , containers >= 0.6
                     , optparse-applicative >= 0.18
-                    , plexus-protocol
+                    , plexus-protocol >= 0.6 && < 0.7
+                    , plexus-synapse >= 4.0 && < 5
                     , stm >= 2.5
     default-extensions: OverloadedStrings
                       , LambdaCase
                       , ScopedTypeVariables
+    if !flag(live-tests)
+        buildable: False
+
+test-suite safe6-authcheck
+    default-language: Haskell2010
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          AuthCheckSpec.hs
+    build-depends:    base >= 4.14 && < 5
+                    , synapse-cc
+                    , aeson >= 2.0
+                    , aeson-qq >= 0.8
+                    , bytestring >= 0.11
+                    , filepath >= 1.4
+                    , temporary >= 1.3
+                    , text >= 1.2
+    default-extensions: OverloadedStrings, QuasiQuotes
+    ghc-options:      -threaded
+
+-- NAMING-10: the naming gate and the old names read but never written.
+-- Hermetic (no backend), unlike synapse-cc-test.
+test-suite naming
+    default-language: Haskell2010
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          NamingSpec.hs
+    build-depends:    base >= 4.14 && < 5
+                    , synapse-cc
+                    , hspec >= 2.0
+                    , aeson >= 2.0
+                    , bytestring >= 0.11
+                    , directory >= 1.3
+                    , filepath >= 1.4
+                    , optparse-applicative >= 0.18
+                    , process >= 1.6
+                    , temporary >= 1.3
+                    , text >= 1.2
+    default-extensions: OverloadedStrings
+    ghc-options:      -threaded
diff --git a/test/AuthCheckSpec.hs b/test/AuthCheckSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/AuthCheckSpec.hs
@@ -0,0 +1,154 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+-- | SAFE-6 acceptance tests for SynapseCC.AuthCheck.
+module Main where
+
+import Data.Aeson (Value, eitherDecodeStrict)
+import Data.Aeson.QQ.Simple (aesonQQ)
+import qualified Data.IORef as IORef
+import qualified Data.Text as T
+import Data.Text (Text)
+import System.Exit (exitFailure, exitSuccess)
+import System.FilePath ((</>))
+import qualified System.IO.Temp as Temp
+import qualified Data.Text.IO as TIO
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.Aeson as Aeson
+
+import SynapseCC.AuthCheck (backendRequiresCookieAuth, detectCookieAuthMismatch)
+
+-- ─── Fixtures ─────────────────────────────────────────────────
+
+irWithAuthMethod :: Value
+irWithAuthMethod = [aesonQQ|{
+  "irVersion": "2.0",
+  "irBackend": "test",
+  "irMethods": {
+    "svc.list": {
+      "mdName": "list",
+      "mdFullPath": "svc.list",
+      "mdNamespace": "svc",
+      "mdParams": [
+        { "pdName": "user",
+          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
+          "pdRequired": false,
+          "pdSource": { "from": "auth", "resolver": "self.db.validate_user" }
+        },
+        { "pdName": "search",
+          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
+          "pdRequired": false
+        }
+      ]
+    }
+  }
+}|]
+
+irWithoutAuthMethods :: Value
+irWithoutAuthMethods = [aesonQQ|{
+  "irVersion": "2.0",
+  "irBackend": "test",
+  "irMethods": {
+    "svc.ping": {
+      "mdName": "ping",
+      "mdFullPath": "svc.ping",
+      "mdNamespace": "svc",
+      "mdParams": [
+        { "pdName": "message",
+          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
+          "pdRequired": true
+        }
+      ]
+    }
+  }
+}|]
+
+irWithCookieButNoAuth :: Value
+irWithCookieButNoAuth = [aesonQQ|{
+  "irVersion": "2.0",
+  "irBackend": "test",
+  "irMethods": {
+    "svc.origin_log": {
+      "mdName": "origin_log",
+      "mdFullPath": "svc.origin_log",
+      "mdNamespace": "svc",
+      "mdParams": [
+        { "pdName": "origin",
+          "pdType": {"tag":"RefPrimitive","contents":["string",null]},
+          "pdRequired": false,
+          "pdSource": { "from": "header", "key": "origin" }
+        }
+      ]
+    }
+  }
+}|]
+
+-- ─── Runner ───────────────────────────────────────────────────
+
+data TestResult = Pass String | Fail String String
+
+main :: IO ()
+main = do
+  failures <- IORef.newIORef (0 :: Int)
+  let run (Pass name)        = putStrLn $ "  [PASS] " <> name
+      run (Fail name reason) = do
+        putStrLn $ "  [FAIL] " <> name <> ": " <> reason
+        IORef.modifyIORef' failures (+ 1)
+
+  putStrLn "=== SAFE-6: backendRequiresCookieAuth detection ==="
+
+  run $ boolCase "IR with #[from_auth] method → detected as requiring cookie auth"
+    (backendRequiresCookieAuth irWithAuthMethod) True
+
+  run $ boolCase "IR with only RPC params → not detected"
+    (backendRequiresCookieAuth irWithoutAuthMethods) False
+
+  run $ boolCase "IR with cookie/header params but no auth → not detected"
+    (backendRequiresCookieAuth irWithCookieButNoAuth) False
+
+  putStrLn "=== SAFE-6: detectCookieAuthMismatch end-to-end ==="
+
+  Temp.withSystemTempDirectory "safe6" $ \tmp -> do
+    let irFile   = tmp </> "ir.json"
+        tsFile   = tmp </> "transport.ts"
+    BS.writeFile irFile (BL.toStrict (Aeson.encode irWithAuthMethod))
+
+    -- Case 1: auth required, no prior transport.ts → no warning
+    r1 <- detectCookieAuthMismatch irFile tmp
+    run $ case r1 of
+      Nothing -> Pass "first build (no transport.ts) → no warning"
+      Just _  -> Fail "first build" "expected Nothing; got warning"
+
+    -- Case 2: auth required, transport.ts lacks SAFE-7 marker → warning
+    TIO.writeFile tsFile "// legacy transport.ts, no marker\nexport const ws = null;"
+    r2 <- detectCookieAuthMismatch irFile tmp
+    run $ case r2 of
+      Just w | "SAFE-6" `T.isInfixOf` w && "cookie" `T.isInfixOf` w ->
+        Pass "stale transport.ts + auth required → warning mentions SAFE-6 + cookie"
+      Just _  -> Fail "stale transport.ts" "got a warning but wrong content"
+      Nothing -> Fail "stale transport.ts" "expected warning; got Nothing"
+
+    -- Case 3: auth required, transport.ts contains SAFE-7 marker → no warning
+    TIO.writeFile tsFile "// SAFE-7-cookie-auth-marker: ok\nexport const ws = null;"
+    r3 <- detectCookieAuthMismatch irFile tmp
+    run $ case r3 of
+      Nothing -> Pass "SAFE-7-marked transport.ts → no warning"
+      Just _  -> Fail "marked transport.ts" "expected Nothing; got warning"
+
+    -- Case 4: no auth in backend → no warning regardless of transport.ts
+    BS.writeFile irFile (BL.toStrict (Aeson.encode irWithoutAuthMethods))
+    r4 <- detectCookieAuthMismatch irFile tmp  -- transport.ts still marked from case 3
+    run $ case r4 of
+      Nothing -> Pass "no-auth backend → no warning regardless of transport.ts"
+      Just _  -> Fail "no-auth backend" "expected Nothing; got warning"
+
+  fc <- IORef.readIORef failures
+  if fc == 0
+    then putStrLn "\nSAFE-6: ALL TESTS PASSED" >> exitSuccess
+    else putStrLn ("\nSAFE-6: " <> show fc <> " test(s) failed") >> exitFailure
+
+boolCase :: String -> Bool -> Bool -> TestResult
+boolCase name got want
+  | got == want = Pass name
+  | otherwise   = Fail name ("expected " <> show want <> ", got " <> show got)
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -37,17 +37,23 @@
   )
 import GHC.IO.Encoding (setLocaleEncoding, utf8)
 import Options.Applicative (execParserPure, defaultPrefs, getParseResult)
+import System.Environment (lookupEnv, setEnv, unsetEnv)
+import qualified Synapse.Self as Self
+import qualified SynapseCC.Auth as Auth
 import Plexus.Client (SubstrateConfig(..), defaultConfig)
 import qualified Plexus.Transport as Transport
 import Plexus.Types (TransportError(..), PlexusStreamItem)
 import SynapseCC.CLI (synapseCCParserInfo, synapseCCCommandParserInfo)
-import SynapseCC.Config (loadSynapseConfig, validateSynapseConfig, initSynapseConfig, synapseConfigPath)
+import SynapseCC.Config (loadSynapseConfig, validateSynapseConfig, initSynapseConfig, InitResult(..), synapseConfigPath)
 import SynapseCC.Detect
   ( ProjectHint(..), Detector(..), emptyHint
   , runDetectors, defaultDetectors
   , detectTauri, detectVite, detectNextJS, detectNodeProject
+  , BackendInference(..)
+  , inferBackendFromCrate, parseServiceCrateName
   )
-import SynapseCC.Discover (discoverTools)
+import SynapseCC.Discover (discoverTools, serviceCodegenSendsInjectedParams)
+import qualified SynapseCC.Language as Language
 import SynapseCC.Merge (additiveMerge)
 import SynapseCC.Pipeline (runPipeline)
 import SynapseCC.Types
@@ -56,6 +62,7 @@
   , WatchArgs(..), Command(..)
   , defaultSynapseConfig
   , TransportType(..)
+  , SynapseCCError(..), GeneratedPath(..)
   )
 import SynapseCC.Watch
   ( matchesAnyPrefix, parseUrl, extractPluginHashesFromBytes
@@ -63,6 +70,20 @@
   , fetchBackendHash
   )
 
+-- | Sample scaffold config for tests. The backend name here is arbitrary
+-- test data — Z2H-5 removed the zero-argument default from the init path,
+-- so tests must construct their sample explicitly.
+testSynapseConfig :: SynapseConfig
+testSynapseConfig = defaultSynapseConfig "substrate"
+
+-- | Registry coordinates guaranteed dead for init/inference tests
+-- (discard port — connection refused immediately, never inferable).
+deadRegistryHost :: Text
+deadRegistryHost = "127.0.0.1"
+
+deadRegistryPort :: Int
+deadRegistryPort = 9
+
 -- | Shared test context: the path to generated output
 data TestEnv = TestEnv
   { teOutputDir :: !FilePath
@@ -775,7 +796,7 @@
           when tmpExists $ removeDirectoryRecursive dir'
           createDirectoryIfMissing True dir'
           -- Pass [] — no detectors, so defaults apply regardless of test environment
-          result <- withCurrentDirectory dir' $ initSynapseConfig synapseConfigPath []
+          result <- withCurrentDirectory dir' $ initSynapseConfig synapseConfigPath [] (Just "demo") deadRegistryHost deadRegistryPort
           result `shouldSatisfy` isRight
           exists <- doesFileExist (dir' </> synapseConfigPath)
           exists `shouldBe` True
@@ -785,7 +806,7 @@
           let dir' = tmpBase </> "synapse-cc-init-exists-test"
           createDirectoryIfMissing True dir'
           writeFile (dir' </> synapseConfigPath) "{}"
-          result <- withCurrentDirectory dir' $ initSynapseConfig synapseConfigPath []
+          result <- withCurrentDirectory dir' $ initSynapseConfig synapseConfigPath [] (Just "demo") deadRegistryHost deadRegistryPort
           result `shouldSatisfy` isLeft
 
         it "created file round-trips through loadSynapseConfig" $ do
@@ -794,7 +815,7 @@
           tmpExists <- doesDirectoryExist dir'
           when tmpExists $ removeDirectoryRecursive dir'
           createDirectoryIfMissing True dir'
-          _ <- withCurrentDirectory dir' $ initSynapseConfig synapseConfigPath []
+          _ <- withCurrentDirectory dir' $ initSynapseConfig synapseConfigPath [] (Just "demo") deadRegistryHost deadRegistryPort
           result <- withCurrentDirectory dir' loadSynapseConfig
           result `shouldSatisfy` isRight
 
@@ -831,19 +852,19 @@
                 Just cfg -> Map.size (scTargets (cfg :: SynapseConfig)) `shouldBe` 2
 
       describe "validateSynapseConfig" $ do
-        it "accepts defaultSynapseConfig" $
-          validateSynapseConfig defaultSynapseConfig `shouldBe` Right ()
+        it "accepts testSynapseConfig" $
+          validateSynapseConfig testSynapseConfig `shouldBe` Right ()
 
         it "rejects empty backend" $
-          validateSynapseConfig (defaultSynapseConfig { scBackend = Just "" })
+          validateSynapseConfig (testSynapseConfig { scBackend = Just "" })
             `shouldSatisfy` isLeft
 
         it "rejects whitespace-only backend" $
-          validateSynapseConfig (defaultSynapseConfig { scBackend = Just "   " })
+          validateSynapseConfig (testSynapseConfig { scBackend = Just "   " })
             `shouldSatisfy` isLeft
 
         it "rejects empty targets map" $
-          validateSynapseConfig (defaultSynapseConfig { scTargets = Map.empty })
+          validateSynapseConfig (testSynapseConfig { scTargets = Map.empty })
             `shouldSatisfy` isLeft
 
         it "rejects target with empty generate list" $ do
@@ -857,7 +878,7 @@
                 , tcPlugins         = Nothing
                 , tcSharedTransport = Nothing
                 }
-          let cfg = defaultSynapseConfig { scTargets = Map.singleton "t" badTarget }
+          let cfg = testSynapseConfig { scTargets = Map.singleton "t" badTarget }
           validateSynapseConfig cfg `shouldSatisfy` isLeft
 
         it "rejects target with empty outputDir" $ do
@@ -871,12 +892,12 @@
                 , tcPlugins         = Nothing
                 , tcSharedTransport = Nothing
                 }
-          let cfg = defaultSynapseConfig { scTargets = Map.singleton "t" badTarget }
+          let cfg = testSynapseConfig { scTargets = Map.singleton "t" badTarget }
           validateSynapseConfig cfg `shouldSatisfy` isLeft
 
       describe "JSON round-trip" $ do
         it "encode/decode SynapseConfig preserves data" $ do
-          let cfg = defaultSynapseConfig
+          let cfg = testSynapseConfig
           case Aeson.decode (Aeson.encode cfg) of
             Nothing      -> expectationFailure "Failed to decode encoded SynapseConfig"
             Just decoded -> scBackend (decoded :: SynapseConfig) `shouldBe` scBackend cfg
@@ -982,9 +1003,37 @@
 
         it "produces CmdInit" $ do
           case parse ["init"] of
-            Just CmdInit -> pure ()
-            other -> expectationFailure $ "Expected CmdInit, got: " ++ show other
+            Just (CmdInit Nothing) -> pure ()
+            other -> expectationFailure $ "Expected CmdInit Nothing, got: " ++ show other
 
+        it "parses 'init demo' with explicit backend" $ do
+          case parse ["init", "demo"] of
+            Just (CmdInit (Just b)) -> b `shouldBe` "demo"
+            other -> expectationFailure $ "Expected CmdInit (Just \"demo\"), got: " ++ show other
+
+      -- SELF-6: `_self` subcommand. synapse-cc's job is to parse the
+      -- invocation and hand the resulting SelfCommand to the shared
+      -- runSelfCommand handler. These tests cover parsing only — the
+      -- handler's behavior is verified in plexus-synapse:self-test.
+      describe "_self subcommand" $ do
+        it "parses '_self BACKEND show'" $
+          parse ["_self", "testbk", "show"] `shouldSatisfy` isJust
+
+        it "produces CmdSelf" $
+          case parse ["_self", "testbk", "show"] of
+            Just (CmdSelf _) -> pure ()
+            other -> expectationFailure $ "Expected CmdSelf, got: " ++ show other
+
+        it "parses '_self BACKEND set cookie NAME VALUE'" $
+          case parse ["_self", "testbk", "set", "cookie", "access_token", "literal:abc"] of
+            Just (CmdSelf _) -> pure ()
+            other -> expectationFailure $ "Expected CmdSelf for set, got: " ++ show other
+
+        it "parses '_self BACKEND resolve header NAME'" $
+          case parse ["_self", "testbk", "resolve", "header", "authorization"] of
+            Just (CmdSelf _) -> pure ()
+            other -> expectationFailure $ "Expected CmdSelf for resolve, got: " ++ show other
+
       describe "legacy build parser compatibility" $ do
         it "synapseCCParserInfo still parses old-style args" $
           getParseResult (execParserPure defaultPrefs synapseCCParserInfo
@@ -992,8 +1041,89 @@
             `shouldSatisfy` isJust
 
     -- ═══════════════════════════════════════════
+    -- SELF-6: dedup between SynapseCC.Auth.resolveToken
+    -- and Synapse.Self.resolveToken.
+    --
+    -- Proves the final-fallback path in SynapseCC.Auth delegates to
+    -- the shared Synapse.Self helper. With the CLI-flag + env-var
+    -- slots empty, SynapseCC.Auth.resolveToken must produce the same
+    -- Maybe Text as Synapse.Self.resolveToken for the same HOME.
+    -- ═══════════════════════════════════════════
+    describe "SELF-6 resolveToken dedup" $ do
+      let backend = "self6-dedup-test" :: Text
+          jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.sig" :: Text
+          -- Options with no CLI flags set (final-fallback regime).
+          bareOpts = defaultOptions { optToken = Nothing, optTokenFile = Nothing }
+
+          withIsolatedHome :: (FilePath -> IO a) -> IO a
+          withIsolatedHome action = do
+            -- SYNAPSE_TOKEN must not be set when exercising the fallback.
+            mPrevSynTok <- lookupEnv "SYNAPSE_TOKEN"
+            unsetEnv "SYNAPSE_TOKEN"
+            mPrevHome <- lookupEnv "HOME"
+            tmpRoot <- getTemporaryDirectory
+            let tmpHome = tmpRoot </> ("synapse-cc-self6-" <> T.unpack backend)
+            createDirectoryIfMissing True tmpHome
+            setEnv "HOME" tmpHome
+            r <- action tmpHome
+            -- Restore env; best-effort.
+            case mPrevHome of
+              Just h  -> setEnv "HOME" h
+              Nothing -> unsetEnv "HOME"
+            case mPrevSynTok of
+              Just v  -> setEnv "SYNAPSE_TOKEN" v
+              Nothing -> pure ()
+            removeDirectoryRecursive tmpHome
+            pure r
+
+      it "both return Nothing when no defaults file exists" $
+        withIsolatedHome $ \_home -> do
+          authResult <- Auth.resolveToken bareOpts backend
+          selfResult <- Self.resolveToken backend
+          authResult `shouldBe` Nothing
+          selfResult `shouldBe` Nothing
+
+      it "both return the resolved access_token cookie when defaults carry one" $
+        withIsolatedHome $ \_home -> do
+          -- Write a defaults.json with a literal: cookie.access_token
+          -- through the canonical library surface.
+          let sd0 = Self.emptyStoredDefaults
+              sd  = sd0 { Self.sdCookies =
+                            Map.singleton "access_token"
+                              (Self.CredentialRef ("literal:" <> jwt)) }
+          Self.writeDefaults backend sd
+          authResult <- Auth.resolveToken bareOpts backend
+          selfResult <- Self.resolveToken backend
+          authResult `shouldBe` Just jwt
+          selfResult `shouldBe` Just jwt
+          authResult `shouldBe` selfResult
+
+      it "Auth.resolveToken CLI flag still wins over the shared fallback" $
+        withIsolatedHome $ \_home -> do
+          let sd0 = Self.emptyStoredDefaults
+              sd  = sd0 { Self.sdCookies =
+                            Map.singleton "access_token"
+                              (Self.CredentialRef "literal:from-defaults-file") }
+          Self.writeDefaults backend sd
+          let optsWithFlag = bareOpts { optToken = Just "from-cli-flag" }
+          authResult <- Auth.resolveToken optsWithFlag backend
+          selfResult <- Self.resolveToken backend
+          authResult `shouldBe` Just "from-cli-flag"
+          selfResult `shouldBe` Just "from-defaults-file"
+
+    -- ═══════════════════════════════════════════
     -- Section 12: Watch utilities (pure)
     -- ═══════════════════════════════════════════
+    describe "SYNAPSE-CC-2 service-codegen version" $ do
+      it "every service-codegen before 0.5.0 sends injected params" $ do
+        map serviceCodegenSendsInjectedParams ["0.4.1", "0.4.0", "0.1.0", "0.4.9-rc1"]
+          `shouldBe` [True, True, True, True]
+      it "0.5.0 and later leave them out" $ do
+        map serviceCodegenSendsInjectedParams ["0.5.0", "0.5.1", "0.10.0", "1.0.0"]
+          `shouldBe` [False, False, False, False]
+      it "an unparseable version is not warned about" $ do
+        map serviceCodegenSendsInjectedParams ["unknown", "", "dev"] `shouldBe` [False, False, False]
+
     describe "Watch utilities" $ do
 
       describe "matchesAnyPrefix" $ do
@@ -1082,28 +1212,28 @@
       describe "buildConfigFromTarget" $ do
         it "sets outputDir from target" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
-          let sc  = defaultSynapseConfig { scUrl = Just "ws://localhost:4444" }
+          let sc  = testSynapseConfig { scUrl = Just "ws://localhost:4444" }
           let tc  = TargetConfig ["plugins"] BrowserTransport "my/output/dir" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           optOutput (cfgOptions cfg) `shouldBe` "my/output/dir"
 
         it "sets transport from target" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
-          let sc  = defaultSynapseConfig
+          let sc  = testSynapseConfig
           let tc  = TargetConfig ["plugins"] BrowserTransport "out" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           optTransport (cfgOptions cfg) `shouldBe` BrowserTransport
 
         it "parses host from URL" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
-          let sc  = defaultSynapseConfig { scUrl = Just "ws://myhost:9000" }
+          let sc  = testSynapseConfig { scUrl = Just "ws://myhost:9000" }
           let tc  = TargetConfig ["plugins"] WsTransport "out" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           cfgHost cfg `shouldBe` "myhost"
 
         it "parses port from URL" $ do
           let wa  = WatchArgs "substrate" [] Nothing Nothing defaultOptions
-          let sc  = defaultSynapseConfig { scUrl = Just "ws://localhost:9000" }
+          let sc  = testSynapseConfig { scUrl = Just "ws://localhost:9000" }
           let tc  = TargetConfig ["plugins"] WsTransport "out" Nothing Nothing Nothing Nothing Nothing
           let cfg = buildConfigFromTarget wa sc tc
           cfgPort cfg `shouldBe` "9000"
@@ -1249,7 +1379,7 @@
           withTempProject $ \dir -> do
             createDirectoryIfMissing True (dir </> "src-tauri")
             hint <- withCurrentDirectory dir $
-              initSynapseConfig synapseConfigPath defaultDetectors
+              initSynapseConfig synapseConfigPath defaultDetectors (Just "demo") deadRegistryHost deadRegistryPort
             hint `shouldSatisfy` isRight
             cfgResult <- withCurrentDirectory dir loadSynapseConfig
             case cfgResult of
@@ -1262,7 +1392,7 @@
           withTempProject $ \dir -> do
             writeFile (dir </> "vite.config.ts") "export default {}"
             hint <- withCurrentDirectory dir $
-              initSynapseConfig synapseConfigPath defaultDetectors
+              initSynapseConfig synapseConfigPath defaultDetectors (Just "demo") deadRegistryHost deadRegistryPort
             hint `shouldSatisfy` isRight
             cfgResult <- withCurrentDirectory dir loadSynapseConfig
             case cfgResult of
@@ -1275,7 +1405,7 @@
           withTempProject $ \dir -> do
             writeFile (dir </> "package.json") "{}"
             hint <- withCurrentDirectory dir $
-              initSynapseConfig synapseConfigPath defaultDetectors
+              initSynapseConfig synapseConfigPath defaultDetectors (Just "demo") deadRegistryHost deadRegistryPort
             hint `shouldSatisfy` isRight
             cfgResult <- withCurrentDirectory dir loadSynapseConfig
             case cfgResult of
@@ -1287,13 +1417,13 @@
         it "empty project → generated config has ws transport (default)" $ do
           withTempProject $ \dir -> do
             hint <- withCurrentDirectory dir $
-              initSynapseConfig synapseConfigPath defaultDetectors
+              initSynapseConfig synapseConfigPath defaultDetectors (Just "demo") deadRegistryHost deadRegistryPort
             hint `shouldSatisfy` isRight
             cfgResult <- withCurrentDirectory dir loadSynapseConfig
             case cfgResult of
               Left err -> expectationFailure $ "loadSynapseConfig failed: " <> T.unpack err
               Right sc -> do
-                -- No detector fired → no hint → default transport (WsTransport from defaultSynapseConfig)
+                -- No detector fired → no hint → default transport (WsTransport from testSynapseConfig)
                 let transports = map tcTransport (Map.elems (scTargets sc))
                 transports `shouldSatisfy` (not . null)
 
@@ -1301,10 +1431,192 @@
           withTempProject $ \dir -> do
             createDirectoryIfMissing True (dir </> "src-tauri")
             result <- withCurrentDirectory dir $
-              initSynapseConfig synapseConfigPath defaultDetectors
+              initSynapseConfig synapseConfigPath defaultDetectors (Just "demo") deadRegistryHost deadRegistryPort
             case result of
               Left err -> expectationFailure $ "Expected Right but got Left: " <> T.unpack err
-              Right hint -> phReason hint `shouldSatisfy` isJust
+              Right ir -> phReason (irHint ir) `shouldSatisfy` isJust
+
+    -- ═══════════════════════════════════════════
+    -- Z2H-5: init backend naming / inference / error
+    -- ═══════════════════════════════════════════
+    describe "Z2H-5 init backend resolution" $ do
+
+      let withTempInitDir :: String -> (FilePath -> IO a) -> IO a
+          withTempInitDir name action = do
+            tmpBase <- getTemporaryDirectory
+            let dir' = tmpBase </> name
+            exists <- doesDirectoryExist dir'
+            when exists $ removeDirectoryRecursive dir'
+            createDirectoryIfMissing True dir'
+            action dir'
+
+      it "explicit backend: init demo writes backend \"demo\"" $ do
+        withTempInitDir "synapse-cc-z2h5-explicit" $ \dir' -> do
+          result <- withCurrentDirectory dir' $
+            initSynapseConfig synapseConfigPath [] (Just "demo") deadRegistryHost deadRegistryPort
+          case result of
+            Left err -> expectationFailure (T.unpack err)
+            Right ir -> do
+              irBackend ir `shouldBe` "demo"
+              irBackendNote ir `shouldBe` Nothing  -- explicit, nothing to announce
+          cfgResult <- withCurrentDirectory dir' loadSynapseConfig
+          case cfgResult of
+            Left err -> expectationFailure (T.unpack err)
+            Right sc -> scBackend sc `shouldBe` Just "demo"
+
+      it "inference: co-located service crate names the backend AND announces it" $ do
+        withTempInitDir "synapse-cc-z2h5-crate" $ \dir' -> do
+          writeFile (dir' </> "Cargo.toml") $ unlines
+            [ "[package]"
+            , "name = \"my-cool-service\""
+            , "version = \"0.1.0\""
+            , ""
+            , "[dependencies]"
+            , "plexus-core = { path = \"../plexus-core\" }"
+            ]
+          result <- withCurrentDirectory dir' $
+            initSynapseConfig synapseConfigPath [] Nothing deadRegistryHost deadRegistryPort
+          case result of
+            Left err -> expectationFailure (T.unpack err)
+            Right ir -> do
+              irBackend ir `shouldBe` "my-cool-service"
+              -- Inference MUST be announced (never silent)
+              case irBackendNote ir of
+                Nothing   -> expectationFailure "expected an inference announcement"
+                Just note -> note `shouldSatisfy` T.isInfixOf "my-cool-service"
+          cfgResult <- withCurrentDirectory dir' loadSynapseConfig
+          case cfgResult of
+            Left err -> expectationFailure (T.unpack err)
+            Right sc -> scBackend sc `shouldBe` Just "my-cool-service"
+
+      it "nothing inferable: clear error naming the missing BACKEND argument, no file written" $ do
+        withTempInitDir "synapse-cc-z2h5-nothing" $ \dir' -> do
+          result <- withCurrentDirectory dir' $
+            initSynapseConfig synapseConfigPath [] Nothing deadRegistryHost deadRegistryPort
+          case result of
+            Right ir -> expectationFailure $
+              "expected an error, but init picked backend: " <> T.unpack (irBackend ir)
+            Left err -> do
+              err `shouldSatisfy` T.isInfixOf "BACKEND"
+              err `shouldSatisfy` T.isInfixOf "synapse-cc init"
+          exists <- doesFileExist (dir' </> synapseConfigPath)
+          exists `shouldBe` False
+
+      describe "inferBackendFromCrate" $ do
+        it "finds a service crate Cargo.toml in the parent directory" $ do
+          withTempInitDir "synapse-cc-z2h5-parent" $ \dir' -> do
+            writeFile (dir' </> "Cargo.toml") $ unlines
+              [ "[package]"
+              , "name = \"parent-service\""
+              , "[dependencies]"
+              , "plexus-transport = { path = \"../plexus-transport\" }"
+              ]
+            let sub = dir' </> "nested"
+            createDirectoryIfMissing True sub
+            result <- inferBackendFromCrate sub
+            fmap biBackend result `shouldBe` Just "parent-service"
+
+        it "returns Nothing for a non-service crate" $ do
+          withTempInitDir "synapse-cc-z2h5-nonservice" $ \dir' -> do
+            writeFile (dir' </> "Cargo.toml") $ unlines
+              [ "[package]"
+              , "name = \"just-a-crate\""
+              , "[dependencies]"
+              , "serde = \"1.0\""
+              ]
+            result <- inferBackendFromCrate dir'
+            result `shouldBe` Nothing
+
+      describe "parseServiceCrateName" $ do
+        it "accepts a crate depending on plexus-core" $
+          parseServiceCrateName (T.unlines
+            [ "[package]", "name = \"fidget-spinner\"", "version = \"0.1.0\""
+            , "[dependencies]", "serde = \"1.0\"", "plexus-core = { path = \"../plexus-core\" }"
+            ]) `shouldBe` Just "fidget-spinner"
+
+        it "accepts a crate depending on plexus-rpc" $
+          parseServiceCrateName (T.unlines
+            [ "[package]", "name = \"svc\""
+            , "[dependencies]", "plexus-rpc = \"0.3\""
+            ]) `shouldBe` Just "svc"
+
+        it "accepts table-form dependency [dependencies.plexus-rpc]" $
+          parseServiceCrateName (T.unlines
+            [ "[package]", "name = \"svc-table\""
+            , "[dependencies.plexus-rpc]", "version = \"0.3\""
+            ]) `shouldBe` Just "svc-table"
+
+        it "rejects a crate with no plexus dependency" $
+          parseServiceCrateName (T.unlines
+            [ "[package]", "name = \"plain\""
+            , "[dependencies]", "serde = \"1.0\""
+            ]) `shouldBe` Nothing
+
+        it "rejects plexus mentioned only in a comment" $
+          parseServiceCrateName (T.unlines
+            [ "[package]", "name = \"sneaky\""
+            , "[dependencies]", "# plexus-core would be nice", "serde = \"1.0\""
+            ]) `shouldBe` Nothing
+
+        it "rejects when [package] has no name" $
+          parseServiceCrateName (T.unlines
+            [ "[package]", "version = \"0.1.0\""
+            , "[dependencies]", "plexus-core = \"1.0\""
+            ]) `shouldBe` Nothing
+
+    -- ═══════════════════════════════════════════
+    -- Z2H-7: Rust compile step (Language.buildProject Rust)
+    -- ═══════════════════════════════════════════
+    describe "Z2H-7 Rust compile step" $ do
+
+      let withTempCrate :: String -> [(FilePath, String)] -> (FilePath -> IO a) -> IO a
+          withTempCrate name files action = do
+            tmpBase <- getTemporaryDirectory
+            let dir' = tmpBase </> name
+            exists <- doesDirectoryExist dir'
+            when exists $ removeDirectoryRecursive dir'
+            createDirectoryIfMissing True (dir' </> "src")
+            forM_ files $ \(rel, content) -> writeFile (dir' </> rel) content
+            action dir'
+
+          minimalCargoToml = unlines
+            [ "[package]"
+            , "name = \"z2h7-test-crate\""
+            , "version = \"0.1.0\""
+            , "edition = \"2021\""
+            , ""
+            , "[dependencies]"
+            ]
+
+      it "cargo build of a valid generated-style crate succeeds" $ do
+        cargoAvail <- findExecutable "cargo"
+        case cargoAvail of
+          Nothing -> pendingWith "cargo not found on PATH"
+          Just _ -> withTempCrate "synapse-cc-z2h7-green"
+            [ ("Cargo.toml", minimalCargoToml)
+            , ("src/lib.rs", "pub fn answer() -> i32 { 42 }\n")
+            ] $ \dir' -> do
+              result <- Language.buildProject Rust (GeneratedPath dir') False
+              case result of
+                Left err -> expectationFailure $ T.unpack (formatError err)
+                Right _  -> pure ()
+
+      it "cargo build failure surfaces as LanguageToolError with cargo diagnostics" $ do
+        cargoAvail <- findExecutable "cargo"
+        case cargoAvail of
+          Nothing -> pendingWith "cargo not found on PATH"
+          Just _ -> withTempCrate "synapse-cc-z2h7-red"
+            [ ("Cargo.toml", minimalCargoToml)
+            , ("src/lib.rs", "pub fn broken( -> {\n")
+            ] $ \dir' -> do
+              result <- Language.buildProject Rust (GeneratedPath dir') False
+              case result of
+                Right _ -> expectationFailure "expected cargo build to fail"
+                Left (LanguageToolError tool output _code) -> do
+                  tool `shouldBe` "cargo build"
+                  -- cargo diagnostics must reach the pipeline error verbatim
+                  output `shouldSatisfy` T.isInfixOf "error"
+                Left other -> expectationFailure $ "unexpected error variant: " <> show other
 
     -- ═══════════════════════════════════════════
     -- Section 14: Standalone config-driven build
diff --git a/test/NamingSpec.hs b/test/NamingSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/NamingSpec.hs
@@ -0,0 +1,117 @@
+-- | NAMING-10: a Plexus backend is a service (owner, 2026-10-29), and the code
+-- generator is service-codegen. The gate fails on any remnant of the old word;
+-- the rest holds the old spellings to "read, never written" for one release:
+-- the lock key, the generator's output key, the cache's toolchain key, the
+-- binary's name in discovery and the CLI flag. Hermetic: no backend, no
+-- network; a fixture PATH, HOME and working directory.
+module Main (main) where
+
+import Control.Exception (bracket)
+import qualified Data.Aeson as Aeson
+import qualified Data.ByteString.Lazy.Char8 as LBS8
+import Data.List (isInfixOf)
+import Data.Maybe (isJust)
+import qualified Data.Text as T
+import Options.Applicative (defaultPrefs, execParserPure, getParseResult)
+import System.Directory
+import System.Environment (lookupEnv, setEnv, unsetEnv)
+import System.Exit (ExitCode (..))
+import System.FilePath ((</>))
+import System.IO.Temp (withSystemTempDirectory)
+import System.Process (readProcessWithExitCode)
+import Test.Hspec
+
+import SynapseCC.CLI (deprecatedFlagNotes, synapseCCParserInfo)
+import SynapseCC.Discover (findServiceCodegen, oldCodegenName, serviceCodegenName, toolPathToFilePath)
+import SynapseCC.Lock (SynapseLock (..), readSynapseLock, synapseLockPath, writeSynapseLock)
+import SynapseCC.Types
+
+oldLockKey, oldOutputKey, oldToolchainKey :: String
+oldLockKey = "hubCodegen" -- naming-alias: synapse.lock's key before NAMING-10
+oldOutputKey = "hubCodegenVersion" -- naming-alias: the generator's output key before NAMING-10
+oldToolchainKey = "tvHubCodegen" -- naming-alias: the cache manifest's key before NAMING-10
+
+main :: IO ()
+main = hspec $ do
+  describe "the naming gate" $
+    it "scripts/naming-gate.sh finds no remnant of the old word" $ do
+      (code, out, err) <- readProcessWithExitCode "bash" ["scripts/naming-gate.sh"] ""
+      (code, out <> err) `shouldBe` (ExitSuccess, "")
+
+  describe "synapse.lock" $ do
+    let lockWith key = "{\"version\":\"1\",\"synapseCC\":\"0.3.5\",\"" <> key
+          <> "\":\"0.5.0\",\"updatedAt\":\"2026-10-29T00:00:00Z\",\"targets\":{}}"
+    it "a lock written with the old key reads, and is written back with the new key only" $
+      inTempDir $ do
+        writeFile synapseLockPath (lockWith oldLockKey)
+        Just lock <- readSynapseLock
+        slServiceCodegen lock `shouldBe` "0.5.0"
+        writeSynapseLock lock
+        written <- readFile synapseLockPath
+        written `shouldSatisfy` ("\"serviceCodegen\"" `isInfixOf`)
+        written `shouldNotSatisfy` (oldLockKey `isInfixOf`)
+    it "a lock with the new key reads" $
+      inTempDir $ do
+        writeFile synapseLockPath (lockWith "serviceCodegen")
+        fmap slServiceCodegen <$> readSynapseLock `shouldReturn` Just "0.5.0"
+
+  describe "the generator's JSON" $ do
+    let output key = LBS8.pack $ "{\"files\":{},\"fileHashes\":{},\"warnings\":[],\"" <> key
+          <> "\":\"0.5.1\",\"dependencies\":{},\"devDependencies\":{}}"
+    it "reads serviceCodegenVersion" $
+      fmap coServiceCodegenVersion (Aeson.decode (output "serviceCodegenVersion")) `shouldBe` Just "0.5.1"
+    it "reads the old output key from a generator before 0.5.1" $
+      fmap coServiceCodegenVersion (Aeson.decode (output oldOutputKey)) `shouldBe` Just "0.5.1"
+    it "a cache manifest's toolchain under the old key reads, and is written under the new" $ do
+      let old = LBS8.pack $ "{\"tvSynapseCC\":\"0.3.5\",\"tvSynapse\":\"3.16.0\",\"" <> oldToolchainKey
+            <> "\":\"0.5.0\",\"tvPlexusCore\":null}"
+      Just tv <- pure (Aeson.decode old)
+      tvServiceCodegen tv `shouldBe` Just "0.5.0"
+      let back = LBS8.unpack (Aeson.encode tv)
+      back `shouldSatisfy` ("\"tvServiceCodegen\":\"0.5.0\"" `isInfixOf`)
+      back `shouldNotSatisfy` (oldToolchainKey `isInfixOf`)
+
+  describe "discovery" $ do
+    it "with both names on PATH, finds service-codegen" $
+      withFixturePath [serviceCodegenName, oldCodegenName] $ \bin -> do
+        Just (path, note) <- findServiceCodegen False
+        toolPathToFilePath path `shouldBe` bin </> serviceCodegenName
+        note `shouldBe` Nothing
+    it "with only the old name on PATH, finds it and says the name is deprecated" $
+      withFixturePath [oldCodegenName] $ \bin -> do
+        Just (path, note) <- findServiceCodegen False
+        toolPathToFilePath path `shouldBe` bin </> oldCodegenName
+        note `shouldSatisfy` maybe False (T.isInfixOf "deprecated")
+    it "with neither, finds nothing" $
+      withFixturePath [] $ \_ -> findServiceCodegen False >>= (`shouldSatisfy` not . isJust)
+
+  describe "the CLI" $ do
+    let parse = getParseResult . execParserPure defaultPrefs synapseCCParserInfo
+    it "--service-codegen PATH sets the generator's path" $
+      fmap (optServiceCodegenPath . cfgOptions) (parse ["typescript", "menger", "--service-codegen", "/x/sc"])
+        `shouldBe` Just (Just "/x/sc")
+    it "the old flag still works, and is noted as deprecated" $ do
+      let args = ["typescript", "menger", "--" <> oldCodegenName, "/x/old"]
+      fmap (optServiceCodegenPath . cfgOptions) (parse args) `shouldBe` Just (Just "/x/old")
+      deprecatedFlagNotes args `shouldSatisfy` any (T.isInfixOf "deprecated")
+      deprecatedFlagNotes ["typescript", "menger", "--service-codegen", "/x"] `shouldBe` []
+
+-- | Run in a fresh temporary working directory.
+inTempDir :: IO a -> IO a
+inTempDir act = withSystemTempDirectory "naming10" $ \d -> withCurrentDirectory d act
+
+-- | Run with PATH holding only fake executables of these names (each prints a
+-- version), HOME and the working directory empty, so no fallback path is found.
+withFixturePath :: [String] -> (FilePath -> IO a) -> IO a
+withFixturePath names act = withSystemTempDirectory "naming10" $ \d -> do
+  let bin = d </> "bin"; home = d </> "home"; cwd = d </> "a" </> "b"
+  mapM_ (createDirectoryIfMissing True) [bin, home, cwd]
+  mapM_ (\n -> do
+           let f = bin </> n
+           writeFile f "#!/bin/sh\necho 0.5.1\n"
+           p <- getPermissions f
+           setPermissions f (setOwnerExecutable True p)) names
+  withEnv "PATH" bin $ withEnv "HOME" home $ withCurrentDirectory cwd (act bin)
+
+withEnv :: String -> String -> IO a -> IO a
+withEnv k v act = bracket (lookupEnv k <* setEnv k v) (maybe (unsetEnv k) (setEnv k)) (const act)
