MXC - Microsoft's sandbox for code you shouldn't trust!
14 Oct 2026But you’re going to run anyway!
Every .NET developer knows this one, even if we don’t like to say it out loud. A NuGet package you pull in runs with the same permissions as your process. It gets your files, your network and your environment variables. We’ve lived with that for twenty years and call it “dependency management”.
Then AI agents showed up and we went one step further: we now let model output run commands on our machines. What could go wrong? A lot.
I’ve tried to solve this a couple of ways, with .NET libraries inside WebAssembly and with custom AssemblyLoadContext implementations. Both work, but neither is pleasant to adopt. You get different toolchains, extra build steps and a runtime boundary you have to design around. Sandboxing that’s hard to adopt doesn’t get adopted.
That’s why MXC (Microsoft eXecution Container) caught my eye. It’s open source (MIT), went GA on Windows 11 at the beginning of October 2026, and powers the local sandboxing in GitHub Copilot CLI. It takes the opposite approach: stay close to your code, with one SDK and a few lines of policy. It’s very new, so most of what follows comes from the project’s own documentation.
What it is
MXC is a library you build into your own application to run untrusted code (model output, plugins, tools) in a sandbox on Windows, Linux and macOS. It isn’t an agent platform, and installing it doesn’t make your machine safe. Your application decides what to run and under which rules. MXC enforces them.
The core is written in Rust, with Rust, .NET (Microsoft.Mxc.Sdk on NuGet) and Node SDKs on top. There’s also a raw executor (wxc-exec.exe) that takes JSON requests, for testing or for cases where you can’t embed the SDK. OpenAI Codex, GitHub Copilot, OpenClaw, Replit, LM Studio, NVIDIA OpenShell and Unsloth AI all support it, which makes sense when none of them wants to build a sandbox per OS.
The flow is simple. Your app builds a versioned request (command, container type, containment rules), MXC validates it and picks a backend, and the workload runs isolated from the rest of the machine. It’s the same idea as the Chromium sandbox: don’t trust the component that handles untrusted input, and limit what it can touch.
Backends: it uses what the OS gives you
| Platform | Default backend | Alternatives |
|---|---|---|
| Windows 11 (x64 / ARM64) | processcontainer |
windows_sandbox*, wslc, microvm*, hyperlight*, isolation_session |
| Linux (x64 / ARM64) | bubblewrap |
lxc, microvm, hyperlight |
| macOS (ARM64 / x64) | seatbelt |
none |
* experimental
Isolation runs from an OS-level process sandbox up to a full VM, all behind one model. The guarantees are not identical across platforms. A process container is not a Hyper-V boundary, and the same policy on bubblewrap and on Windows (process container plus WFP for network, default-deny outbound) behaves differently. Check what you actually get on your target platform.
The policy is where security happens
Policy covers the filesystem (read-only, read-write, denied paths), the network (proxy and outbound controls) and the UI (clipboard, display, GUI). In C# it’s typed objects. This small demo starts from deny:
using Microsoft.Mxc.Sdk;
using Microsoft.Mxc.Sdk.V1;
var work = Directory.CreateTempSubdirectory("mxc-demo").FullName;
try
{
File.WriteAllText(Path.Combine(work, "input.txt"), "hello from the host\n");
var request = new ContainerRequest(
"sh -c \"cat input.txt; echo x > blocked.txt && echo WRITE-OK || echo write-denied; " +
"curl -s -m 3 https://example.com >/dev/null && echo NET-OK || echo net-denied\"")
{
Containment = new Containment.Process(),
Filesystem = new FilesystemPolicy { ReadonlyPaths = { work } },
Network = new NetworkPolicy
{
Egress = new NetworkEgressPolicy { Default = NetworkAction.Deny },
},
WorkingDirectory = work,
TimeoutMs = 30_000,
};
try
{
var result = await MxcContainer.RunAsync(request);
Console.Write(result.Stdout);
Console.Error.Write(result.Stderr);
foreach (var warning in result.Warnings)
{
Console.Error.WriteLine($"warning: {warning}");
}
Console.WriteLine($"exit={result.ExitCode} timedOut={result.TimedOut}");
}
catch (MxcException error)
{
Console.Error.WriteLine($"MXC error [{error.Code}]: {error.Message}");
return 1;
}
return 0;
}
finally
{
Directory.Delete(work, recursive: true);
}
The workload can read the one directory we hand it, can’t write to it, and can’t reach the network. The sh: line is stderr, which the code writes after stdout, so it appears last:
hello from the host
write-denied
net-denied
sh: 1: cannot create blocked.txt: Read-only file system
exit=0 timedOut=False
exit=0 is the shell’s exit code. The sandbox did its job, and the workload just didn’t get what it asked for. (The command is Linux-flavored. On Windows, use a cmd.exe equivalent.) A sandbox is only as strong as its policy. Give it read-write on your whole home directory and you’ve built a very elegant container with nothing inside it.
Containing parts of your app from .NET
MXC contains processes, not code inside your process. From .NET, your app orchestrates contained child workloads. You can’t fence off a single method.
Modern .NET has no in-process isolation (no CAS, no AppDomains), so you push the risky part out of process and contain that. Here are two patterns I’d use:
- Self-relaunch worker. Start your own app via
Environment.ProcessPathwith a worker flag inside MXC and talk to it over stdio. Parsing untrusted files, deserializing third-party data and loading plugins then all happen contained. It’s the closest you get to “sandbox this method”. - Contained tool calls. With Agent Framework and/or Semantic Kernel, send every shell or code-exec tool call through
MxcContainer.RunAsyncwith a per-tool policy instead ofProcess.Start. Each tool gets only the paths and hosts it needs.
What if my app does not work inside the sandbox? (Windows)
At first, it won’t, and the documentation says so. We’ve all seen the sandbox that was “temporarily” disabled in week two and stayed that way until the pentest.
On Windows (ProcessContainer) you can ask MXC to record every denied access:
var request = new ContainerRequest(command)
{
Containment = new Containment.ProcessContainer
{
CaptureDenials = new CaptureDenialsPolicy(),
},
Filesystem = new FilesystemPolicy { ReadonlyPaths = { inputDir } },
TimeoutMs = 30_000,
};
var result = await MxcContainer.RunAsync(request);
foreach (var warning in result.Warnings)
{
Console.Error.WriteLine($"warning: {warning}");
}
if (result.OutputMetadata?.CaptureDenialsError is { } error)
{
throw new InvalidOperationException(error.Message);
}
var capture = result.OutputMetadata?.CaptureDenials;
Console.WriteLine($"captured {capture?.TotalDenials} denial(s) in {capture?.OutputPath}");
By default (CaptureDenialsMode.Block) denials are recorded and enforced. You get a report of what to add to your policy, and the sandbox stays intact. That’s the mode to use in an application.
CaptureDenialsMode.Allow records accesses and lets them through, so nothing is enforced. Use it only to learn what your own trusted tool needs, and never with untrusted code. Then write the policy, switch back to Block and test again. Check BackendCapability.CaptureDenials to see whether the current host supports it (Windows only).
This could also work as a feedback loop for Interactive Application Security Testing (IAST). Run an instrumented application in Allow mode on a test machine and see which files it touches and which external addresses it reaches out to.
What I would check before using it
- Threat model first. What is the untrusted input, and what’s the worst the workload can do with the rights you give it?
- Stay on the default backend unless you know why you need another. The experimental ones are marked experimental for a reason.
- Test the denials. Try to read a denied path and reach a blocked host, and make sure both fail.
- Expect uneven visibility. Only Windows process containers produce an agent activity report. Session containers, WSLc and microVMs don’t.
- Telemetry is off unless the run opts in, the user consents and admin policy allows it. Local open-source builds send nothing to Microsoft. Verify that in your own build.
- It’s one layer. A sandbox reduces blast radius, but it doesn’t make bad code good. You still need least privilege, review and monitoring.
Conclusion
I’m skeptical by profession, and “new security sandbox” usually sends me looking for the part where it breaks. Still, the approach is sound: deny by default, policy as data, native OS isolation per platform, and honest documentation about the limits. If you’re building agents or plugin systems, it’s a good first step. It still needs least privilege, review and monitoring around it.
Documentation and source: https://github.com/microsoft/mxc
Further reading: