diff --git a/StabilityMatrix/PyVenvRunner.cs b/StabilityMatrix/PyVenvRunner.cs
new file mode 100644
index 00000000..d30d8277
--- /dev/null
+++ b/StabilityMatrix/PyVenvRunner.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace StabilityMatrix;
+
+///
+/// Python runner using a subprocess, mainly for venv support.
+///
+public class PyVenvRunner: IDisposable
+{
+ public Process? Process { get; private set; }
+ private ProcessStartInfo StartInfo => new()
+ {
+ FileName = PythonPath,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ CreateNoWindow = true
+ };
+ public string RootPath { get; private set; }
+
+ public event EventHandler OnStdoutUpdate;
+
+ public PyVenvRunner(string path)
+ {
+ RootPath = path;
+ OnStdoutUpdate += (_, _) => { };
+ }
+
+ // Whether the activate script exists
+ public bool Exists() => System.IO.File.Exists(RootPath + "Scripts/activate");
+
+ ///
+ /// The path to the python executable.
+ ///
+ public string PythonPath => RootPath + @"\Scripts\python.exe";
+
+ ///
+ /// Configures the venv at path.
+ ///
+ public async Task Setup(bool existsOk = false)
+ {
+ if (!existsOk && Exists())
+ {
+ throw new InvalidOperationException("Venv already exists");
+ }
+
+ // Create RootPath if it doesn't exist
+ if (!System.IO.Directory.Exists(RootPath))
+ {
+ System.IO.Directory.CreateDirectory(RootPath);
+ }
+
+ await PyRunner.Exec(@"
+import venv
+venv.EnvBuilder(with_pip=True).create(r""" + RootPath + @""")
+ ");
+ }
+
+ public void RunDetached(string arguments)
+ {
+ this.Process = new Process();
+ Process.StartInfo = StartInfo;
+ Process.StartInfo.Arguments = arguments;
+
+ // Bind output data event
+ Process.OutputDataReceived += OnProcessOutputReceived;
+
+ // Start the process
+ Process.Start();
+ Process.BeginOutputReadLine();
+ }
+
+ ///
+ /// Called on process output data.
+ ///
+ private void OnProcessOutputReceived(object sender, DataReceivedEventArgs e)
+ {
+ var data = e.Data;
+ if (!string.IsNullOrEmpty(data))
+ {
+ OnStdoutUpdate?.Invoke(this, data);
+ }
+ }
+
+ public void Dispose()
+ {
+ Process?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
\ No newline at end of file