MXC - Microsoft's sandbox for code you shouldn't trust!

But 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:

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

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: