Skip to content

feat(mxc): hide paths inside filesystem grants with driver_config.mxc.denied_paths #5032

Closed

Description

@jquesnelle

User Story

I'm integrating OpenShell into Hermes Agent at Nous Research, working with the OpenShell team. A Hermes user can switch a profile into an OpenShell sandbox from the desktop app: MXC on Windows, Docker on Linux. The sandboxed Hermes runs against the user's real profile folder, so its sessions and memories carry over when the sandbox is switched off.

That folder also holds a .env with API keys and an auth.json with tokens. OpenShell providers give the agent placeholder keys, but the real files sit inside the folder's read-write grant, so code in the sandbox can read the keys or rewrite the files. On Linux we cover those files with empty read-only mounts. On Windows, MXC has deniedPaths for this, and the MXC driver always sends it an empty list.

Without a way to deny paths inside a grant, we can't ship the Windows sandbox with credentials kept on the host.

Problem Statement

On the windows branch, an MXC sandbox can read system locations such as C:\Windows without a grant, but it reaches user files only through its policy's read_write and read_only grants, and a grant covers everything below the granted directory. There is no way to exclude a file or directory inside a grant: the sandbox policy has no such field, and the MXC driver's caller config block, driver_config.mxc, accepts only command and cwd.

MXC supports exclusions itself through filesystem.deniedPaths, and the MXC driver already builds that field when it starts the ProcessContainer, but it always sends an empty list (denied_paths: Vec::new() in crates/openshell-driver-mxc/src/driver.rs).

Impact / Why This Matters

An agent's working directory often holds files the agent must not read or replace, for example a profile's .env with API keys or an auth.json with tokens. With a read-write grant on that directory, the workload can read those files, overwrite them, and create them when they are absent, and the host process that owns the directory loads them on its next start.

Measured on windows 5d1bfbf with MXC 1.0.1, using only an OpenShell gateway and the openshell CLI: a sandbox whose policy grants work\ read-write runs type profile\.env, which prints the file; echo overwritten> profile\.env, which replaces it on the host; and echo created> profile\auth.json, which creates the file on the host.

The workarounds do not cover this case:

  • Granting each sibling path separately leaves out files created later, and the workload can no longer create files in the directory itself.
  • Moving the credential files out of the working directory is not possible for tools with a fixed layout.
  • Other drivers can cover a path with a mount through their own driver config. MXC has no mount primitive.

Proposed Design

Add an optional denied_paths list to the MXC driver's driver_config.mxc block, next to command and cwd. The driver passes it to MXC as filesystem.deniedPaths.

$config = '{"mxc":{"command":["C:\\Windows\\System32\\cmd.exe","/d","/c","agent.cmd"],"cwd":"C:\\work","denied_paths":["C:\\work\\profile\\.env","C:\\work\\profile\\auth.json"]}}'
openshell sandbox create --name agent --policy policy.yaml --driver-config-json $config --detach
  • The workload cannot read or write a denied file, list a denied directory, or create a denied path, including inside a grant. Other paths in the grant keep their access.
  • A denied file's name, size, timestamps and attributes remain visible to the workload. A deny covers the listed path: a hard link to the same file, created on the host before launch at a path the workload can read, still exposes the contents.
  • A listed path may be absent; the workload then cannot create it. Its parent directory must exist, because MXC refuses to start the ProcessContainer otherwise. To cover a missing directory, the caller denies the directory itself; the error names it.
  • Each entry must be an absolute path that starts with a drive letter. An entry that equals or contains a filesystem grant is rejected, because MXC removes a grant that equals a denied path and keeps access to a grant nested inside a denied directory; neither hides part of a grant. MXC matches paths after resolving 8.3 short names and junctions and ignoring letter case, so the driver compares the resolved paths.
  • Invalid entries fail sandbox create before anything is provisioned, with an error that names the entry.
  • When the list is not empty, the driver sends MXC fallback.allowDaclMutation: false, so on a host whose base container cannot enforce the denies, the launch fails and MXC does not add deny entries to host file ACLs.
  • The list is empty by default, is fixed at creation, and needs allow_driver_config = true like the rest of the mxc block. It only removes access.

Why driver config and not the sandbox policy: see the first alternative below.

Acceptance Criteria

  • A sandbox created with driver_config.mxc.denied_paths listing a file inside a read-write grant cannot read or overwrite that file, and another file in the same grant stays readable.
  • A listed path that does not exist cannot be created by the workload.
  • sandbox create fails before provisioning, naming the entry, when an entry is not an absolute path with a drive letter, equals or contains a filesystem grant (also through an 8.3 short name, a junction or another letter case), or has no existing parent directory.
  • A request without denied_paths produces the same MXC request as today.
  • The MXC driver README and the gateway configuration page document the field and what stays visible to the workload.

Alternatives Considered

  • A portable filesystem_policy field with a driver capability, as feat(policy): add portable UI policy controls #3221 does for UI controls.
    feat(policy): add portable UI policy controls #3221 adds UI controls as a portable policy section that only MXC enforces; drivers without the
    capability reject it. The windows branch already carries it (SandboxPolicy.ui, openshell.policy.ui.v1). A deny list could take the same shape. It is proposed in driver config for three reasons:

    • RFC 0006 assigns driver-owned meaning to the driver's block: "The selected driver owns the nested schema, validation, safety policy, and compatibility behavior for its config block." The rules above (what MXC does with a deny equal to or containing a grant, the parent requirement, the alias resolution) are MXC's.
    • Only MXC can enforce a deny inside a grant. The Linux drivers enforce filesystem policy with Landlock, which grants access to path hierarchies and has no rule that removes a path below a grant, so a portable field would have no meaning there beyond being rejected. Docker and Podman already hide paths through their own driver_config.<driver>.mounts.
    • UI controls describe behavior other platforms can implement; a deny inside a grant has no portable semantics today.

    Driver config has costs: the denies sit behind the allow_driver_config opt-in (every MXC sandbox already needs it for cwd), and policy get and policy hashing do not show them. If maintainers prefer the feat(policy): add portable UI policy controls #3221 shape, the field can move to filesystem_policy with a capability; the validation and the MXC mapping stay the same.

  • Narrower grants. Described above; they cannot cover files created later.

  • Middleware, interceptors and providers. Middleware and interceptors act on network traffic and gateway requests, not on file access inside the ProcessContainer. Providers keep credentials out of the sandbox environment, but do not cover files that an existing tool keeps in its working directory.

Agent Investigation

  • MxcSandboxConfig in crates/openshell-driver-mxc/src/driver.rs uses deny_unknown_fields, so today a request with denied_paths fails with invalid mxc driver_config: unknown field `denied_paths`, expected `command` or `cwd` .
  • oneshot_config_json in crates/openshell-driver-mxc/src/mxc.rs already emits filesystem.deniedPaths when the list is non-empty.
  • MXC 1.0.1 behavior, measured with wxc-exec one-shot ProcessContainers on Windows 11 build 26340.9767 (--probe: tier base-container, baseContainerSupportsDenyPaths: true):
    • a denied file inside a read-write grant cannot be read, overwritten, appended to or copied; a sibling file stays readable and new files can be created next to it;
    • an absent denied file or directory cannot be created;
    • a denied path whose parent is missing, or whose parent is a file, stops the launch with CreateProcessSecurityEnvironment failed (HRESULT = 0x80070003);
    • a denied path equal to a read-write grant removes the whole grant, also when it names the grant in another letter case (ASCII or not), through an 8.3 short name, or through a host junction;
    • a denied directory that contains a read-write or read-only grant leaves that grant's access in place, also when it is spelled with an 8.3 short name;
    • the workload still sees a denied file's name, size, timestamps and attributes (directory listing, file metadata), and reads its contents through a hard link made on the host;
    • spellings the workload chooses do not get around a deny: 8.3 names, letter case, ::$DATA, a trailing dot, \\?\, \\.\, GLOBALROOT and volume GUID paths, and a symbolic link the workload creates are all refused.
  • By reading MXC 1.0.1's tier selection (fallback_detector.rs, choose_backend_tier): MXC enforces deniedPaths in the base-container tier only when the OS reports native deny support; otherwise it falls back to a tier that adds deny entries to host file ACLs, which MXC 1.0 allows by default (fallback.allowDaclMutation defaults to true). The network ingress the MXC driver requests today is served only in the base-container tier, so MXC already refuses such a launch; sending allowDaclMutation: false with denies keeps that refusal if the ingress request changes.

Checklist

  • I've reviewed existing issues and the published docs
  • This is a design proposal, not a "please build this" request

Activity

  1. jquesnelle commented on Oct 30, 2026

    @jquesnelle
    Author

    Correction to the text above: a workload cannot create an absent denied path by name, but MXC 1.0.1 and 1.0.0 don't check the target of a rename against deniedPaths, so in a writable directory the workload can create an absent denied path, or replace a denied file, by renaming its own file onto the name. It still can't read a denied file's original contents. Denying the directory that holds the files prevents it. Details and the updated rules are in #5033.

  2. shailendra-nv commented on Oct 30, 2026

    @shailendra-nv
    Collaborator

    @jquesnelle - Minor comments on issue's acceptance criteria. It states that

    First checkbox: “cannot read or overwrite that file”
    Second checkbox: “A listed path that does not exist cannot be created by the workload.”

    However, with current change the sandbox can still put a file at a filename already on deny list. This doesn't have any security implication but rather integrity issue. So, I am ok with the overall change. Can you please update the issue acceptance criteria with something like

    "Direct reads, writes, and creation are blocked. However, when the parent directory is writable, renaming another file onto the denied path can replace an existing file or create an absent one."

    Example Seq. of issue:

    1. .env is denied.
    2. The workload creates replacement.tmp containing its own data.
    3. It renames that file over .env.
    4. The host’s .env now contains the replacement data, although the workload still cannot read it through the denied path.
      That is a file-integrity limitation. It could matter if the host later trusts that file, but this PR does not introduce the underlying write capability—the directory was already writable.
  3. added
    state:acceptedA maintainer decided OpenShell should pursue this issue
    and removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Oct 30, 2026
  4. added a commit that references this issue on Oct 30, 2026
    7bc5754
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

state:acceptedA maintainer decided OpenShell should pursue this issue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions