Repository navigation
feat(mxc): hide paths inside filesystem grants with driver_config.mxc.denied_paths #5032
Description
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 createbefore 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 = truelike the rest of themxcblock. 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_pathslisting 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 createfails 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_pathsproduces 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_policyfield 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. Thewindowsbranch 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_configopt-in (every MXC sandbox already needs it forcwd), andpolicy getand policy hashing do not show them. If maintainers prefer the feat(policy): add portable UI policy controls #3221 shape, the field can move tofilesystem_policywith 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
MxcSandboxConfigincrates/openshell-driver-mxc/src/driver.rsusesdeny_unknown_fields, so today a request withdenied_pathsfails withinvalid mxc driver_config: unknown field `denied_paths`, expected `command` or `cwd`.oneshot_config_jsonincrates/openshell-driver-mxc/src/mxc.rsalready emitsfilesystem.deniedPathswhen the list is non-empty.- MXC 1.0.1 behavior, measured with
wxc-execone-shot ProcessContainers on Windows 11 build 26340.9767 (--probe: tierbase-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,\\?\,\\.\,GLOBALROOTand 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 enforcesdeniedPathsin 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.allowDaclMutationdefaults 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; sendingallowDaclMutation: falsewith 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
jquesnelle commentedon Oct 30, 2026
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.shailendra-nv commentedon Oct 30, 2026
@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:
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.
feat(mxc): pass driver_config denied_paths to MXC deniedPaths (#5033)