Policy and locked config
Before this shipped, a repo-local config overrode the user's own settings, and nothing overrode either. There was no scope above the person at the keyboard, so an organisation could not state "isolation is on and you may not turn it off."
Two changes fixed that: a scope above the user, and consent before executing a config that is code.
Precedence
Scopes merge in this order, later winning, except for keys the system scope has locked:
system /etc/moxxy/config.yaml
%PROGRAMDATA%\moxxy\config.yaml
$MOXXY_SYSTEM_CONFIG
▼
user ~/.moxxy/config.yaml
▼
project ./moxxy.config.{yaml,yml,ts,js,mjs,cjs} (searched upward)
▼
explicit --config <path>
▼
flags per-invocationmoxxy config path prints which files are actually in effect on a machine.
Locked keys
The system scope may carry a locked: list of dot-paths. Those paths are stripped out of the user, project and explicit layers before the merge, so a lower layer cannot set them at all, and an attempted override is logged.
security:
enabled: true
strict: true
plugins:
isolator:
default: subprocess
locked:
- security.enabled
- security.strict
- plugins.isolator
- auditA locked path can name a leaf (security.enabled) or a subtree (audit, which locks everything under it).
Lock deliberately
Lock what a user must not be able to switch off. Everything else — the model, the theme, the density, which channels they use — is a preference, and locking preferences generates support tickets without improving your posture.
The system scope is YAML only. An executable file at a root-owned path would run as whoever starts moxxy, which is a worse hole than the one this feature closes.
Executable project configs
moxxy.config.ts is code. It is executed with your full privileges before the permission engine, the vault or any isolator exists, and the project search walks upward through parent directories. That meant cloning a repository and typing moxxy inside it silently ran that repository's config.
moxxy now asks first, and records the approval against the file's contents, so editing the file asks again.
moxxy config trust # approve ./moxxy.config.ts
moxxy config trust ./path/to.ts # approve a specific file
moxxy config trust --list # what is approved
moxxy config untrust <file> # withdraw approvalNon-interactive runs (CI, daemons, containers) skip an unapproved executable config rather than executing unreviewed code. Pre-approve with moxxy config trust when building an image.
To forbid them outright across an estate:
config:
allowExecutable: false
locked:
- config.allowExecutableYAML configs are unaffected by any of this. They are data.
Behaviour change
A project moxxy.config.ts that used to load silently now requires one-time approval.
Permission policy
Permission rules are policy too, and they follow the same idea. Config-supplied rules apply as a managed layer rather than being copied into the user's writable ~/.moxxy/permissions.json, so a rule pushed from the system scope does not become something the user can simply delete.
permissions:
deny:
- name: Write
inputPathPrefix: { path: /etc }
reason: system files are managed by configuration
- name: Bash
inputGlob: { command: '* curl *' }
allow:
- name: Read
inputPathPrefix: { path: /srv/app }
- name: Bash
inputGlob: { command: 'git *' }
locked:
- permissionsOrder is: managed deny, file deny, managed allow, file allow. The managed layer is checked first on purpose — checking the user's file first would let an "allow always" answer, which the agent itself can provoke, silently defeat a rule the operator pushed.
Prefer the anchored matchers
Three matchers exist, and for operator-written policy only two of them are safe:
| Matcher | Semantics |
|---|---|
inputPathPrefix | Anchored, path-aware. /srv/app matches /srv/app/x, not /srv/apple. Paths are normalised first, so .. cannot walk out. |
inputGlob | Anchored whole-value. * within a segment, ** across, ? one character. |
inputMatches | Raw regex, substring match, unanchored. |
inputMatches is unanchored by deliberate, documented contract: existing user files depend on it and silently anchoring would break them. That makes it a trap in an org policy. The obvious { path: '/etc' } reads like "under /etc" and actually means "contains /etc anywhere", which over-blocks as a deny rule and grants far more than intended as an allow rule.
Use inputPathPrefix and inputGlob when you are writing rules other people will live under.
See permissions for the full engine.
Next
- Deploying to a fleet
- Identity — the subject a policy decision is about
- The audit trail — proving the policy held