diff --git a/Cargo.lock b/Cargo.lock
index 2a6f10dcc19..cf96770e9b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2883,6 +2883,7 @@ dependencies = [
  "noirc_frontend",
  "semver",
  "serde",
+ "tempfile",
  "test-case",
  "thiserror",
  "toml 0.7.8",
diff --git a/docs/docs/noir/modules_packages_crates/workspaces.md b/docs/docs/noir/modules_packages_crates/workspaces.md
index 513497f12bf..2fbf10aec52 100644
--- a/docs/docs/noir/modules_packages_crates/workspaces.md
+++ b/docs/docs/noir/modules_packages_crates/workspaces.md
@@ -33,10 +33,14 @@ members = ["crates/a", "crates/b"]
 default-member = "crates/a"
 ```
 
-`members` indicates which packages are included in the workspace. As such, all member packages of a workspace will be processed when the `--workspace` flag is used with various commands or if a `default-member` is not specified.
+`members` indicates which packages are included in the workspace. As such, all member packages of a workspace will be processed when the `--workspace` flag is used with various commands or if a `default-member` is not specified. The `--package` option can be used to limit
+the scope of some commands to a specific member of the workspace; otherwise these commands run on the package nearest on the path to the
+current directory where `nargo` was invoked.
 
 `default-member` indicates which package various commands process by default.
 
 Libraries can be defined in a workspace. Inside a workspace, these are consumed as `{ path = "../to_lib" }` dependencies in Nargo.toml.
 
 Inside a workspace, these are consumed as `{ path = "../to_lib" }` dependencies in Nargo.toml.
+
+Please note that nesting regular packages is not supported: certain commands work on the workspace level and will use the topmost Nargo.toml file they can find on the path; unless this is a workspace configuration with `members`, the command might run on some unintended package.
diff --git a/tooling/nargo_cli/src/cli/check_cmd.rs b/tooling/nargo_cli/src/cli/check_cmd.rs
index 9059f1dd8e8..c8695a8f626 100644
--- a/tooling/nargo_cli/src/cli/check_cmd.rs
+++ b/tooling/nargo_cli/src/cli/check_cmd.rs
@@ -4,33 +4,25 @@ use clap::Args;
 use fm::FileManager;
 use iter_extended::btree_map;
 use nargo::{
-    errors::CompileError,
-    insert_all_files_for_workspace_into_file_manager,
-    ops::report_errors,
-    package::{CrateName, Package},
-    parse_all, prepare_package,
+    errors::CompileError, insert_all_files_for_workspace_into_file_manager, ops::report_errors,
+    package::Package, parse_all, prepare_package,
 };
-use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection};
+use nargo_toml::{get_package_manifest, resolve_workspace_from_toml};
 use noirc_abi::{AbiParameter, AbiType, MAIN_RETURN_NAME};
 use noirc_driver::{
     check_crate, compute_function_abi, CompileOptions, CrateId, NOIR_ARTIFACT_VERSION_STRING,
 };
 use noirc_frontend::hir::{Context, ParsedFiles};
 
-use super::fs::write_to_file;
 use super::NargoConfig;
+use super::{fs::write_to_file, PackageOptions};
 
 /// Checks the constraint system for errors
 #[derive(Debug, Clone, Args)]
 #[clap(visible_alias = "c")]
 pub(crate) struct CheckCommand {
-    /// The name of the package to check
-    #[clap(long, conflicts_with = "workspace")]
-    package: Option<CrateName>,
-
-    /// Check all packages in the workspace
-    #[clap(long, conflicts_with = "package")]
-    workspace: bool,
+    #[clap(flatten)]
+    pub(super) package_options: PackageOptions,
 
     /// Force overwrite of existing files
     #[clap(long = "overwrite")]
@@ -42,9 +34,7 @@ pub(crate) struct CheckCommand {
 
 pub(crate) fn run(args: CheckCommand, config: NargoConfig) -> Result<(), CliError> {
     let toml_path = get_package_manifest(&config.program_dir)?;
-    let default_selection =
-        if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
-    let selection = args.package.map_or(default_selection, PackageSelection::Selected);
+    let selection = args.package_options.package_selection();
     let workspace = resolve_workspace_from_toml(
         &toml_path,
         selection,
diff --git a/tooling/nargo_cli/src/cli/compile_cmd.rs b/tooling/nargo_cli/src/cli/compile_cmd.rs
index f134374f89e..2ecf6959a94 100644
--- a/tooling/nargo_cli/src/cli/compile_cmd.rs
+++ b/tooling/nargo_cli/src/cli/compile_cmd.rs
@@ -5,7 +5,7 @@ use std::time::Duration;
 use acvm::acir::circuit::ExpressionWidth;
 use fm::FileManager;
 use nargo::ops::{collect_errors, compile_contract, compile_program, report_errors};
-use nargo::package::{CrateName, Package};
+use nargo::package::Package;
 use nargo::workspace::Workspace;
 use nargo::{insert_all_files_for_workspace_into_file_manager, parse_all};
 use nargo_toml::{
@@ -23,19 +23,14 @@ use notify_debouncer_full::new_debouncer;
 use crate::errors::CliError;
 
 use super::fs::program::{read_program_from_file, save_contract_to_file, save_program_to_file};
-use super::NargoConfig;
+use super::{NargoConfig, PackageOptions};
 use rayon::prelude::*;
 
 /// Compile the program and its secret execution trace into ACIR format
 #[derive(Debug, Clone, Args)]
 pub(crate) struct CompileCommand {
-    /// The name of the package to compile
-    #[clap(long, conflicts_with = "workspace")]
-    package: Option<CrateName>,
-
-    /// Compile all packages in the workspace.
-    #[clap(long, conflicts_with = "package")]
-    workspace: bool,
+    #[clap(flatten)]
+    pub(super) package_options: PackageOptions,
 
     #[clap(flatten)]
     compile_options: CompileOptions,
@@ -46,10 +41,7 @@ pub(crate) struct CompileCommand {
 }
 
 pub(crate) fn run(args: CompileCommand, config: NargoConfig) -> Result<(), CliError> {
-    let default_selection =
-        if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
-    let selection = args.package.map_or(default_selection, PackageSelection::Selected);
-
+    let selection = args.package_options.package_selection();
     let workspace = read_workspace(&config.program_dir, selection)?;
 
     if args.watch {
diff --git a/tooling/nargo_cli/src/cli/debug_cmd.rs b/tooling/nargo_cli/src/cli/debug_cmd.rs
index e837f297475..f4dd607a53e 100644
--- a/tooling/nargo_cli/src/cli/debug_cmd.rs
+++ b/tooling/nargo_cli/src/cli/debug_cmd.rs
@@ -38,7 +38,7 @@ pub(crate) struct DebugCommand {
 
     /// The name of the package to execute
     #[clap(long)]
-    package: Option<CrateName>,
+    pub(super) package: Option<CrateName>,
 
     #[clap(flatten)]
     compile_options: CompileOptions,
diff --git a/tooling/nargo_cli/src/cli/execute_cmd.rs b/tooling/nargo_cli/src/cli/execute_cmd.rs
index fa95d3123c6..81aca16846b 100644
--- a/tooling/nargo_cli/src/cli/execute_cmd.rs
+++ b/tooling/nargo_cli/src/cli/execute_cmd.rs
@@ -8,8 +8,8 @@ use clap::Args;
 use nargo::constants::PROVER_INPUT_FILE;
 use nargo::errors::try_to_diagnose_runtime_error;
 use nargo::foreign_calls::DefaultForeignCallExecutor;
-use nargo::package::{CrateName, Package};
-use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection};
+use nargo::package::Package;
+use nargo_toml::{get_package_manifest, resolve_workspace_from_toml};
 use noirc_abi::input_parser::{Format, InputValue};
 use noirc_abi::InputMap;
 use noirc_artifacts::debug::DebugArtifact;
@@ -17,7 +17,7 @@ use noirc_driver::{CompileOptions, CompiledProgram, NOIR_ARTIFACT_VERSION_STRING
 
 use super::compile_cmd::compile_workspace_full;
 use super::fs::{inputs::read_inputs_from_file, witness::save_witness_to_dir};
-use super::NargoConfig;
+use super::{NargoConfig, PackageOptions};
 use crate::cli::fs::program::read_program_from_file;
 use crate::errors::CliError;
 
@@ -34,13 +34,8 @@ pub(crate) struct ExecuteCommand {
     #[clap(long, short, default_value = PROVER_INPUT_FILE)]
     prover_name: String,
 
-    /// The name of the package to execute
-    #[clap(long, conflicts_with = "workspace")]
-    package: Option<CrateName>,
-
-    /// Execute all packages in the workspace
-    #[clap(long, conflicts_with = "package")]
-    workspace: bool,
+    #[clap(flatten)]
+    pub(super) package_options: PackageOptions,
 
     #[clap(flatten)]
     compile_options: CompileOptions,
@@ -52,9 +47,7 @@ pub(crate) struct ExecuteCommand {
 
 pub(crate) fn run(args: ExecuteCommand, config: NargoConfig) -> Result<(), CliError> {
     let toml_path = get_package_manifest(&config.program_dir)?;
-    let default_selection =
-        if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
-    let selection = args.package.map_or(default_selection, PackageSelection::Selected);
+    let selection = args.package_options.package_selection();
     let workspace = resolve_workspace_from_toml(
         &toml_path,
         selection,
diff --git a/tooling/nargo_cli/src/cli/export_cmd.rs b/tooling/nargo_cli/src/cli/export_cmd.rs
index c3752db7fbd..cb92b987c4e 100644
--- a/tooling/nargo_cli/src/cli/export_cmd.rs
+++ b/tooling/nargo_cli/src/cli/export_cmd.rs
@@ -10,13 +10,11 @@ use nargo::package::Package;
 use nargo::prepare_package;
 use nargo::workspace::Workspace;
 use nargo::{insert_all_files_for_workspace_into_file_manager, parse_all};
-use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection};
+use nargo_toml::{get_package_manifest, resolve_workspace_from_toml};
 use noirc_driver::{
     compile_no_check, CompileOptions, CompiledProgram, NOIR_ARTIFACT_VERSION_STRING,
 };
 
-use noirc_frontend::graph::CrateName;
-
 use clap::Args;
 
 use crate::errors::CliError;
@@ -24,18 +22,13 @@ use crate::errors::CliError;
 use super::check_cmd::check_crate_and_report_errors;
 
 use super::fs::program::save_program_to_file;
-use super::NargoConfig;
+use super::{NargoConfig, PackageOptions};
 
 /// Exports functions marked with #[export] attribute
 #[derive(Debug, Clone, Args)]
 pub(crate) struct ExportCommand {
-    /// The name of the package to compile
-    #[clap(long, conflicts_with = "workspace")]
-    package: Option<CrateName>,
-
-    /// Compile all packages in the workspace
-    #[clap(long, conflicts_with = "package")]
-    workspace: bool,
+    #[clap(flatten)]
+    pub(super) package_options: PackageOptions,
 
     #[clap(flatten)]
     compile_options: CompileOptions,
@@ -43,10 +36,7 @@ pub(crate) struct ExportCommand {
 
 pub(crate) fn run(args: ExportCommand, config: NargoConfig) -> Result<(), CliError> {
     let toml_path = get_package_manifest(&config.program_dir)?;
-    let default_selection =
-        if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
-    let selection = args.package.map_or(default_selection, PackageSelection::Selected);
-
+    let selection = args.package_options.package_selection();
     let workspace = resolve_workspace_from_toml(
         &toml_path,
         selection,
diff --git a/tooling/nargo_cli/src/cli/fmt_cmd.rs b/tooling/nargo_cli/src/cli/fmt_cmd.rs
index 66496db517c..7b29a90c5c0 100644
--- a/tooling/nargo_cli/src/cli/fmt_cmd.rs
+++ b/tooling/nargo_cli/src/cli/fmt_cmd.rs
@@ -9,7 +9,7 @@ use noirc_frontend::{hir::def_map::parse_file, parser::ParserError};
 
 use crate::errors::CliError;
 
-use super::NargoConfig;
+use super::{NargoConfig, PackageOptions};
 
 /// Format the Noir files in a workspace
 #[derive(Debug, Clone, Args)]
@@ -17,15 +17,22 @@ pub(crate) struct FormatCommand {
     /// Run noirfmt in check mode
     #[arg(long)]
     check: bool,
+
+    #[clap(flatten)]
+    pub(super) package_options: PackageOptions,
 }
 
 pub(crate) fn run(args: FormatCommand, config: NargoConfig) -> Result<(), CliError> {
     let check_mode = args.check;
 
     let toml_path = get_package_manifest(&config.program_dir)?;
+    let selection = match args.package_options.package_selection() {
+        PackageSelection::DefaultOrAll => PackageSelection::All,
+        other => other,
+    };
     let workspace = resolve_workspace_from_toml(
         &toml_path,
-        PackageSelection::All,
+        selection,
         Some(NOIR_ARTIFACT_VERSION_STRING.to_string()),
     )?;
 
diff --git a/tooling/nargo_cli/src/cli/info_cmd.rs b/tooling/nargo_cli/src/cli/info_cmd.rs
index 769a1f79d81..4dab7da2ffb 100644
--- a/tooling/nargo_cli/src/cli/info_cmd.rs
+++ b/tooling/nargo_cli/src/cli/info_cmd.rs
@@ -3,11 +3,9 @@ use bn254_blackbox_solver::Bn254BlackBoxSolver;
 use clap::Args;
 use iter_extended::vecmap;
 use nargo::{
-    constants::PROVER_INPUT_FILE,
-    foreign_calls::DefaultForeignCallExecutor,
-    package::{CrateName, Package},
+    constants::PROVER_INPUT_FILE, foreign_calls::DefaultForeignCallExecutor, package::Package,
 };
-use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection};
+use nargo_toml::{get_package_manifest, resolve_workspace_from_toml};
 use noirc_abi::input_parser::Format;
 use noirc_artifacts::program::ProgramArtifact;
 use noirc_driver::{CompileOptions, NOIR_ARTIFACT_VERSION_STRING};
@@ -20,7 +18,7 @@ use crate::{cli::fs::inputs::read_inputs_from_file, errors::CliError};
 use super::{
     compile_cmd::{compile_workspace_full, get_target_width},
     fs::program::read_program_from_file,
-    NargoConfig,
+    NargoConfig, PackageOptions,
 };
 
 /// Provides detailed information on each of a program's function (represented by a single circuit)
@@ -31,13 +29,8 @@ use super::{
 #[derive(Debug, Clone, Args)]
 #[clap(visible_alias = "i")]
 pub(crate) struct InfoCommand {
-    /// The name of the package to detail
-    #[clap(long, conflicts_with = "workspace")]
-    package: Option<CrateName>,
-
-    /// Detail all packages in the workspace
-    #[clap(long, conflicts_with = "package")]
-    workspace: bool,
+    #[clap(flatten)]
+    pub(super) package_options: PackageOptions,
 
     /// Output a JSON formatted report. Changes to this format are not currently considered breaking.
     #[clap(long, hide = true)]
@@ -56,9 +49,7 @@ pub(crate) struct InfoCommand {
 
 pub(crate) fn run(mut args: InfoCommand, config: NargoConfig) -> Result<(), CliError> {
     let toml_path = get_package_manifest(&config.program_dir)?;
-    let default_selection =
-        if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
-    let selection = args.package.map_or(default_selection, PackageSelection::Selected);
+    let selection = args.package_options.package_selection();
     let workspace = resolve_workspace_from_toml(
         &toml_path,
         selection,
diff --git a/tooling/nargo_cli/src/cli/mod.rs b/tooling/nargo_cli/src/cli/mod.rs
index 284dd10cb88..cc72092daa1 100644
--- a/tooling/nargo_cli/src/cli/mod.rs
+++ b/tooling/nargo_cli/src/cli/mod.rs
@@ -1,8 +1,8 @@
 use clap::{Args, Parser, Subcommand};
 use const_format::formatcp;
-use nargo_toml::find_package_root;
-use noirc_driver::NOIR_ARTIFACT_VERSION_STRING;
-use std::path::PathBuf;
+use nargo_toml::{ManifestError, PackageSelection};
+use noirc_driver::{CrateName, NOIR_ARTIFACT_VERSION_STRING};
+use std::path::{Path, PathBuf};
 
 use color_eyre::eyre;
 
@@ -52,6 +52,48 @@ pub(crate) struct NargoConfig {
     program_dir: PathBuf,
 }
 
+/// Options for commands that work on either workspace or package scope.
+#[derive(Args, Clone, Debug, Default)]
+pub(crate) struct PackageOptions {
+    /// The name of the package to run the command on.
+    /// By default run on the first one found moving up along the ancestors of the current directory.
+    #[clap(long, conflicts_with = "workspace")]
+    package: Option<CrateName>,
+
+    /// Run on all packages in the workspace
+    #[clap(long, conflicts_with = "package")]
+    workspace: bool,
+}
+
+impl PackageOptions {
+    /// Decide which package to run the command on:
+    /// * `package` if non-empty
+    /// * all packages if `workspace` is `true`
+    /// * otherwise the default package
+    pub(crate) fn package_selection(&self) -> PackageSelection {
+        let default_selection =
+            if self.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
+
+        self.package.clone().map_or(default_selection, PackageSelection::Selected)
+    }
+
+    /// Whether we need to look for the package manifest at the workspace level.
+    /// If a package is specified, it might not be the current package.
+    fn scope(&self) -> CommandScope {
+        if self.workspace || self.package.is_some() {
+            CommandScope::Workspace
+        } else {
+            CommandScope::CurrentPackage
+        }
+    }
+}
+
+enum CommandScope {
+    Workspace,
+    CurrentPackage,
+    Any,
+}
+
 #[non_exhaustive]
 #[derive(Subcommand, Clone, Debug)]
 enum NargoCommand {
@@ -83,22 +125,8 @@ pub(crate) fn start_cli() -> eyre::Result<()> {
     }
 
     // Search through parent directories to find package root if necessary.
-    match &command {
-        NargoCommand::Check(..)
-        | NargoCommand::Fmt(..)
-        | NargoCommand::Compile(..)
-        | NargoCommand::Execute(..)
-        | NargoCommand::Export(..)
-        | NargoCommand::Debug(..)
-        | NargoCommand::Test(..)
-        | NargoCommand::Info(..) => {
-            config.program_dir = find_package_root(&config.program_dir)?;
-        }
-        NargoCommand::New(..)
-        | NargoCommand::Init(..)
-        | NargoCommand::Lsp(..)
-        | NargoCommand::Dap(..)
-        | NargoCommand::GenerateCompletionScript(..) => (),
+    if let Some(program_dir) = command_dir(&command, &config.program_dir)? {
+        config.program_dir = program_dir;
     }
 
     match command {
@@ -127,6 +155,43 @@ pub(crate) fn start_cli() -> eyre::Result<()> {
     Ok(())
 }
 
+/// Some commands have package options, which we use here to decide whether to
+/// alter `--program-dir` to point at a manifest, depending on whether we want
+/// to work on a specific package or the entire workspace.
+fn command_scope(cmd: &NargoCommand) -> CommandScope {
+    match &cmd {
+        NargoCommand::Check(cmd) => cmd.package_options.scope(),
+        NargoCommand::Compile(cmd) => cmd.package_options.scope(),
+        NargoCommand::Execute(cmd) => cmd.package_options.scope(),
+        NargoCommand::Export(cmd) => cmd.package_options.scope(),
+        NargoCommand::Test(cmd) => cmd.package_options.scope(),
+        NargoCommand::Info(cmd) => cmd.package_options.scope(),
+        NargoCommand::Fmt(cmd) => cmd.package_options.scope(),
+        NargoCommand::Debug(cmd) => {
+            if cmd.package.is_some() {
+                CommandScope::Workspace
+            } else {
+                CommandScope::CurrentPackage
+            }
+        }
+        NargoCommand::New(..)
+        | NargoCommand::Init(..)
+        | NargoCommand::Lsp(..)
+        | NargoCommand::Dap(..)
+        | NargoCommand::GenerateCompletionScript(..) => CommandScope::Any,
+    }
+}
+
+/// A manifest directory we need to change into, if the command needs it.
+fn command_dir(cmd: &NargoCommand, program_dir: &Path) -> Result<Option<PathBuf>, ManifestError> {
+    let workspace = match command_scope(cmd) {
+        CommandScope::Workspace => true,
+        CommandScope::CurrentPackage => false,
+        CommandScope::Any => return Ok(None),
+    };
+    Ok(Some(nargo_toml::find_root(program_dir, workspace)?))
+}
+
 #[cfg(test)]
 mod tests {
     use clap::Parser;
diff --git a/tooling/nargo_cli/src/cli/test_cmd.rs b/tooling/nargo_cli/src/cli/test_cmd.rs
index e8c0e16ff4a..92d3c91e713 100644
--- a/tooling/nargo_cli/src/cli/test_cmd.rs
+++ b/tooling/nargo_cli/src/cli/test_cmd.rs
@@ -5,12 +5,10 @@ use bn254_blackbox_solver::Bn254BlackBoxSolver;
 use clap::Args;
 use fm::FileManager;
 use nargo::{
-    insert_all_files_for_workspace_into_file_manager,
-    ops::TestStatus,
-    package::{CrateName, Package},
-    parse_all, prepare_package,
+    insert_all_files_for_workspace_into_file_manager, ops::TestStatus, package::Package, parse_all,
+    prepare_package,
 };
-use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection};
+use nargo_toml::{get_package_manifest, resolve_workspace_from_toml};
 use noirc_driver::{check_crate, CompileOptions, NOIR_ARTIFACT_VERSION_STRING};
 use noirc_frontend::hir::{FunctionNameMatch, ParsedFiles};
 use rayon::prelude::{IntoParallelIterator, ParallelBridge, ParallelIterator};
@@ -18,7 +16,7 @@ use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
 
 use crate::{cli::check_cmd::check_crate_and_report_errors, errors::CliError};
 
-use super::NargoConfig;
+use super::{NargoConfig, PackageOptions};
 
 /// Run the tests for this program
 #[derive(Debug, Clone, Args)]
@@ -35,13 +33,8 @@ pub(crate) struct TestCommand {
     #[clap(long)]
     exact: bool,
 
-    /// The name of the package to test
-    #[clap(long, conflicts_with = "workspace")]
-    package: Option<CrateName>,
-
-    /// Test all packages in the workspace
-    #[clap(long, conflicts_with = "package")]
-    workspace: bool,
+    #[clap(flatten)]
+    pub(super) package_options: PackageOptions,
 
     #[clap(flatten)]
     compile_options: CompileOptions,
@@ -53,9 +46,7 @@ pub(crate) struct TestCommand {
 
 pub(crate) fn run(args: TestCommand, config: NargoConfig) -> Result<(), CliError> {
     let toml_path = get_package_manifest(&config.program_dir)?;
-    let default_selection =
-        if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
-    let selection = args.package.map_or(default_selection, PackageSelection::Selected);
+    let selection = args.package_options.package_selection();
     let workspace = resolve_workspace_from_toml(
         &toml_path,
         selection,
diff --git a/tooling/nargo_toml/Cargo.toml b/tooling/nargo_toml/Cargo.toml
index 2bc24153836..f5f7d7cd595 100644
--- a/tooling/nargo_toml/Cargo.toml
+++ b/tooling/nargo_toml/Cargo.toml
@@ -25,4 +25,5 @@ noirc_driver.workspace = true
 semver = "1.0.20"
 
 [dev-dependencies]
+tempfile.workspace = true
 test-case.workspace = true
diff --git a/tooling/nargo_toml/src/lib.rs b/tooling/nargo_toml/src/lib.rs
index c1990dab4a6..b5c45977618 100644
--- a/tooling/nargo_toml/src/lib.rs
+++ b/tooling/nargo_toml/src/lib.rs
@@ -47,6 +47,35 @@ pub fn find_file_manifest(current_path: &Path) -> Option<PathBuf> {
 }
 
 /// Returns the [PathBuf] of the directory containing the `Nargo.toml` by searching from `current_path` to the root of its [Path].
+/// When `workspace` is `true` it returns the topmost directory, when `false` the innermost one.
+///
+/// Returns a [ManifestError] if no parent directories of `current_path` contain a manifest file.
+pub fn find_root(current_path: &Path, workspace: bool) -> Result<PathBuf, ManifestError> {
+    if workspace {
+        find_package_root(current_path)
+    } else {
+        find_file_root(current_path)
+    }
+}
+
+/// Returns the [PathBuf] of the directory containing the `Nargo.toml` by searching from `current_path` to the root of its [Path],
+/// returning at the innermost directory found, i.e. the one corresponding to the package that contains the `current_path`.
+///
+/// Returns a [ManifestError] if no parent directories of `current_path` contain a manifest file.
+pub fn find_file_root(current_path: &Path) -> Result<PathBuf, ManifestError> {
+    match find_file_manifest(current_path) {
+        Some(manifest_path) => {
+            let package_root = manifest_path
+                .parent()
+                .expect("infallible: manifest file path can't be root directory");
+            Ok(package_root.to_path_buf())
+        }
+        None => Err(ManifestError::MissingFile(current_path.to_path_buf())),
+    }
+}
+
+/// Returns the [PathBuf] of the directory containing the `Nargo.toml` by searching from `current_path` to the root of its [Path],
+/// returning at the topmost directory found, i.e. the one corresponding to the entire workspace.
 ///
 /// Returns a [ManifestError] if no parent directories of `current_path` contain a manifest file.
 pub fn find_package_root(current_path: &Path) -> Result<PathBuf, ManifestError> {
@@ -60,6 +89,11 @@ pub fn find_package_root(current_path: &Path) -> Result<PathBuf, ManifestError>
 }
 
 // TODO(#2323): We are probably going to need a "filepath utils" crate soon
+/// Get the root of path, for example:
+/// * `C:\foo\bar` -> `C:\foo`
+/// * `//shared/foo/bar` -> `//shared/foo`
+/// * `/foo` -> `/foo`
+/// otherwise empty path.
 fn path_root(path: &Path) -> PathBuf {
     let mut components = path.components();
 
@@ -101,6 +135,7 @@ pub fn find_package_manifest(
         })
     }
 }
+
 /// Returns the [PathBuf] of the `Nargo.toml` file in the `current_path` directory.
 ///
 /// Returns a [ManifestError] if `current_path` does not contain a manifest file.
@@ -490,9 +525,20 @@ pub fn resolve_workspace_from_toml(
     Ok(workspace)
 }
 
-#[test]
-fn parse_standard_toml() {
-    let src = r#"
+#[cfg(test)]
+mod tests {
+    use std::{
+        path::{Path, PathBuf},
+        str::FromStr,
+    };
+
+    use test_case::test_matrix;
+
+    use crate::{find_root, Config, ManifestError};
+
+    #[test]
+    fn parse_standard_toml() {
+        let src = r#"
 
         [package]
         name = "test"
@@ -505,49 +551,49 @@ fn parse_standard_toml() {
         hello = {path = "./noir_driver"}
     "#;
 
-    assert!(Config::try_from(String::from(src)).is_ok());
-    assert!(Config::try_from(src).is_ok());
-}
+        assert!(Config::try_from(String::from(src)).is_ok());
+        assert!(Config::try_from(src).is_ok());
+    }
 
-#[test]
-fn parse_package_toml_no_deps() {
-    let src = r#"
+    #[test]
+    fn parse_package_toml_no_deps() {
+        let src = r#"
         [package]
         name = "test"
         authors = ["kev", "foo"]
         compiler_version = "*"
     "#;
 
-    assert!(Config::try_from(String::from(src)).is_ok());
-    assert!(Config::try_from(src).is_ok());
-}
+        assert!(Config::try_from(String::from(src)).is_ok());
+        assert!(Config::try_from(src).is_ok());
+    }
 
-#[test]
-fn parse_workspace_toml() {
-    let src = r#"
+    #[test]
+    fn parse_workspace_toml() {
+        let src = r#"
         [workspace]
         members = ["a", "b"]
     "#;
 
-    assert!(Config::try_from(String::from(src)).is_ok());
-    assert!(Config::try_from(src).is_ok());
-}
+        assert!(Config::try_from(String::from(src)).is_ok());
+        assert!(Config::try_from(src).is_ok());
+    }
 
-#[test]
-fn parse_workspace_default_member_toml() {
-    let src = r#"
+    #[test]
+    fn parse_workspace_default_member_toml() {
+        let src = r#"
         [workspace]
         members = ["a", "b"]
         default-member = "a"
     "#;
 
-    assert!(Config::try_from(String::from(src)).is_ok());
-    assert!(Config::try_from(src).is_ok());
-}
+        assert!(Config::try_from(String::from(src)).is_ok());
+        assert!(Config::try_from(src).is_ok());
+    }
 
-#[test]
-fn parse_package_expression_width_toml() {
-    let src = r#"
+    #[test]
+    fn parse_package_expression_width_toml() {
+        let src = r#"
     [package]
     name = "test"
     version = "0.1.0"
@@ -556,6 +602,124 @@ fn parse_package_expression_width_toml() {
     expression_width = "3"
     "#;
 
-    assert!(Config::try_from(String::from(src)).is_ok());
-    assert!(Config::try_from(src).is_ok());
+        assert!(Config::try_from(String::from(src)).is_ok());
+        assert!(Config::try_from(src).is_ok());
+    }
+
+    /// Test that `find_root` handles all kinds of prefixes.
+    /// (It dispatches based on `workspace` to methods which handle paths differently).
+    #[test_matrix(
+        [true, false],
+        ["C:\\foo\\bar", "//shared/foo/bar", "/foo/bar", "bar/baz", ""]
+    )]
+    fn test_find_root_does_not_panic(workspace: bool, path: &str) {
+        let path = PathBuf::from_str(path).unwrap();
+        let error = find_root(&path, workspace).expect_err("non-existing paths");
+        assert!(matches!(error, ManifestError::MissingFile(_)));
+    }
+
+    /// Test to demonstrate how `find_root` works.
+    #[test]
+    fn test_find_root_example() {
+        const INDENT_SIZE: usize = 4;
+        /// Create directories and files according to a YAML-like layout below
+        fn setup(layout: &str, root: &Path) {
+            fn is_dir(item: &str) -> bool {
+                !item.contains('.')
+            }
+            let mut current_dir = root.to_path_buf();
+            let mut current_indent = 0;
+            let mut last_item: Option<String> = None;
+
+            for line in layout.lines() {
+                if let Some((prefix, item)) = line.split_once('-') {
+                    let item = item.replace(std::path::MAIN_SEPARATOR, "_").trim().to_string();
+
+                    let indent = prefix.len() / INDENT_SIZE;
+
+                    if last_item.is_none() {
+                        current_indent = indent;
+                    }
+
+                    assert!(
+                        indent <= current_indent + 1,
+                        "cannot increase indent by more than {INDENT_SIZE}; item = {item}, current_dir={}", current_dir.display()
+                    );
+
+                    // Go into the last created directory
+                    if indent > current_indent && last_item.is_some() {
+                        let last_item = last_item.unwrap();
+                        assert!(is_dir(&last_item), "last item was not a dir: {last_item}");
+                        current_dir.push(last_item);
+                        current_indent += 1;
+                    }
+                    // Go back into an ancestor directory
+                    while indent < current_indent {
+                        current_dir.pop();
+                        current_indent -= 1;
+                    }
+                    // Create a file or a directory
+                    let item_path = current_dir.join(&item);
+                    if is_dir(&item) {
+                        std::fs::create_dir(&item_path).unwrap_or_else(|e| {
+                            panic!("failed to create dir {}: {e}", item_path.display())
+                        });
+                    } else {
+                        std::fs::write(&item_path, "").expect("failed to create file");
+                    }
+
+                    last_item = Some(item);
+                }
+            }
+        }
+
+        // Temporary directory to hold the project.
+        let tmp = tempfile::tempdir().unwrap();
+        // Join a string path to the tmp dir
+        let path = |p: &str| tmp.path().join(p);
+        // Check that an expected root is found
+        let assert_ok = |current_dir: &str, ws: bool, exp: &str| {
+            let root = find_root(&path(current_dir), ws).expect("should find a root");
+            assert_eq!(root, path(exp));
+        };
+        // Check that a root is not found
+        let assert_err = |current_dir: &str| {
+            find_root(&path(current_dir), true).expect_err("shouldn't find a root");
+        };
+
+        let layout = r"
+            - project
+                - docs
+                - workspace
+                    - packages
+                        - foo
+                            - Nargo.toml
+                            - Prover.toml
+                            - src
+                                - main.nr
+                        - bar
+                            - Nargo.toml
+                            - src
+                                - lib.nr
+                    - Nargo.toml
+                - examples
+                    - baz
+                        - Nargo.toml
+                        - src
+                            - main.nr
+            ";
+
+        // Set up the file system.
+        setup(layout, tmp.path());
+
+        assert_err("dummy");
+        assert_err("project/docs");
+        assert_err("project/examples");
+        assert_ok("project/workspace", true, "project/workspace");
+        assert_ok("project/workspace", false, "project/workspace");
+        assert_ok("project/workspace/packages/foo", true, "project/workspace");
+        assert_ok("project/workspace/packages/bar", false, "project/workspace/packages/bar");
+        assert_ok("project/examples/baz/src", true, "project/examples/baz");
+        assert_ok("project/examples/baz/src", false, "project/examples/baz");
+    }
 }