diff --git a/cli/src/app.rs b/cli/src/app.rs
index 102e19041..a5d66cabe 100644
--- a/cli/src/app.rs
+++ b/cli/src/app.rs
@@ -575,6 +575,22 @@ pub fn add_subcommands(command: Command) -> Command {
                 .about("Find all lockfile and manifest paths")
                 .hide(true),
         )
+        .subcommand(
+            Command::new("generate-lockfile")
+                .args(&[
+                    Arg::new("lockfile-type")
+                        .value_name("TYPE")
+                        .required(true)
+                        .help("Lockfile type whose generator will be used")
+                        .value_parser(PossibleValuesParser::new(parse::lockfile_types(true))),
+                    Arg::new("manifest")
+                        .value_name("MANIFEST")
+                        .required(true)
+                        .help("Canonicalized manifest path"),
+                ])
+                .about("Run lockfile generation inside sandbox and write it to STDOUT")
+                .hide(true),
+        )
         .subcommand(extensions::command());
 
     #[cfg(unix)]
diff --git a/cli/src/bin/phylum.rs b/cli/src/bin/phylum.rs
index 3273a1238..0d9188e2b 100644
--- a/cli/src/bin/phylum.rs
+++ b/cli/src/bin/phylum.rs
@@ -13,8 +13,8 @@ use phylum_cli::commands::sandbox;
 #[cfg(feature = "selfmanage")]
 use phylum_cli::commands::uninstall;
 use phylum_cli::commands::{
-    auth, extensions, find_lockable_files, group, init, jobs, packages, parse, project, status,
-    CommandResult, ExitCode,
+    auth, extensions, find_lockable_files, generate_lockfile, group, init, jobs, packages, parse,
+    project, status, CommandResult, ExitCode,
 };
 use phylum_cli::config::{self, Config};
 use phylum_cli::spinner::Spinner;
@@ -162,6 +162,7 @@ async fn handle_commands() -> CommandResult {
         #[cfg(unix)]
         "sandbox" => sandbox::handle_sandbox(sub_matches).await,
         "find-lockable-files" => find_lockable_files::handle_command(),
+        "generate-lockfile" => generate_lockfile::handle_command(sub_matches),
         extension_subcmd => {
             extensions::handle_run_extension(Box::pin(api), extension_subcmd, sub_matches).await
         },
diff --git a/cli/src/commands/extensions/api.rs b/cli/src/commands/extensions/api.rs
index adffab3e0..d1189817b 100644
--- a/cli/src/commands/extensions/api.rs
+++ b/cli/src/commands/extensions/api.rs
@@ -377,6 +377,7 @@ async fn parse_lockfile(
     lockfile: String,
     lockfile_type: Option<String>,
     generate_lockfiles: Option<bool>,
+    sandbox_generation: Option<bool>,
 ) -> Result<PackageLock> {
     // Ensure extension has file read-access.
     {
@@ -390,12 +391,13 @@ async fn parse_lockfile(
     let project_root = current_project.as_ref().map(|p| p.root());
 
     // Attempt to parse as requested lockfile type.
+    let sandbox = sandbox_generation.unwrap_or(true);
     let generate_lockfiles = generate_lockfiles.unwrap_or(true);
     let parsed = parse::parse_lockfile(
         lockfile,
         project_root,
         lockfile_type.as_deref(),
-        false,
+        sandbox,
         generate_lockfiles,
     )?;
 
diff --git a/cli/src/commands/generate_lockfile.rs b/cli/src/commands/generate_lockfile.rs
new file mode 100644
index 000000000..f6e5b27aa
--- /dev/null
+++ b/cli/src/commands/generate_lockfile.rs
@@ -0,0 +1,107 @@
+//! `phylum generate-lockfile` subcommand.
+
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+use birdcage::{Birdcage, Exception, Sandbox};
+use clap::ArgMatches;
+use phylum_lockfile::LockfileFormat;
+
+use crate::commands::extensions::permissions;
+use crate::commands::{CommandResult, ExitCode};
+use crate::dirs;
+
+/// Handle `phylum generate-lockfile` subcommand.
+pub fn handle_command(matches: &ArgMatches) -> CommandResult {
+    let lockfile_type = matches.get_one::<String>("lockfile-type").unwrap();
+    let manifest = matches.get_raw("manifest").unwrap().next().unwrap();
+    let manifest_path = PathBuf::from(manifest);
+
+    // Get generator for the lockfile type.
+    let lockfile_format = lockfile_type.parse::<LockfileFormat>().unwrap();
+    let generator = lockfile_format.parser().generator().unwrap();
+
+    // Setup sandbox for lockfile generation.
+    let birdcage = lockfile_generation_sandbox(&manifest_path)?;
+    birdcage.lock()?;
+
+    // Generate the lockfile.
+    let generated_lockfile = generator
+        .generate_lockfile(&manifest_path)
+        .context("lockfile generation subcommand failed")?;
+
+    // Write lockfile to stdout.
+    println!("{}", generated_lockfile);
+
+    Ok(ExitCode::Ok)
+}
+
+/// Create sandbox with exceptions allowing generation of any lockfile.
+fn lockfile_generation_sandbox(canonical_manifest_path: &Path) -> Result<Birdcage> {
+    let mut birdcage = permissions::default_sandbox()?;
+
+    // Allow all networking.
+    birdcage.add_exception(Exception::Networking)?;
+
+    // Add exception for the manifest's parent directory.
+    let project_path = canonical_manifest_path.parent().expect("Invalid manifest path");
+    permissions::add_exception(&mut birdcage, Exception::WriteAndRead(project_path.into()))?;
+
+    // Add exception for all the executables required for generation.
+    let ecosystem_bins = [
+        "cargo", "bundle", "mvn", "gradle", "npm", "pnpm", "yarn", "python3", "pipenv", "poetry",
+        "go", "dotnet",
+    ];
+    for bin in ecosystem_bins {
+        let absolute_path = permissions::resolve_bin_path(bin);
+        permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead(absolute_path))?;
+    }
+
+    // Allow any executable in common binary directories.
+    //
+    // Reading binaries shouldn't be an attack vector, but significantly simplifies
+    // complex ecosystems (like Python's symlinks).
+    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead("/usr/bin".into()))?;
+    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead("/bin".into()))?;
+
+    // Add paths required by specific ecosystems.
+    let home = dirs::home_dir()?;
+    // Cargo.
+    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead(home.join(".rustup")))?;
+    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead(home.join(".cargo")))?;
+    permissions::add_exception(&mut birdcage, Exception::Read("/etc/passwd".into()))?;
+    // Bundle.
+    permissions::add_exception(&mut birdcage, Exception::Read("/dev/urandom".into()))?;
+    // Maven.
+    permissions::add_exception(&mut birdcage, Exception::WriteAndRead(home.join(".m2")))?;
+    permissions::add_exception(&mut birdcage, Exception::WriteAndRead("/var/folders".into()))?;
+    permissions::add_exception(&mut birdcage, Exception::Read("/opt/maven".into()))?;
+    permissions::add_exception(&mut birdcage, Exception::Read("/etc/java-openjdk".into()))?;
+    permissions::add_exception(&mut birdcage, Exception::Read("/usr/local/Cellar/maven".into()))?;
+    permissions::add_exception(&mut birdcage, Exception::Read("/usr/local/Cellar/openjdk".into()))?;
+    permissions::add_exception(
+        &mut birdcage,
+        Exception::Read("/opt/homebrew/Cellar/maven".into()),
+    )?;
+    permissions::add_exception(
+        &mut birdcage,
+        Exception::Read("/opt/homebrew/Cellar/openjdk".into()),
+    )?;
+    // Gradle.
+    permissions::add_exception(&mut birdcage, Exception::WriteAndRead(home.join(".gradle")))?;
+    permissions::add_exception(
+        &mut birdcage,
+        Exception::Read("/usr/share/java/gradle/lib".into()),
+    )?;
+    permissions::add_exception(&mut birdcage, Exception::Read("/usr/local/Cellar/gradle".into()))?;
+    permissions::add_exception(
+        &mut birdcage,
+        Exception::Read("/opt/homebrew/Cellar/gradle".into()),
+    )?;
+    // Pnpm.
+    permissions::add_exception(&mut birdcage, Exception::Read("/tmp".into()))?;
+    // Yarn.
+    permissions::add_exception(&mut birdcage, Exception::Read(home.join("./yarn")))?;
+
+    Ok(birdcage)
+}
diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs
index 675815d38..633cca5ca 100644
--- a/cli/src/commands/mod.rs
+++ b/cli/src/commands/mod.rs
@@ -3,6 +3,7 @@ use std::process;
 pub mod auth;
 pub mod extensions;
 pub mod find_lockable_files;
+pub mod generate_lockfile;
 pub mod group;
 pub mod init;
 pub mod jobs;
diff --git a/cli/src/commands/parse.rs b/cli/src/commands/parse.rs
index 5a7b03a0e..5c9bf5224 100644
--- a/cli/src/commands/parse.rs
+++ b/cli/src/commands/parse.rs
@@ -2,18 +2,17 @@
 
 use std::borrow::Cow;
 use std::path::{Path, PathBuf};
+use std::process::Command;
 use std::result::Result as StdResult;
 use std::{env, fs, io};
 
 use anyhow::{anyhow, Context, Result};
-use birdcage::{Birdcage, Exception, Sandbox};
 use phylum_lockfile::generator::Generator;
 use phylum_lockfile::{LockfileFormat, Package, PackageVersion, Parse, ThirdPartyVersion};
 use phylum_types::types::package::{PackageDescriptor, PackageDescriptorAndLockfile};
 
-use crate::commands::extensions::permissions;
 use crate::commands::{CommandResult, ExitCode};
-use crate::{config, dirs, print_user_failure, print_user_warning};
+use crate::{config, print_user_failure, print_user_warning};
 
 /// Lockfile parsing error.
 #[derive(thiserror::Error, Debug)]
@@ -158,7 +157,8 @@ pub fn parse_lockfile(
     eprintln!("Generating lockfile for manifest {display_path:?} using {format:?}…");
 
     // Generate a new lockfile.
-    let generated_lockfile = generate_lockfile(generator, &path, sandbox_generation)?;
+    let generated_lockfile =
+        generate_lockfile(generator, format.name(), &path, sandbox_generation)?;
 
     // Parse the generated lockfile.
     let packages = parse_lockfile_content(&generated_lockfile, parser)?;
@@ -167,91 +167,37 @@ pub fn parse_lockfile(
 }
 
 /// Generate a lockfile from a manifest inside a sandbox.
-fn generate_lockfile(generator: &dyn Generator, path: &Path, sandbox: bool) -> Result<String> {
+fn generate_lockfile(
+    generator: &dyn Generator,
+    lockfile_type: &str,
+    path: &Path,
+    sandbox: bool,
+) -> Result<String> {
     let canonical_path = path.canonicalize()?;
 
-    // Enable the sandbox.
     if sandbox {
-        let birdcage = lockfile_generation_sandbox(&canonical_path)?;
-        birdcage.lock()?;
-    }
-
-    let generated_lockfile = generator
-        .generate_lockfile(&canonical_path)
-        .context("Lockfile generation failed! For details, see: \
-            https://docs.phylum.io/docs/lockfile_generation")?;
-
-    Ok(generated_lockfile)
-}
-
-/// Create sandbox with exceptions allowing generation of any lockfile.
-fn lockfile_generation_sandbox(canonical_manifest_path: &Path) -> Result<Birdcage> {
-    let mut birdcage = permissions::default_sandbox()?;
-
-    // Allow all networking.
-    birdcage.add_exception(Exception::Networking)?;
-
-    // Add exception for the manifest's parent directory.
-    let project_path = canonical_manifest_path.parent().expect("Invalid manifest path");
-    permissions::add_exception(&mut birdcage, Exception::WriteAndRead(project_path.into()))?;
-
-    // Add exception for all the executables required for generation.
-    let ecosystem_bins = [
-        "cargo", "bundle", "mvn", "gradle", "npm", "pnpm", "yarn", "python3", "pipenv", "poetry",
-        "go", "dotnet",
-    ];
-    for bin in ecosystem_bins {
-        let absolute_path = permissions::resolve_bin_path(bin);
-        permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead(absolute_path))?;
+        // Spawn separate sandboxed process to generate the lockfile.
+        let current_exe = env::current_exe()?;
+        let output = Command::new(current_exe)
+            .arg("generate-lockfile")
+            .arg(lockfile_type)
+            .arg(canonical_path)
+            .output()?;
+
+        if !output.status.success() {
+            let stderr = String::from_utf8_lossy(&output.stderr);
+            Err(anyhow!("subprocess failed:\n{stderr}"))
+                .context("Lockfile generation failed! For details, see: \
+                    https://docs.phylum.io/docs/lockfile_generation")
+        } else {
+            Ok(String::from_utf8_lossy(&output.stdout).into())
+        }
+    } else {
+        generator
+            .generate_lockfile(&canonical_path)
+            .context("Lockfile generation failed! For details, see: \
+                https://docs.phylum.io/docs/lockfile_generation")
     }
-
-    // Allow any executable in common binary directories.
-    //
-    // Reading binaries shouldn't be an attack vector, but significantly simplifies
-    // complex ecosystems (like Python's symlinks).
-    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead("/usr/bin".into()))?;
-    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead("/bin".into()))?;
-
-    // Add paths required by specific ecosystems.
-    let home = dirs::home_dir()?;
-    // Cargo.
-    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead(home.join(".rustup")))?;
-    permissions::add_exception(&mut birdcage, Exception::ExecuteAndRead(home.join(".cargo")))?;
-    permissions::add_exception(&mut birdcage, Exception::Read("/etc/passwd".into()))?;
-    // Bundle.
-    permissions::add_exception(&mut birdcage, Exception::Read("/dev/urandom".into()))?;
-    // Maven.
-    permissions::add_exception(&mut birdcage, Exception::WriteAndRead(home.join(".m2")))?;
-    permissions::add_exception(&mut birdcage, Exception::WriteAndRead("/var/folders".into()))?;
-    permissions::add_exception(&mut birdcage, Exception::Read("/opt/maven".into()))?;
-    permissions::add_exception(&mut birdcage, Exception::Read("/etc/java-openjdk".into()))?;
-    permissions::add_exception(&mut birdcage, Exception::Read("/usr/local/Cellar/maven".into()))?;
-    permissions::add_exception(&mut birdcage, Exception::Read("/usr/local/Cellar/openjdk".into()))?;
-    permissions::add_exception(
-        &mut birdcage,
-        Exception::Read("/opt/homebrew/Cellar/maven".into()),
-    )?;
-    permissions::add_exception(
-        &mut birdcage,
-        Exception::Read("/opt/homebrew/Cellar/openjdk".into()),
-    )?;
-    // Gradle.
-    permissions::add_exception(&mut birdcage, Exception::WriteAndRead(home.join(".gradle")))?;
-    permissions::add_exception(
-        &mut birdcage,
-        Exception::Read("/usr/share/java/gradle/lib".into()),
-    )?;
-    permissions::add_exception(&mut birdcage, Exception::Read("/usr/local/Cellar/gradle".into()))?;
-    permissions::add_exception(
-        &mut birdcage,
-        Exception::Read("/opt/homebrew/Cellar/gradle".into()),
-    )?;
-    // Pnpm.
-    permissions::add_exception(&mut birdcage, Exception::Read("/tmp".into()))?;
-    // Yarn.
-    permissions::add_exception(&mut birdcage, Exception::Read(home.join("./yarn")))?;
-
-    Ok(birdcage)
 }
 
 /// Attempt to parse a lockfile.
diff --git a/extensions/CHANGELOG.md b/extensions/CHANGELOG.md
index 41b28967e..170e9f766 100644
--- a/extensions/CHANGELOG.md
+++ b/extensions/CHANGELOG.md
@@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Added
 
 - `generateLockfiles` parameter for `parseLockfile` to inhibit lockfile generation
+- `sandboxGeneration` parameter for `parseLockfile` to disable the lockfile
+    generation sandbox
 
 ### Fixed
 
diff --git a/extensions/phylum.ts b/extensions/phylum.ts
index 4b34582d6..33376118b 100644
--- a/extensions/phylum.ts
+++ b/extensions/phylum.ts
@@ -443,12 +443,14 @@ export class PhylumApi {
     lockfile: string,
     lockfileType?: string,
     generateLockfiles?: boolean,
+    sandboxGeneration?: boolean,
   ): Promise<Lockfile> {
     return DenoCore.opAsync(
       "parse_lockfile",
       lockfile,
       lockfileType,
       generateLockfiles,
+      sandboxGeneration,
     );
   }