How the Seatbelt Backend Generates TinyScheme Profiles in MXC

The Seatbelt backend translates MXC ExecutionRequest policies into TinyScheme sandbox profiles at runtime through the build_profile function, assembling layered allow/deny rules before applying them via Apple's sandbox_init API.

The Seatbelt backend in the Microsoft MXC (Multi-platform eXecution Container) project provides macOS sandboxing by generating TinyScheme profiles that Apple's Seatbelt security framework can enforce. When you specify containment: "seatbelt" in an execution request, the backend dynamically constructs a sandbox profile string that translates your MXC policy into native macOS sandbox rules. This process involves translating filesystem, network, and execution constraints into Scheme expressions that the sandbox_init system call consumes.

Profile Construction in build_profile

The core generation logic resides in src/backends/seatbelt/common/src/profile_builder.rs, specifically within the build_profile function. This pure-Rust builder transforms an ExecutionRequest into a valid TinyScheme string through a structured assembly process.

Profile Override Handling

Before generating rules, the function checks for experimental.seatbelt.profile_override. If present, the builder returns this raw TinyScheme string verbatim, bypassing all automatic rule generation.

Layered Rule Assembly

When no override exists, the builder constructs the profile in strict order:

  1. Header and Baseline(version 1) and (deny default) establish the deny-by-default security posture
  2. Baseline Allow RulesBASELINE_ALLOW constants permitting essential process operations
  3. System Read-OnlySYSTEM_READ_ALLOW for necessary system library access
  4. PTY AccessTTY_ALLOW rules for terminal interaction
  5. Policy-Derived Allows – Dynamic rules for:
    • Filesystem access (write_filesystem_allow)
    • Network connectivity (write_network_rules)
    • Nested PTY and Keychain access
    • UI isolation when guiAccess is enabled
  6. Policy-Derived Denies – Deny rules for deniedPaths via write_filesystem_deny

The builder uses expand_tilde to resolve home directory paths and quote_scheme to escape strings for Scheme literal safety.

Runner Invocation and Sandbox Application

Once constructed, the profile string passes to SeatbeltScriptRunner in src/backends/seatbelt/common/src/seatbelt_runner.rs. This component handles the actual sandbox instantiation through two distinct launch methods.

Exec Mode

In exec mode (the default), the runner forks a child process and uses Command::pre_exec to invoke sandbox_init with the generated profile before executing /bin/sh -c <script>. This applies the sandbox restrictions immediately before the target script gains control.

Open Mode

When experimental.seatbelt.launch_method is set to "open", the runner instead uses LaunchServices to spawn a helper application (such as Terminal.app) and applies the profile using the sandbox-exec CLI utility, enabling GUI applications to run within the sandbox.

Rule Ordering and Last-Match-Wins Semantics

The generated profile follows Apple's last-match-wins evaluation order. By emitting broad allow rules first and specific deny rules for deniedPaths last, the backend ensures that explicit denials override general allowances. This ordering matches MXC's semantic model across all containment backends, allowing deniedPaths to prevail even when parent directories appear in readwritePaths.

Practical Examples

Generating Profiles Directly in Rust

use wxc_common::models::{ExecutionRequest, Policy, NetworkPolicy};
use seatbelt_common::profile_builder::build_profile;

let request = ExecutionRequest {
    policy: Policy {
        readonly_paths: vec!["/Users/me/project".into()],
        readwrite_paths: vec!["/tmp/output".into()],
        denied_paths: vec!["/Users/me/.ssh".into()],
        default_network_policy: NetworkPolicy::Block,
        allowed_hosts: vec![],
        blocked_hosts: vec![],
        ..Default::default()
    },
    experimental: Default::default(),
    script_code: "echo hello from seatbelt".into(),
    ..Default::default()
};

let profile = build_profile(&request).expect("profile generation");
println!("{}", profile);

Using the TypeScript SDK

import { exec } from "@microsoft/mxc";

await exec({
  containment: "seatbelt",
  process: { commandLine: "echo hi from seatbelt", timeout: 30000 },
  filesystem: {
    readwritePaths: ["/tmp/out"],
    readonlyPaths: ["/Users/me/project"],
    deniedPaths: ["/Users/me/.ssh"]
  },
  network: { defaultPolicy: "block" },
  experimental: {
    seatbelt: {
      launchMethod: "exec",
      guiAccess: false
    }
  }
});

Supplying a Raw Profile Override

{
  "experimental": {
    "seatbelt": {
      "profileOverride": "(version 1)\n(deny default)\n(allow file-read-data (literal \"/usr/bin\"))\n(deny file-read* file-write* (subpath \"/secret\"))"
    }
  }
}

When provided, this TinyScheme string bypasses the automatic rule builder entirely.

Summary

  • The build_profile function in profile_builder.rs constructs TinyScheme profiles by layering allow rules atop a deny-by-default baseline, finishing with specific deny rules for deniedPaths
  • Helper functions expand_tilde and quote_scheme ensure path literals are correctly escaped for Scheme syntax
  • SeatbeltScriptRunner applies profiles via sandbox_init in exec mode or through sandbox-exec in open mode
  • Rule ordering follows Apple's last-match-wins semantics, allowing explicit denials to override broader allowances
  • Users can bypass automatic generation entirely using experimental.seatbelt.profileOverride in the ExecutionRequest

Frequently Asked Questions

What is the difference between exec and open launch methods in the Seatbelt backend?

The exec method forks a new process, applies the TinyScheme profile via the sandbox_init C API using Command::pre_exec, and then executes the target script through /bin/sh. The open method instead uses macOS LaunchServices to spawn a helper application like Terminal.app, applying the sandbox through the sandbox-exec CLI tool. Use exec for command-line tools and open for GUI applications requiring WindowServer access.

How does the Seatbelt backend handle path escaping in TinyScheme profiles?

The builder uses the quote_scheme helper function to escape user-provided paths into valid Scheme string literals. This prevents injection attacks and syntax errors when paths contain spaces, quotes, or other special characters. Additionally, expand_tilde resolves ~ characters to absolute home directory paths before quoting occurs.

Can I use custom TinyScheme profiles with the MXC Seatbelt backend?

Yes. Set experimental.seatbelt.profileOverride to a raw TinyScheme string in your ExecutionRequest. When present, the build_profile function returns this string verbatim without generating baseline, filesystem, or network rules, giving you complete control over the sandbox policy while still using MXC's execution infrastructure.

Why are deny rules emitted last in the generated TinyScheme profile?

Apple's Seatbelt evaluator uses last-match-wins semantics, meaning the final matching rule in the profile determines access. By emitting deniedPaths rules after broader allow rules, the backend ensures that explicit denials take precedence over general filesystem allowances, maintaining consistency with MXC's security model across all platforms.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →