Repository: probe-rs/probe-rs
Pull request #3149 (2025-03-06 20:06:59)

Remove global registry
The main point behind this change is to remove global state from probe-rs, allowing the CLI to use separate registries in parallel sessions without interfering with each other.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index f1dfdacf..29017381 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2974,6 +2974,7 @@ dependencies = [
  "scroll",
  "serde",
  "serde_json",
+ "serde_yaml",
  "sha2",
  "signal-hook",
  "svd-parser",
diff --git a/changelog/added-with-registry.md b/changelog/added-with-registry.md
new file mode 100644
index 00000000..3bac09b0
--- /dev/null
+++ b/changelog/added-with-registry.md
@@ -0,0 +1,1 @@
+Added `Probe::attach_with_registry` and `attach_under_reset_with_registry` to allow working with a custom registry.
\ No newline at end of file
diff --git a/changelog/changed-registry.md b/changelog/changed-registry.md
new file mode 100644
index 00000000..b525ce19
--- /dev/null
+++ b/changelog/changed-registry.md
@@ -0,0 +1,2 @@
+- `Registry` is now a public structure instead of internally global configuration.
+- The `add_target_family, add_target_from_yaml, families, get_family_by_name, get_target_and_family_by_name, get_target_by_name, get_targets_by_family_name search_chips` functions have been removed in favour of `Registry` methods.
\ No newline at end of file
diff --git a/probe-rs-tools/Cargo.toml b/probe-rs-tools/Cargo.toml
index 9d044262..7f0643c0 100644
--- a/probe-rs-tools/Cargo.toml
+++ b/probe-rs-tools/Cargo.toml
@@ -33,6 +33,7 @@ jep106 = "0.2"
 num-traits = "0.2"
 scroll = "0.12"
 serde = { version = "1", features = ["derive"] }
+serde_yaml = "0.9"
 thiserror = { workspace = true }
 tracing = "0.1"
 typed-path = "0.10"
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/benchmark.rs b/probe-rs-tools/src/bin/probe-rs/cmd/benchmark.rs
index 35202ab3..7ac618ff 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/benchmark.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/benchmark.rs
@@ -4,7 +4,7 @@ use std::{
 };
 
 use anyhow::Context;
-use probe_rs::{MemoryInterface, probe::list::Lister};
+use probe_rs::{MemoryInterface, config::Registry, probe::list::Lister};
 
 use crate::util::common_options::LoadedProbeOptions;
 use crate::util::common_options::ProbeOptions;
@@ -83,9 +83,9 @@ struct TestData {
 }
 
 impl Cmd {
-    pub fn run(self, lister: &Lister) -> anyhow::Result<()> {
+    pub fn run(self, registry: &mut Registry, lister: &Lister) -> anyhow::Result<()> {
         let speed = self.common.speed;
-        let common_options = self.common.load()?;
+        let common_options = self.common.load(registry)?;
         let mut max_speed = self.max_speed;
         let mut speeds = vec![];
         // if no max-speed specified, assume the user just wants to use a single speed (as per other cli cmds)
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/mod.rs
index 03cd968a..d4d4077a 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/mod.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/mod.rs
@@ -7,6 +7,7 @@ use anyhow::{Context, Result, anyhow};
 use clap::Parser;
 use colored::Colorize;
 use parking_lot::FairMutex;
+use probe_rs::config::Registry;
 use probe_rs::flashing::{BootInfo, FormatKind};
 use probe_rs::probe::list::Lister;
 use probe_rs::rtt::ScanRegion;
@@ -15,7 +16,6 @@ use std::ffi::OsString;
 use std::time::Instant;
 use std::{fs, thread};
 use std::{
-    fs::File,
     io::Write,
     panic,
     path::{Path, PathBuf},
@@ -138,10 +138,13 @@ fn main_try(args: &[OsString], offset: UtcOffset) -> Result<()> {
 
     let _log_guard = setup_logging(None, config.general.log_level);
 
+    let mut registry = Registry::from_builtin_families();
+
     // Make sure we load the config given in the cli parameters.
     for cdp in &config.general.chip_descriptions {
-        let file = File::open(Path::new(cdp))?;
-        probe_rs::config::add_target_from_yaml(file)
+        let file = std::fs::read_to_string(Path::new(cdp))?;
+        registry
+            .add_target_family_from_yaml(&file)
             .with_context(|| format!("failed to load the chip description from {cdp}"))?;
     }
     let image_instr_set;
@@ -212,7 +215,7 @@ fn main_try(args: &[OsString], offset: UtcOffset) -> Result<()> {
     };
 
     let lister = Lister::new();
-    let (mut session, probe_options) = match probe_options.simple_attach(&lister) {
+    let (mut session, probe_options) = match probe_options.simple_attach(&mut registry, &lister) {
         Ok((session, probe_options)) => (session, probe_options),
 
         Err(OperationError::MultipleProbesFound { list }) => {
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs
index ce59397d..7d130804 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs
@@ -7,14 +7,14 @@ use bytesize::ByteSize;
 
 use probe_rs::{
     Error as ProbeRsError, Target,
-    config::{RegistryError, TargetDescriptionSource},
+    config::{Registry, RegistryError, TargetDescriptionSource},
     flashing::{FileDownloadError, FlashError},
 };
 
 use crate::util::cargo::ArtifactError;
 use crate::util::common_options::OperationError;
 
-pub(crate) fn render_diagnostics(error: OperationError) {
+pub(crate) fn render_diagnostics(registry: &Registry, error: OperationError) {
     let (selected_error, hints) = match &error {
         OperationError::IOError(_e) => (
             error.to_string(),
@@ -76,7 +76,7 @@ pub(crate) fn render_diagnostics(error: OperationError) {
                 "You can select a probe with the `--probe` argument. See `--help` for how to use it.".into()
             ],
         ),
-        OperationError::FlashingFailed { source, target, target_spec, .. } => generate_flash_error_hints(source, target, target_spec),
+        OperationError::FlashingFailed { source, target, target_spec, .. } => generate_flash_error_hints(registry, source, target, target_spec),
         OperationError::ChipDescriptionNotFound{ .. } => (
             error.to_string(),
             vec![],
@@ -236,6 +236,7 @@ pub(crate) fn render_diagnostics(error: OperationError) {
 }
 
 fn generate_flash_error_hints(
+    registry: &Registry,
     error: &FlashError,
     target: &Target,
     target_spec: &Option<String>,
@@ -269,7 +270,7 @@ fn generate_flash_error_hints(
 
             if let Some(target_spec) = target_spec {
                 // Check if the chip specification was unique
-                let matching_chips = probe_rs::config::search_chips(target_spec).unwrap();
+                let matching_chips = registry.search_chips(target_spec);
 
                 tracing::info!(
                     "Searching for all chips for spec '{}', found {}",
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/mod.rs
index 8d5abfc1..994d2c81 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/mod.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/mod.rs
@@ -3,6 +3,7 @@ mod diagnostics;
 use clap::Parser;
 use colored::Colorize;
 use diagnostics::render_diagnostics;
+use probe_rs::config::Registry;
 use probe_rs::probe::list::Lister;
 use std::ffi::OsString;
 use std::{path::PathBuf, process};
@@ -56,7 +57,8 @@ struct CliOptions {
 }
 
 pub fn main(args: &[OsString]) {
-    match main_try(args) {
+    let mut registry = Registry::from_builtin_families();
+    match main_try(&mut registry, args) {
         Ok(_) => (),
         Err(e) => {
             // Ensure stderr is flushed before calling process::exit,
@@ -64,14 +66,14 @@ pub fn main(args: &[OsString]) {
             // to access stderr during shutdown.
             //
             // We ignore the errors, not much we can do anyway.
-            render_diagnostics(e);
+            render_diagnostics(&registry, e);
 
             process::exit(1);
         }
     }
 }
 
-fn main_try(args: &[OsString]) -> Result<(), OperationError> {
+fn main_try(registry: &mut Registry, args: &[OsString]) -> Result<(), OperationError> {
     // Parse the commandline options.
     let opt = CliOptions::parse_from(args);
 
@@ -126,7 +128,7 @@ fn main_try(args: &[OsString]) -> Result<(), OperationError> {
     let lister = Lister::new();
 
     // Attach to specified probe
-    let (mut session, probe_options) = opt.probe_options.simple_attach(&lister)?;
+    let (mut session, probe_options) = opt.probe_options.simple_attach(registry, &lister)?;
 
     // Flash the binary
     let loader =
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/chip.rs b/probe-rs-tools/src/bin/probe-rs/cmd/chip.rs
index aa0d774f..93102180 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/chip.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/chip.rs
@@ -99,7 +99,7 @@ mod test {
         use crate::rpc::functions::RpcApp;
 
         // Create a local server to run commands against.
-        let (mut local_server, tx, rx) = RpcApp::create_server(true, 16);
+        let (mut local_server, tx, rx) = RpcApp::create_server(16);
         let handle = tokio::spawn(async move { local_server.run().await });
 
         // Run the command locally.
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/complete.rs b/probe-rs-tools/src/bin/probe-rs/cmd/complete.rs
index 8e208922..ade6739f 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/complete.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/complete.rs
@@ -7,6 +7,7 @@ use clap_complete::{
     Generator, Shell, generate,
     shells::{Bash, PowerShell, Zsh},
 };
+use probe_rs::config::Registry;
 use probe_rs::probe::list::Lister;
 
 use crate::Cli;
@@ -118,7 +119,7 @@ pub enum CompleteKind {
 /// Output will be one line per chip and print the full name probe-rs expects.
 pub fn list_chips(starts_with: &str) -> Result<String> {
     let mut output = String::new();
-    for family in probe_rs::config::families() {
+    for family in Registry::from_builtin_families().families() {
         for variant in family.variants() {
             if variant.name.starts_with(starts_with) {
                 writeln!(output, "{}", variant.name)?;
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debugger.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debugger.rs
index 1146d869..cc751c5c 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debugger.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debugger.rs
@@ -26,6 +26,7 @@ use crate::{
 use anyhow::{Context, anyhow};
 use probe_rs::{
     Architecture, CoreStatus,
+    config::Registry,
     flashing::{
         DownloadOptions, FileDownloadError, FlashProgress, ProgressEvent, ProgressOperation,
     },
@@ -315,6 +316,7 @@ impl Debugger {
         mut debug_adapter: DebugAdapter<P>,
         lister: &Lister,
     ) -> Result<(), DebuggerError> {
+        let mut registry = Registry::from_builtin_families();
         // The DapClient startup process has a specific sequence.
         // Handle it here before starting a probe-rs session and looping through user generated requests.
         // Handling the initialize, and Attach/Launch requests here in this method,
@@ -336,14 +338,18 @@ impl Debugger {
         };
 
         // Process either the Launch or Attach request.
-        let mut session_data =
-            match self.handle_launch_attach(&launch_attach_request, &mut debug_adapter, lister) {
-                Ok(session_data) => session_data,
-                Err(error) => {
-                    debug_adapter.send_response::<()>(&launch_attach_request, Err(&error))?;
-                    return Ok(());
-                }
-            };
+        let mut session_data = match self.handle_launch_attach(
+            &mut registry,
+            &launch_attach_request,
+            &mut debug_adapter,
+            lister,
+        ) {
+            Ok(session_data) => session_data,
+            Err(error) => {
+                debug_adapter.send_response::<()>(&launch_attach_request, Err(&error))?;
+                return Ok(());
+            }
+        };
         self.debug_logger.flush_to_dap(&mut debug_adapter)?;
 
         if debug_adapter
@@ -403,6 +409,7 @@ impl Debugger {
     #[tracing::instrument(skip_all, name = "Handle Launch/Attach Request")]
     fn handle_launch_attach<P: ProtocolAdapter + 'static>(
         &mut self,
+        registry: &mut Registry,
         launch_attach_request: &Request,
         debug_adapter: &mut DebugAdapter<P>,
         lister: &Lister,
@@ -427,7 +434,8 @@ impl Debugger {
 
         self.config.validate_config_files()?;
 
-        let mut session_data = SessionData::new(lister, &mut self.config, self.timestamp_offset)?;
+        let mut session_data =
+            SessionData::new(registry, lister, &mut self.config, self.timestamp_offset)?;
 
         debug_adapter.halt_after_reset = self.config.flashing_config.halt_after_reset;
 
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/session_data.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/session_data.rs
index 11522a51..7e08767f 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/session_data.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/session_data.rs
@@ -13,7 +13,11 @@ use crate::{
     util::common_options::OperationError,
 };
 use anyhow::{Result, anyhow};
-use probe_rs::{CoreStatus, Session, config::TargetSelector, probe::list::Lister};
+use probe_rs::{
+    CoreStatus, Session,
+    config::{Registry, TargetSelector},
+    probe::list::Lister,
+};
 use probe_rs_debug::{
     DebugRegisters, SourceLocation, debug_info::DebugInfo, exception_handler_for_core,
 };
@@ -64,13 +68,14 @@ pub(crate) struct SessionData {
 
 impl SessionData {
     pub(crate) fn new(
+        registry: &mut Registry,
         lister: &Lister,
         config: &mut configuration::SessionConfig,
         timestamp_offset: UtcOffset,
     ) -> Result<Self, DebuggerError> {
         let target_selector = TargetSelector::from(config.chip.as_deref());
 
-        let options = config.probe_options().load()?;
+        let options = config.probe_options().load(registry)?;
         let target_probe = options.attach_probe(lister)?;
         let target_session = options
             .attach_session(target_probe, target_selector)
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/debug.rs b/probe-rs-tools/src/bin/probe-rs/cmd/debug.rs
index 118b24f5..76dd93fd 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/debug.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/debug.rs
@@ -13,6 +13,7 @@ use probe_rs::CoreDump;
 use probe_rs::CoreDumpError;
 use probe_rs::CoreInterface;
 use probe_rs::architecture::arm::ap::AccessPortError;
+use probe_rs::config::Registry;
 use probe_rs::flashing::FileDownloadError;
 use probe_rs::probe::DebugProbeError;
 use probe_rs::probe::list::Lister;
@@ -38,8 +39,8 @@ pub struct Cmd {
 }
 
 impl Cmd {
-    pub fn run(self, lister: &Lister) -> anyhow::Result<()> {
-        let (mut session, _probe_options) = self.common.simple_attach(lister)?;
+    pub fn run(self, registry: &mut Registry, lister: &Lister) -> anyhow::Result<()> {
+        let (mut session, _probe_options) = self.common.simple_attach(registry, lister)?;
 
         let di = self
             .exe
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/gdb_server/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/gdb_server/mod.rs
index cdaa02c5..42e289a9 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/gdb_server/mod.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/gdb_server/mod.rs
@@ -9,7 +9,7 @@ pub(crate) use stub::{GdbInstanceConfiguration, run};
 use std::time::Duration;
 
 use parking_lot::FairMutex;
-use probe_rs::probe::list::Lister;
+use probe_rs::{config::Registry, probe::list::Lister};
 
 use crate::util::common_options::ProbeOptions;
 
@@ -33,8 +33,8 @@ pub struct Cmd {
 }
 
 impl Cmd {
-    pub fn run(self, lister: &Lister) -> anyhow::Result<()> {
-        let (mut session, _probe_options) = self.common.simple_attach(lister)?;
+    pub fn run(self, registry: &mut Registry, lister: &Lister) -> anyhow::Result<()> {
+        let (mut session, _probe_options) = self.common.simple_attach(registry, lister)?;
 
         if self.reset_halt {
             session
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/itm.rs b/probe-rs-tools/src/bin/probe-rs/cmd/itm.rs
index 386cb4af..8a5759c3 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/itm.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/itm.rs
@@ -3,6 +3,7 @@
 use std::time::{Duration, Instant};
 
 use probe_rs::architecture::arm::{component::TraceSink, swo::SwoConfig};
+use probe_rs::config::Registry;
 use probe_rs::probe::list::Lister;
 
 use crate::CoreOptions;
@@ -68,8 +69,8 @@ pub struct Cmd {
 }
 
 impl Cmd {
-    pub fn run(self, lister: &Lister) -> anyhow::Result<()> {
-        let (mut session, _probe_options) = self.common.simple_attach(lister)?;
+    pub fn run(self, registry: &mut Registry, lister: &Lister) -> anyhow::Result<()> {
+        let (mut session, _probe_options) = self.common.simple_attach(registry, lister)?;
 
         match self.source {
             ItmSource::TraceMemory { coreclk } => {
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/profile.rs b/probe-rs-tools/src/bin/probe-rs/cmd/profile.rs
index 5f0fa252..2f2daf93 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/profile.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/profile.rs
@@ -6,6 +6,7 @@ use std::time::Instant;
 use addr2line::Loader;
 use anyhow::anyhow;
 use itm::TracePacket;
+use probe_rs::config::Registry;
 use probe_rs::{
     architecture::arm::{
         SwoConfig,
@@ -67,12 +68,12 @@ impl std::fmt::Display for ProfileMethod {
 }
 
 impl ProfileCmd {
-    pub fn run(self, lister: &Lister) -> anyhow::Result<()> {
+    pub fn run(self, registry: &mut Registry, lister: &Lister) -> anyhow::Result<()> {
         let (mut session, probe_options) = self
             .run
             .shared_options
             .probe_options
-            .simple_attach(lister)?;
+            .simple_attach(registry, lister)?;
 
         let loader = build_loader(
             &mut session,
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/serve.rs b/probe-rs-tools/src/bin/probe-rs/cmd/serve.rs
index d2eae126..e5d0c19b 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/serve.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/serve.rs
@@ -147,7 +147,7 @@ async fn ws_handler(ws: WebSocketUpgrade, state: State<Arc<ServerState>>) -> imp
 /// Actual websocket statemachine (one will be spawned per connection)
 async fn handle_socket(socket: WebSocket, challenge: String, state: Arc<ServerState>) {
     let (writer, reader) = socket.split();
-    let (mut server, tx, mut rx) = RpcApp::create_server(false, 16);
+    let (mut server, tx, mut rx) = RpcApp::create_server(16);
 
     let mut reader = WebsocketRx::new(reader.map(|message| {
         message.map(|message| match message {
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/trace.rs b/probe-rs-tools/src/bin/probe-rs/cmd/trace.rs
index 29211e18..bdcc77a5 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/trace.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/trace.rs
@@ -4,6 +4,7 @@ use std::time::Duration;
 use std::time::Instant;
 
 use probe_rs::MemoryInterface;
+use probe_rs::config::Registry;
 use probe_rs::probe::list::Lister;
 use scroll::{LE, Pwrite};
 
@@ -24,13 +25,13 @@ pub struct Cmd {
 }
 
 impl Cmd {
-    pub fn run(self, lister: &Lister) -> anyhow::Result<()> {
+    pub fn run(self, registry: &mut Registry, lister: &Lister) -> anyhow::Result<()> {
         let mut xs = vec![];
         let mut ys = vec![];
 
         let start = Instant::now();
 
-        let (mut session, _probe_options) = self.common.simple_attach(lister)?;
+        let (mut session, _probe_options) = self.common.simple_attach(registry, lister)?;
 
         let mut core = session.core(self.shared.core)?;
 
diff --git a/probe-rs-tools/src/bin/probe-rs/main.rs b/probe-rs-tools/src/bin/probe-rs/main.rs
index 59eac1e3..575e4f60 100644
--- a/probe-rs-tools/src/bin/probe-rs/main.rs
+++ b/probe-rs-tools/src/bin/probe-rs/main.rs
@@ -117,19 +117,19 @@ impl Cli {
             Subcommand::Serve(cmd) => cmd.run(_config.server).await,
             Subcommand::List(cmd) => cmd.run(client).await,
             Subcommand::Info(cmd) => cmd.run(client).await,
-            Subcommand::Gdb(cmd) => cmd.run(&lister),
+            Subcommand::Gdb(cmd) => cmd.run(&mut *client.registry().await, &lister),
             Subcommand::Reset(cmd) => cmd.run(client).await,
-            Subcommand::Debug(cmd) => cmd.run(&lister),
+            Subcommand::Debug(cmd) => cmd.run(&mut *client.registry().await, &lister),
             Subcommand::Download(cmd) => cmd.run(client).await,
             Subcommand::Run(cmd) => cmd.run(client, utc_offset).await,
             Subcommand::Attach(cmd) => cmd.run(client, utc_offset).await,
             Subcommand::Verify(cmd) => cmd.run(client).await,
             Subcommand::Erase(cmd) => cmd.run(client).await,
-            Subcommand::Trace(cmd) => cmd.run(&lister),
-            Subcommand::Itm(cmd) => cmd.run(&lister),
+            Subcommand::Trace(cmd) => cmd.run(&mut *client.registry().await, &lister),
+            Subcommand::Itm(cmd) => cmd.run(&mut *client.registry().await, &lister),
             Subcommand::Chip(cmd) => cmd.run(client).await,
-            Subcommand::Benchmark(cmd) => cmd.run(&lister),
-            Subcommand::Profile(cmd) => cmd.run(&lister),
+            Subcommand::Benchmark(cmd) => cmd.run(&mut *client.registry().await, &lister),
+            Subcommand::Profile(cmd) => cmd.run(&mut *client.registry().await, &lister),
             Subcommand::Read(cmd) => cmd.run(client).await,
             Subcommand::Write(cmd) => cmd.run(client).await,
             Subcommand::Complete(cmd) => cmd.run(&lister),
@@ -491,7 +491,7 @@ async fn main() -> Result<()> {
     }
 
     // Create a local server to run commands against.
-    let (mut local_server, tx, rx) = RpcApp::create_server(true, 16);
+    let (mut local_server, tx, rx) = RpcApp::create_server(16);
     let handle = tokio::spawn(async move { local_server.run().await });
 
     // Run the command locally.
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/client.rs b/probe-rs-tools/src/bin/probe-rs/rpc/client.rs
index ee4e5b05..5714f371 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/client.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/client.rs
@@ -13,9 +13,9 @@ use postcard_rpc::{
     host_client::{HostClient, HostClientConfig, HostErr, IoClosed, Subscription},
 };
 use postcard_schema::Schema;
-use probe_rs::{Session, flashing::FlashLoader};
+use probe_rs::{Session, config::Registry, flashing::FlashLoader};
 use serde::{Serialize, de::DeserializeOwned};
-use tokio::sync::Mutex;
+use tokio::sync::{Mutex, MutexGuard};
 
 use std::{
     collections::HashMap,
@@ -208,6 +208,7 @@ mod tls {
 pub struct RpcClient {
     client: HostClient<String>,
     uploaded_files: Arc<Mutex<HashMap<PathBuf, PathBuf>>>,
+    registry: Arc<Mutex<Registry>>,
     is_localhost: bool,
 }
 
@@ -238,6 +239,7 @@ impl RpcClient {
                 },
             ),
             uploaded_files: Arc::new(Mutex::new(HashMap::new())),
+            registry: Arc::new(Mutex::new(Registry::from_builtin_families())),
             is_localhost: false,
         }
     }
@@ -372,17 +374,9 @@ impl RpcClient {
             .await
     }
 
-    pub async fn load_chip_family(
-        &self,
-        families: probe_rs_target::ChipFamily,
-    ) -> anyhow::Result<()> {
-        // I refuse to add a schema to ChipFamily until we can actually load it on the server.
-        let family = postcard::to_stdvec(&families).context("Failed to serialize chip family")?;
-
-        self.send_resp::<LoadChipFamilyEndpoint, _>(&LoadChipFamilyRequest {
-            family_data: family,
-        })
-        .await
+    pub async fn load_chip_family(&self, families_yaml: String) -> anyhow::Result<()> {
+        self.send_resp::<LoadChipFamilyEndpoint, _>(&LoadChipFamilyRequest { families_yaml })
+            .await
     }
 
     pub async fn list_chip_families(&self) -> anyhow::Result<Vec<ChipFamily>> {
@@ -393,6 +387,10 @@ impl RpcClient {
         self.send_resp::<ChipInfoEndpoint, _>(&ChipInfoRequest { name: name.into() })
             .await
     }
+
+    pub(crate) async fn registry(&self) -> MutexGuard<'_, Registry> {
+        self.registry.lock().await
+    }
 }
 
 #[derive(Clone)]
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions.rs
index e3d9b177..96ea2472 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions.rs
@@ -45,6 +45,7 @@ use postcard_rpc::server::{
 };
 use postcard_rpc::{Topic, TopicDirection, endpoints, host_client, server, topics};
 use postcard_schema::Schema;
+use probe_rs::config::Registry;
 use probe_rs::{Session, probe::list::Lister};
 use serde::{Deserialize, Serialize};
 use tokio::sync::mpsc::{Receiver, Sender, channel};
@@ -212,7 +213,6 @@ pub struct RpcContext {
     state: SessionState,
     token: CancellationToken,
     sender: Option<PostcardSender<WireTxImpl>>,
-    local: bool,
 }
 
 impl SpawnContext for RpcContext {
@@ -229,19 +229,14 @@ impl SpawnContext for RpcContext {
 }
 
 impl RpcContext {
-    pub fn new(local: bool) -> Self {
+    pub fn new() -> Self {
         Self {
             state: SessionState::new(),
             token: CancellationToken::new(),
             sender: None,
-            local,
         }
     }
 
-    pub fn is_local(&self) -> bool {
-        self.local
-    }
-
     pub fn set_sender(&mut self, sender: PostcardSender<WireTxImpl>) {
         self.sender = Some(sender);
     }
@@ -280,6 +275,10 @@ impl RpcContext {
         Lister::new()
     }
 
+    pub async fn registry(&self) -> impl DerefMut<Target = Registry> + Send {
+        self.state.registry.clone().lock_owned().await
+    }
+
     pub async fn run_blocking<T, F, REQ, RESP>(&mut self, request: REQ, task: F) -> RESP
     where
         T: Topic,
@@ -429,8 +428,8 @@ postcard_rpc::define_dispatch! {
         | CreateTempFileEndpoint    | async     | create_temp_file  |
         | TempFileDataEndpoint      | async     | append_temp_file  |
 
-        | ListChipFamiliesEndpoint  | blocking  | list_families     |
-        | ChipInfoEndpoint          | blocking  | chip_info         |
+        | ListChipFamiliesEndpoint  | async     | list_families     |
+        | ChipInfoEndpoint          | async     | chip_info         |
         | LoadChipFamilyEndpoint    | async     | load_chip_family  |
 
         | TargetInfoEndpoint        | async     | target_info       |
@@ -466,14 +465,14 @@ type TxChannel = Sender<Result<Vec<u8>, WireRxErrorKind>>;
 type RxChannel = Receiver<Vec<u8>>;
 
 impl RpcApp {
-    pub fn create_server(local: bool, depth: usize) -> (ServerImpl, TxChannel, RxChannel) {
+    pub fn create_server(depth: usize) -> (ServerImpl, TxChannel, RxChannel) {
         let client_to_server = channel::<Result<Vec<u8>, WireRxErrorKind>>(depth);
         let server_to_client = channel::<Vec<u8>>(depth);
 
         let client_to_server_rx = WireRx::new(client_to_server.1);
         let server_to_client_tx = WireTx::new(server_to_client.0);
 
-        let mut dispatcher = RpcApp::new(RpcContext::new(local), TokioSpawner);
+        let mut dispatcher = RpcApp::new(RpcContext::new(), TokioSpawner);
         let vkk = dispatcher.min_key_len();
         dispatcher
             .context
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions/chip.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions/chip.rs
index 4734eb6a..a4b19e21 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions/chip.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions/chip.rs
@@ -1,12 +1,11 @@
 use std::ops::Range;
 
-use anyhow::Context;
 use postcard_rpc::header::VarHeader;
 use postcard_schema::Schema;
 use probe_rs::Target;
 use serde::{Deserialize, Serialize};
 
-use crate::rpc::functions::{NoResponse, RpcContext, RpcError, RpcResult};
+use crate::rpc::functions::{NoResponse, RpcContext, RpcResult};
 
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, Schema)]
 pub struct JEP106Code {
@@ -47,12 +46,12 @@ pub struct ChipFamily {
     pub variants: Vec<Chip>,
 }
 
-impl From<probe_rs_target::ChipFamily> for ChipFamily {
-    fn from(value: probe_rs_target::ChipFamily) -> Self {
+impl From<&probe_rs_target::ChipFamily> for ChipFamily {
+    fn from(value: &probe_rs_target::ChipFamily) -> Self {
         Self {
-            name: value.name,
+            name: value.name.clone(),
             manufacturer: value.manufacturer.map(|m| m.into()),
-            variants: value.variants.into_iter().map(|v| v.into()).collect(),
+            variants: value.variants.iter().map(|v| v.into()).collect(),
         }
     }
 }
@@ -70,17 +69,26 @@ pub struct Chip {
     pub name: String,
 }
 
-impl From<probe_rs_target::Chip> for Chip {
-    fn from(value: probe_rs_target::Chip) -> Self {
-        Self { name: value.name }
+impl From<&probe_rs_target::Chip> for Chip {
+    fn from(value: &probe_rs_target::Chip) -> Self {
+        Self {
+            name: value.name.clone(),
+        }
     }
 }
 
 pub type ListFamiliesResponse = RpcResult<Vec<ChipFamily>>;
 
-pub fn list_families(_ctx: &mut RpcContext, _header: VarHeader, _req: ()) -> ListFamiliesResponse {
-    Ok(probe_rs::config::families()
-        .into_iter()
+pub async fn list_families(
+    ctx: &mut RpcContext,
+    _header: VarHeader,
+    _req: (),
+) -> ListFamiliesResponse {
+    Ok(ctx
+        .registry()
+        .await
+        .families()
+        .iter()
         .map(|f| f.into())
         .collect())
 }
@@ -310,18 +318,24 @@ impl Default for MemoryAccess {
 
 pub type ChipInfoResponse = RpcResult<ChipData>;
 
-pub fn chip_info(
-    _ctx: &mut RpcContext,
+pub async fn chip_info(
+    ctx: &mut RpcContext,
     _header: VarHeader,
     request: ChipInfoRequest,
 ) -> ChipInfoResponse {
-    Ok(probe_rs::config::get_target_by_name(request.name)?.into())
+    Ok(ctx
+        .registry()
+        .await
+        .get_target_by_name(request.name)?
+        .into())
 }
 
 // Used to avoid uploading a temp file to the remote.
 #[derive(Serialize, Deserialize, Schema)]
 pub struct LoadChipFamilyRequest {
-    pub family_data: Vec<u8>,
+    /// Chip description in YAML format.
+    // TODO: instead, serialize the whole ChipFamily struct
+    pub families_yaml: String,
 }
 
 pub async fn load_chip_family(
@@ -329,17 +343,9 @@ pub async fn load_chip_family(
     _header: VarHeader,
     request: LoadChipFamilyRequest,
 ) -> NoResponse {
-    if !ctx.is_local() {
-        return RpcResult::Err(RpcError::from(
-            "Loading chip families is not supported in the remote interface yet.",
-        ));
-    }
-
-    let family = postcard::from_bytes::<probe_rs_target::ChipFamily>(&request.family_data)
-        .context("Failed to deserialize chip family data")?;
-
-    // TODO: this can only be done safely if we have separate registries per connection.
-    probe_rs::config::add_target_family(family)?;
+    ctx.registry()
+        .await
+        .add_target_family_from_yaml(&request.families_yaml)?;
 
     Ok(())
 }
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions/info.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions/info.rs
index 7f82f5cc..096d2ea2 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions/info.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions/info.rs
@@ -70,7 +70,8 @@ pub async fn target_info(
     _hdr: VarHeader,
     request: TargetInfoRequest,
 ) -> NoResponse {
-    let probe_options = ProbeOptions::from(&request).load()?;
+    let mut registry = ctx.registry().await;
+    let probe_options = ProbeOptions::from(&request).load(&mut registry)?;
 
     let probe = probe_options.attach_probe(&ctx.lister())?;
 
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions/probe.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions/probe.rs
index 7461f7fc..7b00e209 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions/probe.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions/probe.rs
@@ -247,7 +247,8 @@ pub async fn attach(
     _header: VarHeader,
     request: AttachRequest,
 ) -> RpcResult<AttachResult> {
-    let common_options = ProbeOptions::from(&request).load()?;
+    let mut registry = ctx.registry().await;
+    let common_options = ProbeOptions::from(&request).load(&mut registry)?;
     let target = common_options.get_target_selector()?;
 
     let Ok(probe) = common_options.attach_probe(&ctx.lister()) else {
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/mod.rs b/probe-rs-tools/src/bin/probe-rs/rpc/mod.rs
index 266e283d..053d537c 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/mod.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/mod.rs
@@ -13,7 +13,7 @@ use postcard_schema::{
     Schema,
     schema::{DataModelType, NamedType, NamedValue},
 };
-use probe_rs::Session;
+use probe_rs::{Session, config::Registry};
 use serde::{Deserialize, Serialize};
 use tokio::sync::Mutex;
 
@@ -120,6 +120,7 @@ impl ObjectStorage {
 pub struct SessionState {
     dry_run: bool,
     object_storage: Arc<Mutex<ObjectStorage>>,
+    registry: Arc<Mutex<Registry>>,
 }
 
 #[allow(unused)]
@@ -128,6 +129,7 @@ impl SessionState {
         Self {
             dry_run: false,
             object_storage: Arc::new(Mutex::new(ObjectStorage::new())),
+            registry: Arc::new(Mutex::new(Registry::from_builtin_families())),
         }
     }
 
diff --git a/probe-rs-tools/src/bin/probe-rs/util/cli.rs b/probe-rs-tools/src/bin/probe-rs/util/cli.rs
index dc4bc311..af7cabe0 100644
--- a/probe-rs-tools/src/bin/probe-rs/util/cli.rs
+++ b/probe-rs-tools/src/bin/probe-rs/util/cli.rs
@@ -44,13 +44,20 @@ pub async fn attach_probe(
 ) -> anyhow::Result<SessionInterface> {
     // Load the chip description if provided.
     if let Some(chip_description) = probe_options.chip_description_path.take() {
-        let file = std::fs::File::open(chip_description)?;
+        let file = tokio::fs::read_to_string(&chip_description)
+            .await
+            .with_context(|| {
+                format!(
+                    "Failed to read chip description from {}",
+                    chip_description.display()
+                )
+            })?;
 
         // Load the YAML locally to validate it before sending it to the remote.
-        let family_name = probe_rs::config::add_target_from_yaml(file)?;
-        let family = probe_rs::config::get_family_by_name(&family_name)?;
+        // We may also need it locally.
+        client.registry().await.add_target_family_from_yaml(&file)?;
 
-        client.load_chip_family(family).await?;
+        client.load_chip_family(file).await?;
     }
 
     let probe = select_probe(client, probe_options.probe.map(Into::into)).await?;
diff --git a/probe-rs-tools/src/bin/probe-rs/util/common_options.rs b/probe-rs-tools/src/bin/probe-rs/util/common_options.rs
index 83920971..4f6a4e96 100644
--- a/probe-rs-tools/src/bin/probe-rs/util/common_options.rs
+++ b/probe-rs-tools/src/bin/probe-rs/util/common_options.rs
@@ -1,14 +1,10 @@
-use std::{
-    fs::File,
-    io::Write,
-    path::{Path, PathBuf},
-};
+use std::{io::Write, path::PathBuf};
 
 use super::cargo::ArtifactError;
 use crate::util::parse_u64;
 use probe_rs::{
     Permissions, Session, Target,
-    config::{RegistryError, TargetSelector},
+    config::{Registry, RegistryError, TargetSelector},
     flashing::{FileDownloadError, FlashError},
     integration::FakeProbe,
     probe::{
@@ -124,17 +120,18 @@ pub struct ProbeOptions {
 }
 
 impl ProbeOptions {
-    pub fn load(self) -> Result<LoadedProbeOptions, OperationError> {
-        LoadedProbeOptions::new(self)
+    pub fn load(self, registry: &mut Registry) -> Result<LoadedProbeOptions, OperationError> {
+        LoadedProbeOptions::new(self, registry)
     }
 
     /// Convenience method that attaches to the specified probe, target,
     /// and target session.
-    pub fn simple_attach(
+    pub fn simple_attach<'r>(
         self,
+        registry: &'r mut Registry,
         lister: &Lister,
-    ) -> Result<(Session, LoadedProbeOptions), OperationError> {
-        let common_options = self.load()?;
+    ) -> Result<(Session, LoadedProbeOptions<'r>), OperationError> {
+        let common_options = self.load(registry)?;
 
         let target = common_options.get_target_selector()?;
         let probe = common_options.attach_probe(lister)?;
@@ -145,14 +142,16 @@ impl ProbeOptions {
 }
 
 /// Common options and logic when interfacing with a [Probe] which already did all pre operation preparation.
-#[derive(Debug, Clone)]
-pub struct LoadedProbeOptions(ProbeOptions);
+pub struct LoadedProbeOptions<'r>(ProbeOptions, &'r mut Registry);
 
-impl LoadedProbeOptions {
+impl<'r> LoadedProbeOptions<'r> {
     /// Performs necessary init calls such as loading all chip descriptions
     /// and returns a newtype that ensures initialization.
-    pub(crate) fn new(probe_options: ProbeOptions) -> Result<Self, OperationError> {
-        let options = Self(probe_options);
+    pub(crate) fn new(
+        probe_options: ProbeOptions,
+        registry: &'r mut Registry,
+    ) -> Result<Self, OperationError> {
+        let mut options = Self(probe_options, registry);
         // Load the target description, if given in the cli parameters.
         options.maybe_load_chip_desc()?;
         Ok(options)
@@ -162,15 +161,16 @@ impl LoadedProbeOptions {
     /// to probe-rs registry.
     ///
     /// Note: should be called before any functions in [ProbeOptions].
-    fn maybe_load_chip_desc(&self) -> Result<(), OperationError> {
+    fn maybe_load_chip_desc(&mut self) -> Result<(), OperationError> {
         if let Some(ref cdp) = self.0.chip_description_path {
-            let file = File::open(Path::new(cdp)).map_err(|error| {
+            let yaml = std::fs::read_to_string(cdp).map_err(|error| {
                 OperationError::ChipDescriptionNotFound {
                     source: error,
                     path: cdp.clone(),
                 }
             })?;
-            probe_rs::config::add_target_from_yaml(file).map_err(|error| {
+
+            self.1.add_target_family_from_yaml(&yaml).map_err(|error| {
                 OperationError::FailedChipDescriptionParsing {
                     source: error,
                     path: cdp.clone(),
@@ -184,7 +184,7 @@ impl LoadedProbeOptions {
     /// Resolves a resultant target selector from passed [ProbeOptions].
     pub fn get_target_selector(&self) -> Result<TargetSelector, OperationError> {
         let target = if let Some(chip_name) = &self.0.chip {
-            let target = probe_rs::config::get_target_by_name(chip_name).map_err(|error| {
+            let target = self.1.get_target_by_name(chip_name).map_err(|error| {
                 OperationError::ChipNotFound {
                     source: error,
                     name: chip_name.clone(),
@@ -327,7 +327,7 @@ impl LoadedProbeOptions {
     }
 }
 
-impl AsRef<ProbeOptions> for LoadedProbeOptions {
+impl AsRef<ProbeOptions> for LoadedProbeOptions<'_> {
     fn as_ref(&self) -> &ProbeOptions {
         &self.0
     }
diff --git a/probe-rs/src/config/mod.rs b/probe-rs/src/config/mod.rs
index 52a49bcc..6713e6bc 100644
--- a/probe-rs/src/config/mod.rs
+++ b/probe-rs/src/config/mod.rs
@@ -6,6 +6,7 @@
 //! which can be used together with a flash algorithm to program the flash memory
 //! of a target.
 //!
+//!
 //! ## Built-in targets
 //!
 //! The built-in targets are added at build-time, from the `build.rs` script.
@@ -16,9 +17,10 @@
 //!
 //! ## Adding targets at runtime
 //!
-//! To add a target at runtime, the [add_target_from_yaml] function can
-//! be used to read targets from a YAML file.
-//!
+//! Targets are collected in a [`Registry`] in families. A registry with the built-in
+//! targets can be created via the [`Registry::from_builtin_families`] function.
+//! To add a target at runtime, use [Registry::add_target_family]. The target family
+//! is a [`ChipFamily`] struct, usually read from a target description YAML file.
 
 mod chip_info;
 pub(crate) mod registry;
@@ -30,13 +32,9 @@ pub use probe_rs_target::{
     SectorDescription, SectorInfo, TargetDescriptionSource,
 };
 
-pub use registry::{
-    RegistryError, add_target_family, add_target_from_yaml, families, get_family_by_name,
-    get_target_and_family_by_name, get_target_by_name, get_targets_by_family_name, search_chips,
-};
+pub use registry::{Registry, RegistryError};
 pub use target::{DebugSequence, Target, TargetSelector};
 
 // Crate-internal API
 pub(crate) use chip_info::ChipInfo;
-pub(crate) use registry::get_target_by_chip_info;
 pub(crate) use target::CoreExt;
diff --git a/probe-rs/src/config/registry.rs b/probe-rs/src/config/registry.rs
index df137d1a..44e306f4 100644
--- a/probe-rs/src/config/registry.rs
+++ b/probe-rs/src/config/registry.rs
@@ -2,16 +2,9 @@
 
 use super::{Chip, ChipFamily, ChipInfo, Core, Target, TargetDescriptionSource};
 use crate::config::CoreType;
-use parking_lot::{RwLock, RwLockReadGuard};
 use probe_rs_target::{CoreAccessOptions, RiscvCoreAccessOptions};
 use std::cmp::Ordering;
 use std::collections::HashMap;
-use std::io::Read;
-use std::ops::Deref;
-use std::sync::LazyLock;
-
-static REGISTRY: LazyLock<RwLock<Registry>> =
-    LazyLock::new(|| RwLock::new(Registry::from_builtin_families()));
 
 /// Error type for all errors which occur when working
 /// with the internal registry of targets.
@@ -121,7 +114,8 @@ fn add_generic_targets(vec: &mut Vec<ChipFamily>) {
 }
 
 /// Registry of all available targets.
-struct Registry {
+#[derive(Default)]
+pub struct Registry {
     /// All the available chips.
     families: Vec<ChipFamily>,
 }
@@ -141,7 +135,13 @@ fn builtin_targets() -> Vec<ChipFamily> {
 }
 
 impl Registry {
-    fn from_builtin_families() -> Self {
+    /// Create a new registry.
+    pub fn new() -> Self {
+        Self { families: vec![] }
+    }
+
+    /// Add a target from the built-in targets.
+    pub fn from_builtin_families() -> Self {
         let mut families = builtin_targets();
 
         add_generic_targets(&mut families);
@@ -153,7 +153,13 @@ impl Registry {
         Self { families }
     }
 
-    fn get_target_by_name(&self, name: impl AsRef<str>) -> Result<Target, RegistryError> {
+    /// Returns the list of chip families.
+    pub fn families(&self) -> &[ChipFamily] {
+        &self.families
+    }
+
+    /// Returns a particular target by its name.
+    pub fn get_target_by_name(&self, name: impl AsRef<str>) -> Result<Target, RegistryError> {
         let (target, _) = self.get_target_and_family_by_name(name.as_ref())?;
         Ok(target)
     }
@@ -250,7 +256,8 @@ impl Registry {
         Ok((targ, family.clone()))
     }
 
-    fn get_targets_by_family_name(&self, name: &str) -> Result<Vec<String>, RegistryError> {
+    /// Get all target names in a given family.
+    pub fn get_targets_by_family_name(&self, name: &str) -> Result<Vec<String>, RegistryError> {
         let mut found_family = None;
         let mut exact_matches = 0;
         for family in self.families.iter() {
@@ -274,7 +281,11 @@ impl Registry {
         Ok(family.variants.iter().map(|v| v.name.to_string()).collect())
     }
 
-    fn search_chips(&self, name: &str) -> Vec<String> {
+    /// Search for a chip.
+    ///
+    /// This function returns chips that have the given name as a prefix, with any lowercase `x`
+    /// characters in the prefix matching any character in the chip name.
+    pub fn search_chips(&self, name: &str) -> Vec<String> {
         tracing::debug!("Searching registry for chip with name {name}");
 
         let mut targets = Vec::new();
@@ -294,7 +305,10 @@ impl Registry {
         targets
     }
 
-    fn get_target_by_chip_info(&self, chip_info: ChipInfo) -> Result<Target, RegistryError> {
+    pub(crate) fn get_target_by_chip_info(
+        &self,
+        chip_info: ChipInfo,
+    ) -> Result<Target, RegistryError> {
         let (family, chip) = match chip_info {
             ChipInfo::Arm(chip_info) => {
                 // Try get the corresponding chip.
@@ -339,7 +353,8 @@ impl Registry {
         Target::new(family, chip)
     }
 
-    fn add_target_family(&mut self, family: ChipFamily) -> Result<String, RegistryError> {
+    /// Add a target family to the registry.
+    pub fn add_target_family(&mut self, family: ChipFamily) -> Result<String, RegistryError> {
         validate_family(&family).map_err(|error| {
             RegistryError::InvalidChipFamilyDefinition(Box::new(family.clone()), error)
         })?;
@@ -353,105 +368,12 @@ impl Registry {
 
         Ok(family_name)
     }
-}
-
-/// Get a target from the internal registry based on its name.
-pub fn get_family_by_name(name: impl AsRef<str>) -> Result<ChipFamily, RegistryError> {
-    let registry = REGISTRY.read_recursive();
-
-    registry
-        .families
-        .iter()
-        .find(|f| f.name.eq_ignore_ascii_case(name.as_ref()))
-        .cloned()
-        .ok_or_else(|| RegistryError::ChipNotFound(name.as_ref().to_string()))
-}
-
-/// Get a target from the internal registry based on its name.
-pub fn get_target_by_name(name: impl AsRef<str>) -> Result<Target, RegistryError> {
-    REGISTRY.read_recursive().get_target_by_name(name)
-}
-
-/// Get a target & chip family from the internal registry based on its name.
-pub fn get_target_and_family_by_name(
-    name: impl AsRef<str>,
-) -> Result<(Target, ChipFamily), RegistryError> {
-    REGISTRY
-        .read_recursive()
-        .get_target_and_family_by_name(name.as_ref())
-}
-
-/// Get all target from the internal registry based on its family name.
-pub fn get_targets_by_family_name(
-    family_name: impl AsRef<str>,
-) -> Result<Vec<String>, RegistryError> {
-    REGISTRY
-        .read_recursive()
-        .get_targets_by_family_name(family_name.as_ref())
-}
-
-/// Returns targets from the internal registry that match the given name.
-pub fn search_chips(name: impl AsRef<str>) -> Result<Vec<String>, RegistryError> {
-    Ok(REGISTRY.read_recursive().search_chips(name.as_ref()))
-}
 
-/// Try to retrieve a target based on [ChipInfo] read from a target.
-pub(crate) fn get_target_by_chip_info(chip_info: ChipInfo) -> Result<Target, RegistryError> {
-    REGISTRY.read_recursive().get_target_by_chip_info(chip_info)
-}
-
-/// Parse a target description and add the contained targets
-/// to the internal target registry.
-///
-/// # Examples
-///
-/// ## Add targets from a YAML file
-///
-/// ```no_run
-/// use std::path::Path;
-/// use std::fs::File;
-///
-/// let file = File::open(Path::new("/path/target.yaml"))?;
-/// probe_rs::config::add_target_from_yaml(file)?;
-///
-/// # Ok::<(), anyhow::Error>(())
-/// ```
-///
-/// ## Add targets from a embedded YAML file
-///
-/// ```ignore
-/// const BUILTIN_TARGET_YAML: &[u8] = include_bytes!("/path/target.yaml");
-/// probe_rs::config::add_target_from_yaml(BUILTIN_TARGET_YAML)?;
-/// ```
-pub fn add_target_from_yaml<R>(yaml_reader: R) -> Result<String, RegistryError>
-where
-    R: Read,
-{
-    let family: ChipFamily = serde_yaml::from_reader(yaml_reader)?;
-
-    add_target_family(family)
-}
-
-/// Add a target family to the internal registry.
-pub fn add_target_family(family: ChipFamily) -> Result<String, RegistryError> {
-    REGISTRY.write().add_target_family(family)
-}
-
-/// Get a list of all families which are contained in the internal
-/// registry.
-///
-/// As opposed to `families()` this function does not clone the families, but using it is
-/// slightly more cumbersome.
-pub fn families_ref() -> impl Deref<Target = [ChipFamily]> {
-    RwLockReadGuard::map(REGISTRY.read_recursive(), |registry| {
-        registry.families.as_slice()
-    })
-}
-
-/// Get a list of all families which are contained in the internal
-/// registry.
-pub fn families() -> Vec<ChipFamily> {
-    families_ref().to_vec()
+    /// Add a target family to the registry from a YAML-formatted string.
+    pub fn add_target_family_from_yaml(&mut self, yaml: &str) -> Result<String, RegistryError> {
+        let family: ChipFamily = serde_yaml::from_str(yaml)?;
+        self.add_target_family(family)
+    }
 }
 
 /// See if `name` matches the start of `pattern`, treating any lower-case `x`
@@ -486,7 +408,6 @@ mod tests {
     use crate::flashing::FlashAlgorithm;
 
     use super::*;
-    use std::fs::File;
     type TestResult = Result<(), RegistryError>;
 
     // Need to synchronize this with probe-rs/tests/scan_chain_test.yaml
@@ -583,11 +504,13 @@ mod tests {
 
     #[test]
     fn add_targets_with_and_without_scanchain() -> TestResult {
-        let file = File::open("tests/scan_chain_test.yaml")?;
-        add_target_from_yaml(file)?;
+        let mut registry = Registry::new();
+
+        let file = std::fs::read_to_string("tests/scan_chain_test.yaml")?;
+        registry.add_target_family_from_yaml(&file)?;
 
         // Check that the scan chain can read from a target correctly
-        let mut target = get_target_by_name("FULL_SCAN_CHAIN").unwrap();
+        let mut target = registry.get_target_by_name("FULL_SCAN_CHAIN").unwrap();
         let scan_chain = target.jtag.unwrap().scan_chain.unwrap();
         for device in scan_chain {
             if device.name == Some("core0".to_string()) {
@@ -598,15 +521,15 @@ mod tests {
         }
 
         // Now check that a device without a scan chain is read correctly
-        target = get_target_by_name("NO_JTAG_INFO").unwrap();
+        target = registry.get_target_by_name("NO_JTAG_INFO").unwrap();
         assert_eq!(target.jtag, None);
 
         // Now check that a device without a scan chain is read correctly
-        target = get_target_by_name("NO_SCAN_CHAIN").unwrap();
+        target = registry.get_target_by_name("NO_SCAN_CHAIN").unwrap();
         assert_eq!(target.jtag.unwrap().scan_chain, None);
 
         // Check a device with a minimal scan chain
-        target = get_target_by_name("PARTIAL_SCAN_CHAIN").unwrap();
+        target = registry.get_target_by_name("PARTIAL_SCAN_CHAIN").unwrap();
         let scan_chain = target.jtag.unwrap().scan_chain.unwrap();
         assert_eq!(scan_chain[0].ir_len, Some(FIRST_IR_LENGTH));
         assert_eq!(scan_chain[1].ir_len, Some(SECOND_IR_LENGTH));
diff --git a/probe-rs/src/probe.rs b/probe-rs/src/probe.rs
index cbd5b2e4..c0898836 100644
--- a/probe-rs/src/probe.rs
+++ b/probe-rs/src/probe.rs
@@ -24,6 +24,7 @@ use crate::architecture::xtensa::communication_interface::{
     XtensaCommunicationInterface, XtensaDebugInterfaceState, XtensaError,
 };
 use crate::config::TargetSelector;
+use crate::config::registry::Registry;
 use crate::probe::common::IdCode;
 use crate::{Error, Permissions, Session};
 use common::ScanChainError;
@@ -334,13 +335,36 @@ impl Probe {
     ///
     /// This runs all the necessary protocol init routines.
     ///
-    /// If this doesn't work, you might want to try [`Probe::attach_under_reset`]
+    /// The target is loaded from the builtin list of targets.
+    /// If this doesn't work, you might want to try [`Probe::attach_under_reset`].
     pub fn attach(
         self,
         target: impl Into<TargetSelector>,
         permissions: Permissions,
     ) -> Result<Session, Error> {
-        Session::new(self, target.into(), AttachMethod::Normal, permissions)
+        let registry = Registry::from_builtin_families();
+        self.attach_with_registry(target, permissions, &registry)
+    }
+
+    /// Attach to the chip.
+    ///
+    /// This runs all the necessary protocol init routines.
+    ///
+    /// The target is loaded from a custom registry.
+    /// If this doesn't work, you might want to try [`Probe::attach_under_reset`].
+    pub fn attach_with_registry(
+        self,
+        target: impl Into<TargetSelector>,
+        permissions: Permissions,
+        registry: &Registry,
+    ) -> Result<Session, Error> {
+        Session::new(
+            self,
+            target.into(),
+            AttachMethod::Normal,
+            permissions,
+            registry,
+        )
     }
 
     /// Attach to a target without knowing what target you have at hand.
@@ -372,27 +396,51 @@ impl Probe {
     /// This asserts the reset pin via the probe, plays the protocol init routines and deasserts the pin.
     /// This is necessary if the chip is not responding to the SWD reset sequence.
     /// For example this can happen if the chip has the SWDIO pin remapped.
+    ///
+    /// The target is loaded from the builtin list of targets.
     pub fn attach_under_reset(
         self,
         target: impl Into<TargetSelector>,
         permissions: Permissions,
+    ) -> Result<Session, Error> {
+        let registry = Registry::from_builtin_families();
+        self.attach_under_reset_with_registry(target, permissions, &registry)
+    }
+
+    /// Attach to the chip under hard-reset.
+    ///
+    /// This asserts the reset pin via the probe, plays the protocol init routines and deasserts the pin.
+    /// This is necessary if the chip is not responding to the SWD reset sequence.
+    /// For example this can happen if the chip has the SWDIO pin remapped.
+    ///
+    /// The target is loaded from a custom registry.
+    pub fn attach_under_reset_with_registry(
+        self,
+        target: impl Into<TargetSelector>,
+        permissions: Permissions,
+        registry: &Registry,
     ) -> Result<Session, Error> {
         // The session will de-assert reset after connecting to the debug interface.
-        Session::new(self, target.into(), AttachMethod::UnderReset, permissions).map_err(
-            |e| match e {
-                Error::Arm(ArmError::Timeout)
-                | Error::Riscv(RiscvError::Timeout)
-                | Error::Xtensa(XtensaError::Timeout) => Error::Other(
-                    "Timeout while attaching to target under reset. \
+        Session::new(
+            self,
+            target.into(),
+            AttachMethod::UnderReset,
+            permissions,
+            registry,
+        )
+        .map_err(|e| match e {
+            Error::Arm(ArmError::Timeout)
+            | Error::Riscv(RiscvError::Timeout)
+            | Error::Xtensa(XtensaError::Timeout) => Error::Other(
+                "Timeout while attaching to target under reset. \
                     This can happen if the target is not responding to the reset sequence. \
                     Ensure the chip's reset pin is connected, or try attaching without reset \
                     (`connectUnderReset = false` for DAP Clients, or remove `connect-under-reset` \
                         option from CLI options.)."
-                        .to_string(),
-                ),
-                e => e,
-            },
-        )
+                    .to_string(),
+            ),
+            e => e,
+        })
     }
 
     /// Selects the transport protocol to be used by the debug probe.
diff --git a/probe-rs/src/session.rs b/probe-rs/src/session.rs
index dbe65cdb..e3431211 100644
--- a/probe-rs/src/session.rs
+++ b/probe-rs/src/session.rs
@@ -16,7 +16,7 @@ use crate::{
             XtensaCommunicationInterface, XtensaDebugInterfaceState, XtensaError,
         },
     },
-    config::{CoreExt, DebugSequence, RegistryError, Target, TargetSelector},
+    config::{CoreExt, DebugSequence, RegistryError, Target, TargetSelector, registry::Registry},
     core::{Architecture, CombinedCoreState},
     probe::{
         AttachMethod, DebugProbeError, Probe, ProbeCreationError, fake_probe::FakeProbe,
@@ -137,8 +137,9 @@ impl Session {
         target: TargetSelector,
         attach_method: AttachMethod,
         permissions: Permissions,
+        registry: &Registry,
     ) -> Result<Self, Error> {
-        let (probe, target) = get_target_from_selector(target, attach_method, probe)?;
+        let (probe, target) = get_target_from_selector(target, attach_method, probe, registry)?;
 
         let cores = target
             .cores
@@ -858,9 +859,10 @@ fn get_target_from_selector(
     target: TargetSelector,
     attach_method: AttachMethod,
     mut probe: Probe,
+    registry: &Registry,
 ) -> Result<(Probe, Target), Error> {
     let target = match target {
-        TargetSelector::Unspecified(name) => crate::config::get_target_by_name(name)?,
+        TargetSelector::Unspecified(name) => registry.get_target_by_name(name)?,
         TargetSelector::Specified(target) => target,
         TargetSelector::Auto => {
             // At this point we do not know what the target is, so we cannot use the chip specific reset sequence.
@@ -871,7 +873,8 @@ fn get_target_from_selector(
             }
             probe.attach_to_unspecified()?;
 
-            let (returned_probe, found_target) = crate::vendor::auto_determine_target(probe)?;
+            let (returned_probe, found_target) =
+                crate::vendor::auto_determine_target(registry, probe)?;
             probe = returned_probe;
 
             if AttachMethod::UnderReset == attach_method {
diff --git a/probe-rs/src/vendor/espressif/mod.rs b/probe-rs/src/vendor/espressif/mod.rs
index e11eef71..a80c2878 100644
--- a/probe-rs/src/vendor/espressif/mod.rs
+++ b/probe-rs/src/vendor/espressif/mod.rs
@@ -11,7 +11,7 @@ use crate::{
         riscv::communication_interface::RiscvCommunicationInterface,
         xtensa::communication_interface::XtensaCommunicationInterface,
     },
-    config::{DebugSequence, registry},
+    config::{DebugSequence, Registry},
     vendor::{
         Vendor,
         espressif::sequences::{
@@ -41,9 +41,12 @@ fn get_target_by_magic(info: &EspressifDetection, read_magic: u32) -> Option<Str
     None
 }
 
-fn try_detect_espressif_chip(probe: &mut impl MemoryInterface, idcode: u32) -> Option<String> {
-    let families = registry::families_ref();
-    for family in families.iter() {
+fn try_detect_espressif_chip(
+    registry: &Registry,
+    probe: &mut impl MemoryInterface,
+    idcode: u32,
+) -> Option<String> {
+    for family in registry.families() {
         for info in family
             .chip_detection
             .iter()
@@ -94,19 +97,22 @@ impl Vendor for Espressif {
 
     fn try_detect_riscv_chip(
         &self,
+        registry: &Registry,
         probe: &mut RiscvCommunicationInterface,
         idcode: u32,
     ) -> Result<Option<String>, Error> {
-        let result = probe.halted_access(|probe| Ok(try_detect_espressif_chip(probe, idcode)))?;
+        let result =
+            probe.halted_access(|probe| Ok(try_detect_espressif_chip(registry, probe, idcode)))?;
 
         Ok(result)
     }
 
     fn try_detect_xtensa_chip(
         &self,
+        registry: &Registry,
         probe: &mut XtensaCommunicationInterface,
         idcode: u32,
     ) -> Result<Option<String>, Error> {
-        Ok(try_detect_espressif_chip(probe, idcode))
+        Ok(try_detect_espressif_chip(registry, probe, idcode))
     }
 }
diff --git a/probe-rs/src/vendor/infineon/mod.rs b/probe-rs/src/vendor/infineon/mod.rs
index 5dd6c62e..fb3d7e8d 100644
--- a/probe-rs/src/vendor/infineon/mod.rs
+++ b/probe-rs/src/vendor/infineon/mod.rs
@@ -9,7 +9,7 @@ use crate::{
         dp::{DpRegister, TARGETID},
         memory::ArmMemoryInterface,
     },
-    config::{DebugSequence, registry},
+    config::{DebugSequence, Registry},
     error::Error,
     vendor::{Vendor, infineon::sequences::xmc4000::XMC4000},
 };
@@ -36,18 +36,20 @@ impl Vendor for Infineon {
 
     fn try_detect_arm_chip(
         &self,
+        registry: &Registry,
         interface: &mut dyn ArmProbeInterface,
         chip_info: ArmChipInfo,
     ) -> Result<Option<String>, Error> {
         match chip_info.manufacturer {
-            INFINEON => try_detect_xmc4xxx(interface, &chip_info),
-            CYPRESS => try_detect_psoc(interface, &chip_info),
+            INFINEON => try_detect_xmc4xxx(registry, interface, &chip_info),
+            CYPRESS => try_detect_psoc(registry, interface, &chip_info),
             _ => Ok(None),
         }
     }
 }
 
 fn try_detect_xmc4xxx(
+    registry: &Registry,
     interface: &mut dyn ArmProbeInterface,
     chip_info: &ArmChipInfo,
 ) -> Result<Option<String>, Error> {
@@ -74,8 +76,7 @@ fn try_detect_xmc4xxx(
     // Now look up a closest match. We are not able to tell exactly which device this is, because
     // the identical die is packaged up differently for different devices.
 
-    let families = registry::families_ref();
-    for family in families.iter() {
+    for family in registry.families() {
         for info in family
             .chip_detection
             .iter()
@@ -147,6 +148,7 @@ fn probe_xmc4xxx_flash_size(start_addr: u32, memory: &mut dyn ArmMemoryInterface
 }
 
 fn try_detect_psoc(
+    registry: &Registry,
     interface: &mut dyn ArmProbeInterface,
     _chip_info: &ArmChipInfo,
 ) -> Result<Option<String>, Error> {
@@ -155,7 +157,8 @@ fn try_detect_psoc(
     );
     let siid = tid.tpartno();
 
-    Ok(registry::families()
+    Ok(registry
+        .families()
         .iter()
         .flat_map(|f| f.chip_detection.iter())
         .flat_map(ChipDetectionMethod::as_infineon_psoc_siid)
diff --git a/probe-rs/src/vendor/microchip/mod.rs b/probe-rs/src/vendor/microchip/mod.rs
index 33cccbc2..57b8d553 100644
--- a/probe-rs/src/vendor/microchip/mod.rs
+++ b/probe-rs/src/vendor/microchip/mod.rs
@@ -5,7 +5,7 @@ use probe_rs_target::{Chip, chip_detection::ChipDetectionMethod};
 use crate::{
     Error,
     architecture::arm::{ArmChipInfo, ArmProbeInterface, FullyQualifiedApAddress},
-    config::{DebugSequence, registry},
+    config::{DebugSequence, Registry},
     vendor::{
         Vendor,
         microchip::sequences::atsam::{AtSAM, DsuDid},
@@ -36,6 +36,7 @@ impl Vendor for Microchip {
 
     fn try_detect_arm_chip(
         &self,
+        registry: &Registry,
         interface: &mut dyn ArmProbeInterface,
         chip_info: ArmChipInfo,
     ) -> Result<Option<String>, Error> {
@@ -52,8 +53,7 @@ impl Vendor for Microchip {
                 .read_word_32(DsuDid::ADDRESS)?,
         );
 
-        let families = registry::families_ref();
-        for family in families.iter() {
+        for family in registry.families() {
             for info in family
                 .chip_detection
                 .iter()
diff --git a/probe-rs/src/vendor/mod.rs b/probe-rs/src/vendor/mod.rs
index 94a9a507..0a5b68c0 100644
--- a/probe-rs/src/vendor/mod.rs
+++ b/probe-rs/src/vendor/mod.rs
@@ -17,7 +17,7 @@ use crate::{
             XtensaCommunicationInterface, XtensaDebugInterfaceState,
         },
     },
-    config::{ChipInfo, DebugSequence, registry},
+    config::{ChipInfo, DebugSequence, Registry},
     probe::{DebugProbeError, Probe},
 };
 
@@ -39,6 +39,7 @@ pub trait Vendor: Send + Sync + std::fmt::Display {
     /// Tries to identify an ARM chip. Returns `Some(target name)` on success.
     fn try_detect_arm_chip(
         &self,
+        _registry: &Registry,
         _probe: &mut dyn ArmProbeInterface,
         _chip_info: ArmChipInfo,
     ) -> Result<Option<String>, Error> {
@@ -48,6 +49,7 @@ pub trait Vendor: Send + Sync + std::fmt::Display {
     /// Tries to identify an RISC-V chip. Returns `Some(target name)` on success.
     fn try_detect_riscv_chip(
         &self,
+        _registry: &Registry,
         _probe: &mut RiscvCommunicationInterface,
         _idcode: u32,
     ) -> Result<Option<String>, Error> {
@@ -57,6 +59,7 @@ pub trait Vendor: Send + Sync + std::fmt::Display {
     /// Tries to identify an Xtensa chip. Returns `Some(target name)` on success.
     fn try_detect_xtensa_chip(
         &self,
+        _registry: &Registry,
         _probe: &mut XtensaCommunicationInterface,
         _idcode: u32,
     ) -> Result<Option<String>, Error> {
@@ -103,7 +106,10 @@ pub fn try_create_debug_sequence(chip: &Chip) -> Option<DebugSequence> {
     None
 }
 
-fn try_detect_arm_chip(mut probe: Probe) -> Result<(Probe, Option<Target>), Error> {
+fn try_detect_arm_chip(
+    registry: &Registry,
+    mut probe: Probe,
+) -> Result<(Probe, Option<Target>), Error> {
     let mut found_target = None;
 
     if !probe.has_arm_interface() {
@@ -143,18 +149,17 @@ fn try_detect_arm_chip(mut probe: Probe) -> Result<(Probe, Option<Target>), Erro
                     for vendor in vendors.iter() {
                         // TODO: only consider families with matching JEP106.
                         if let Some(target_name) =
-                            vendor.try_detect_arm_chip(interface.as_mut(), found_chip)?
+                            vendor.try_detect_arm_chip(registry, interface.as_mut(), found_chip)?
                         {
-                            found_target = Some(registry::get_target_by_name(&target_name)?);
+                            found_target = Some(registry.get_target_by_name(&target_name)?);
                             break;
                         }
                     }
 
                     // No vendor-specific match, try to find a target by chip info.
                     if found_target.is_none() {
-                        found_target = Some(crate::config::get_target_by_chip_info(
-                            ChipInfo::from(found_chip),
-                        )?);
+                        found_target =
+                            Some(registry.get_target_by_chip_info(ChipInfo::from(found_chip))?);
                     }
                 }
 
@@ -170,7 +175,7 @@ fn try_detect_arm_chip(mut probe: Probe) -> Result<(Probe, Option<Target>), Erro
     Ok((probe, found_target))
 }
 
-fn try_detect_riscv_chip(probe: &mut Probe) -> Result<Option<Target>, Error> {
+fn try_detect_riscv_chip(registry: &Registry, probe: &mut Probe) -> Result<Option<Target>, Error> {
     let mut found_target = None;
 
     probe.select_jtag_tap(0)?;
@@ -192,9 +197,9 @@ fn try_detect_riscv_chip(probe: &mut Probe) -> Result<Option<Target>, Error> {
                     for vendor in vendors.iter() {
                         // TODO: only consider families with matching JEP106.
                         if let Some(target_name) =
-                            vendor.try_detect_riscv_chip(&mut interface, idcode)?
+                            vendor.try_detect_riscv_chip(registry, &mut interface, idcode)?
                         {
-                            found_target = Some(registry::get_target_by_name(target_name)?);
+                            found_target = Some(registry.get_target_by_name(target_name)?);
                             break;
                         }
                     }
@@ -218,7 +223,7 @@ fn try_detect_riscv_chip(probe: &mut Probe) -> Result<Option<Target>, Error> {
     Ok(found_target)
 }
 
-fn try_detect_xtensa_chip(probe: &mut Probe) -> Result<Option<Target>, Error> {
+fn try_detect_xtensa_chip(registry: &Registry, probe: &mut Probe) -> Result<Option<Target>, Error> {
     let mut found_target = None;
 
     probe.select_jtag_tap(0)?;
@@ -238,9 +243,9 @@ fn try_detect_xtensa_chip(probe: &mut Probe) -> Result<Option<Target>, Error> {
                     for vendor in vendors.iter() {
                         // TODO: only consider families with matching JEP106.
                         if let Some(target_name) =
-                            vendor.try_detect_xtensa_chip(&mut interface, idcode)?
+                            vendor.try_detect_xtensa_chip(registry, &mut interface, idcode)?
                         {
-                            found_target = Some(registry::get_target_by_name(target_name)?);
+                            found_target = Some(registry.get_target_by_name(target_name)?);
                             break;
                         }
                     }
@@ -264,22 +269,31 @@ fn try_detect_xtensa_chip(probe: &mut Probe) -> Result<Option<Target>, Error> {
 }
 
 /// Tries to identify the chip using the given probe.
-pub(crate) fn auto_determine_target(mut probe: Probe) -> Result<(Probe, Option<Target>), Error> {
+pub(crate) fn auto_determine_target(
+    registry: &Registry,
+    mut probe: Probe,
+) -> Result<(Probe, Option<Target>), Error> {
     tracing::info!("Auto-detecting target");
     let mut found_target = None;
 
     // Xtensa and RISC-V interfaces don't need moving the probe. For clarity, their
     // handlers work with the borrowed probe, and we use these wrappers to adapt to the
     // ARM way of moving in and out of the probe.
-    fn try_detect_riscv_chip_wrapper(mut probe: Probe) -> Result<(Probe, Option<Target>), Error> {
-        try_detect_riscv_chip(&mut probe).map(|found_target| (probe, found_target))
+    fn try_detect_riscv_chip_wrapper(
+        registry: &Registry,
+        mut probe: Probe,
+    ) -> Result<(Probe, Option<Target>), Error> {
+        try_detect_riscv_chip(registry, &mut probe).map(|found_target| (probe, found_target))
     }
 
-    fn try_detect_xtensa_chip_wrapper(mut probe: Probe) -> Result<(Probe, Option<Target>), Error> {
-        try_detect_xtensa_chip(&mut probe).map(|found_target| (probe, found_target))
+    fn try_detect_xtensa_chip_wrapper(
+        registry: &Registry,
+        mut probe: Probe,
+    ) -> Result<(Probe, Option<Target>), Error> {
+        try_detect_xtensa_chip(registry, &mut probe).map(|found_target| (probe, found_target))
     }
 
-    type DetectFn = fn(Probe) -> Result<(Probe, Option<Target>), Error>;
+    type DetectFn = fn(&Registry, Probe) -> Result<(Probe, Option<Target>), Error>;
     const ARCHITECTURES: &[DetectFn] = &[
         try_detect_arm_chip,
         try_detect_riscv_chip_wrapper,
@@ -287,7 +301,7 @@ pub(crate) fn auto_determine_target(mut probe: Probe) -> Result<(Probe, Option<T
     ];
 
     for architecture in ARCHITECTURES {
-        let (returned_probe, target) = architecture(probe)?;
+        let (returned_probe, target) = architecture(registry, probe)?;
 
         probe = returned_probe;
         if let Some(target) = target {
diff --git a/probe-rs/src/vendor/nordicsemi/mod.rs b/probe-rs/src/vendor/nordicsemi/mod.rs
index 63d9517d..e079287b 100644
--- a/probe-rs/src/vendor/nordicsemi/mod.rs
+++ b/probe-rs/src/vendor/nordicsemi/mod.rs
@@ -13,7 +13,7 @@ use crate::{
     architecture::arm::{
         ArmChipInfo, ArmProbeInterface, FullyQualifiedApAddress, memory::ArmMemoryInterface,
     },
-    config::{DebugSequence, registry},
+    config::{DebugSequence, Registry},
     vendor::{
         Vendor,
         nordicsemi::sequences::{nrf52::Nrf52, nrf53::Nrf5340, nrf91::Nrf9160},
@@ -45,6 +45,7 @@ impl Vendor for NordicSemi {
 
     fn try_detect_arm_chip(
         &self,
+        registry: &Registry,
         probe: &mut dyn ArmProbeInterface,
         chip_info: ArmChipInfo,
     ) -> Result<Option<String>, Error> {
@@ -59,8 +60,7 @@ impl Vendor for NordicSemi {
         // Cache to avoid reading the same register multiple times
         let mut register_values: HashMap<u32, u32> = HashMap::new();
 
-        let families = registry::families_ref();
-        for family in families.iter() {
+        for family in registry.families() {
             for info in family.chip_detection.iter() {
                 let target = if let Some(spec) = info.as_nordic_ficr() {
                     ficr_info_detect(&mut register_values, memory_interface.as_mut(), spec)
diff --git a/target-gen/src/generate.rs b/target-gen/src/generate.rs
index 035ea260..01af46ca 100644
--- a/target-gen/src/generate.rs
+++ b/target-gen/src/generate.rs
@@ -3,6 +3,7 @@ use cmsis_pack::pdsc::{AccessPort, Algorithm, Core, Device, Package, Processor};
 use cmsis_pack::{pack_index::PdscRef, utils::FromElem};
 use futures::StreamExt;
 use jep106::JEP106Code;
+use probe_rs::config::Registry;
 use probe_rs::flashing::FlashAlgorithm;
 use probe_rs_target::{
     Architecture, ArmCoreAccessOptions, Chip, ChipFamily, Core as ProbeCore, CoreAccessOptions,
@@ -79,7 +80,8 @@ where
     devices.sort_by(|a, b| a.0.cmp(&b.0));
 
     // Only process this, if this belongs to a supported family.
-    let currently_supported_chip_families = probe_rs::config::families();
+    let registry = Registry::from_builtin_families();
+    let currently_supported_chip_families = registry.families();
 
     for (device_name, device) in devices {
         if only_supported_familes


Test patch:
diff --git a/smoke-tester/src/dut_definition.rs b/smoke-tester/src/dut_definition.rs
index cad44dc4..950592e0 100644
--- a/smoke-tester/src/dut_definition.rs
+++ b/smoke-tester/src/dut_definition.rs
@@ -6,9 +6,9 @@
 use miette::IntoDiagnostic;
 use miette::Result;
 use miette::WrapErr;
+use probe_rs::config::Registry;
 use probe_rs::{
     Target,
-    config::get_target_by_name,
     probe::{DebugProbeSelector, Probe, list::Lister},
 };
 use serde::Deserialize;
@@ -213,7 +213,8 @@ impl DutDefinition {
 }
 
 fn lookup_unique_target(chip: &str) -> Result<Target> {
-    let target = get_target_by_name(chip).into_diagnostic()?;
+    let registry = Registry::from_builtin_families();
+    let target = registry.get_target_by_name(chip).into_diagnostic()?;
 
     if !target.name.eq_ignore_ascii_case(chip) {
         miette::bail!(
diff --git a/target-gen/src/commands/test.rs b/target-gen/src/commands/test.rs
index 66877d9e..f5169c88 100644
--- a/target-gen/src/commands/test.rs
+++ b/target-gen/src/commands/test.rs
@@ -1,5 +1,4 @@
 use std::cell::RefCell;
-use std::fs::File;
 use std::path::Path;
 use std::rc::Rc;
 use std::time::Instant;
@@ -8,6 +7,7 @@ use anyhow::{Context, Result, anyhow};
 use colored::Colorize;
 use probe_rs::{
     MemoryInterface, Permissions, Session,
+    config::Registry,
     flashing::{
         DownloadOptions, FlashLoader, FlashProgress, ProgressEvent, ProgressOperation, erase_all,
         erase_sectors,
@@ -58,11 +58,14 @@ pub fn cmd_test(
         println!("{error}");
     }
 
+    let mut registry = Registry::new();
+
     // Add the target to the registry from the generated YAML file
-    let file = File::open(Path::new(definition_export_path))?;
-    let family_name = probe_rs::config::add_target_from_yaml(file)?;
+    let yaml = std::fs::read_to_string(definition_export_path)?;
+    let family_name = registry.add_target_family_from_yaml(&yaml)?;
 
-    let targets = probe_rs::config::get_targets_by_family_name(&family_name)
+    let targets = registry
+        .get_targets_by_family_name(&family_name)
         .with_context(|| format!("Failed to get targets of {family_name}"))?;
 
     let target_name = match targets.len() {