> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/openshell/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/openshell/_mcp/server.

# Customize Sandbox Policies

> Apply, iterate, and debug sandbox network policies with hot-reload on running OpenShell sandboxes.

Use this page to apply and iterate policy changes on running sandboxes. For a full field-by-field YAML definition, use the [Policy Schema Reference](/reference/policy-schema).

## Policy Structure

A policy has static sections `filesystem_policy`, `landlock`, and `process` that are locked at sandbox creation, and dynamic `network_policies` and `network_middlewares` sections that are hot-reloadable on a running sandbox.

```yaml wordWrap showLineNumbers={false}
version: 1

# Static: locked at sandbox creation. Paths the agent can read vs read/write.
filesystem_policy:
  include_workdir: true
  read_only: [/usr, /lib, /etc]
  read_write: [/tmp]

# Static: Landlock LSM kernel enforcement. best_effort uses highest ABI the host supports.
landlock:
  compatibility: best_effort

# Static, optional: override the identity selected by the compute driver.
# process:
#   run_as_user: "1500"
#   run_as_group: "1500"

# Dynamic: hot-reloadable. Named blocks of endpoints + binaries allowed to reach them.
network_policies:
  my_api:
    name: my-api
    endpoints:
      - host: api.example.com
        port: 443
        protocol: rest
        enforcement: enforce
        access: full
    binaries:
      - path: /usr/bin/curl

# Dynamic: ordered middleware selected independently by admitted host.
network_middlewares:
  regex-redactor:
    name: Redact API tokens
    middleware: openshell/regex
    order: 10
    config:
      mode: redact
    on_error: fail_closed
    endpoints:
      include: ["api.example.com"]
      exclude: []

```

Static sections are locked at sandbox creation. Changing them requires destroying and recreating the sandbox.
Dynamic sections can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting.
When a hot reload changes rules, the supervisor publishes a new policy generation and closes connections pinned to the previous generation. This includes HTTP keep-alive tunnels, `tls: skip`, non-HTTP payloads, HTTP upgrades such as WebSocket, and long-lived response streams such as SSE. Most clients reconnect automatically, and the next connection or request is evaluated against the current policy. A parsed WebSocket relay closes with code `1012` when its attached policy generation becomes stale. Use `protocol: websocket` when policy should stay attached to the RFC 6455 upgrade and client text messages after the allowed upgrade. Provider-credentialed endpoints reject L4-only and `tls: skip` modes by default. On a credentialed WebSocket upgrade, OpenShell keeps the connection on the parsed relay and rejects binary frames unless the endpoint explicitly sets `allow_uninspected_credentials: true`. Add `websocket_credential_rewrite: true` when the relay should rewrite credential placeholders in client-to-server WebSocket text messages. Add `request_body_credential_rewrite: true` only on inspected REST endpoints that need OpenShell to rewrite placeholders in supported text request bodies.

| Section               | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filesystem_policy`   | Static  | Controls which directories the agent can access on disk. Paths are split into `read_only` and `read_write` lists. Any path not listed in either list is inaccessible. Set `include_workdir: true` to automatically add the agent's working directory to `read_write`. [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforces these restrictions at the kernel level.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `landlock`            | Static  | Configures Landlock LSM enforcement behavior. Set `compatibility` to `best_effort` (skip individual inaccessible paths while applying remaining rules) or `hard_requirement` (fail if any path is inaccessible or the required kernel ABI is unavailable). Refer to the [Policy Schema Reference](/reference/policy-schema#landlock) for the full behavior table.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `process`             | Static  | Optionally overrides the OS-level identity for the agent process. Explicit values must be `sandbox` or numeric UID/GID values from `1` through `4294967294`; root and the invalid identity sentinel are rejected. Docker and Podman may use named identities through per-field OCI `USER` fallback; Kubernetes uses its platform-selected numeric identity. The agent also runs with seccomp filters that block dangerous system calls.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `network_policies`    | Dynamic | Controls outbound traffic from the sandbox, including native model-provider endpoints. Each block has a name, a list of endpoints (host, port, protocol, and optional rules), and a list of binaries allowed to use those endpoints. Every outbound connection passes through the network supervisor, which queries the [policy engine](/about/how-it-works#core-components) with the destination and calling binary. A connection is allowed only when both match an entry in the same policy block. Attached provider profiles can contribute endpoint and binary entries to the effective policy. For endpoints with `protocol: rest`, the proxy auto-detects TLS and terminates it so each HTTP request can be checked against that endpoint's `rules` (method and path). For endpoints with `protocol: websocket`, the proxy validates the RFC 6455 upgrade and evaluates `GET` rules for the handshake plus either `WEBSOCKET_TEXT` rules for raw client text messages or GraphQL operation rules for GraphQL-over-WebSocket messages. Set `websocket_credential_rewrite: true` only when a WebSocket or REST compatibility endpoint must keep placeholder credentials in sandbox-owned text frames and resolve them at the OpenShell relay boundary. Endpoints with `protocol: tcp` allow ordinary DNS resolution and native TCP connections without inspecting payloads. Endpoints without `protocol` retain L4 passthrough through an explicit proxy. If no endpoint matches, the connection is denied. |
| `network_middlewares` | Dynamic | Declares keyed HTTP and WebSocket middleware configs. After network and L7 policy admit a request or upgrade, OpenShell matches each config's host selectors independently and runs matching entries by their unique ascending `order` before credential injection. WebSocket-capable entries continue on complete client text messages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |

When REST body credential rewriting is disabled, OpenShell forwards placeholder
text unchanged if trusted metadata identifies an unknown key or a valid credential
with a current binding, including to the destination itself. For example, an
OpenRouter placeholder printed by `printenv` can remain in an OpenRouter
conversation, just like an unknown documentation example or a GitHub placeholder.
OpenShell preserves the body bytes and does not substitute the secret. Header
credential resolution remains separate: the same placeholder can resolve in an
authentication header while remaining literal text in the body.

Known invalid or revoked references and unavailable classification
metadata also fail closed. The local HTTP 403 response uses the error code
`credential_placeholder_in_request_body`. Remove the offending reference from
conversation history before retrying; if metadata is unavailable, restore provider
access first. Do not enable body rewriting or `allow_uninspected_credentials` just
to forward conversation text. Malformed placeholder candidates and candidates
longer than 4096 wire bytes are denied. Authentication headers and HTTP trailers
retain their credential restrictions.

## Supervisor Middleware

Supervisor middleware can inspect, deny, or replace admitted HTTP request bodies and client WebSocket text messages before provider credentials are injected. Middleware selection is independent of the `network_policies` rule that admitted the traffic: each keyed `network_middlewares` entry matches the destination host through `endpoints.include` and `endpoints.exclude`.

```yaml
network_middlewares:
  regex-redactor:
    name: Redact API tokens
    middleware: openshell/regex
    order: 10
    config:
      mode: redact
    on_error: fail_closed
    endpoints:
      include: ["*.example.com"]
      exclude: ["trusted.example.com"]
```

Matching entries run once each by ascending `order`; lower values run first, and duplicate order values are rejected. The default order is `0`, so policies with multiple entries normally set it explicitly. Map keys are structurally unique. An optional `name` provides a human-readable label, defaults to the map key, and does not change the key used as the config identity. Different keys may use the same implementation and run as distinct stages. `exclude` takes precedence over `include`.

`openshell/regex` is an example built into the supervisor. It applies fixed regular expressions to UTF-8 HTTP request bodies and complete client-to-upstream WebSocket text messages before credential injection. The initial pattern recognizes `sk-` tokens. This is a best-effort text transformation without guarantees that sensitive values will be detected or fully removed; it does not inspect binary or upstream-to-client WebSocket messages. Custom expressions are not configurable yet. Operator-run middleware must be registered by name before a policy can reference it. The gateway validates implementation-owned config before accepting the policy.

`on_error` defaults to `fail_closed`. Use `fail_open` only when skipping a selected stage that fails is acceptable. For WebSocket streams, a broken fail-open stage is disabled for the rest of that connection and OpenShell emits a state-change finding. A host-matched attachment joins only operation chains advertised by its implementation. An HTTP-only attachment may inspect the WebSocket upgrade GET, but post-upgrade messages pass with informational `binding_not_selected` coverage under either error mode. Binary messages also pass without middleware inspection and produce `unsupported_message_type` coverage for active WebSocket stages. Upstream-to-client messages remain uninspected. Policy validation rejects a fail-closed selector that can cover a `tls: skip` endpoint. An all-`fail_open` match may cover the endpoint; the supervisor bypasses the middleware and emits a detection finding.

See [Supervisor Middleware](/extensibility/supervisor-middleware) for registration, chain ordering, body limits, failure behavior, and operations.

## Baseline Filesystem Paths

When a sandbox runs in proxy mode (the default), OpenShell automatically adds baseline filesystem paths required for the sandbox child process to function: `/usr`, `/lib`, `/etc`, and `/var/log` (read-only), plus `/tmp` (read-write). When `filesystem.include_workdir` is `true`, OpenShell also adds the resolved working directory as read-write. Paths like `/app` are included in the baseline set but are only added if they exist in the container image.

For GPU sandboxes, OpenShell also adds existing GPU device nodes as read-write paths. CUDA workloads require write access to procfs for thread metadata, so GPU baseline enrichment moves `/proc` from read-only to read-write when GPU devices are present.

This filtering prevents a missing baseline path from degrading Landlock enforcement. Without it, a single missing path could cause the entire Landlock ruleset to fail, leaving the sandbox with no filesystem restrictions at all.

User-specified paths in your policy YAML are not pre-filtered. If you list a path that does not exist:

* In `best_effort` mode, the path is skipped with a warning and remaining rules are still applied.
* In `hard_requirement` mode, sandbox startup fails immediately.

This distinction means baseline system paths degrade gracefully while user-specified paths surface configuration errors.

## Allow Native TCP Connections

Use `protocol: tcp` when an application needs to resolve a policy-approved hostname and open a normal TCP connection without configuring an HTTP proxy. The endpoint remains L4-only, so OpenShell authorizes the hostname, port, and calling binary but does not inspect application payloads.

```yaml showLineNumbers={false}
network_policies:
  postgres:
    name: postgres
    endpoints:
      - host: db.internal.example
        port: 5432
        protocol: tcp
    binaries:
      - path: /usr/bin/psql
```

OpenShell answers DNS only for hostnames eligible under an active `protocol: tcp` endpoint. It validates upstream answers against destination and SSRF controls, returns a supervisor-owned synthetic address, and records the validated real addresses. When the application connects to the synthetic address, OpenShell recovers the hostname and port, evaluates the calling process against the current policy generation, and dials only an address pinned by that DNS result.

Treat that hostname as a connection-routing constraint, not an application-authority boundary. OpenShell does not inspect TLS SNI, HTTP `Host`, or another protocol-level destination inside a `protocol: tcp` stream. If the approved hostname reaches compatible shared infrastructure, a client may be able to select another tenant, virtual host, or service behind the same front door. Use native TCP only when the trust boundary includes every destination that the shared infrastructure can expose; use an inspected protocol when application authority must remain constrained.

Applications must honor the returned DNS TTL and resolve the hostname again before reconnecting after that TTL expires. A client that caches the synthetic address indefinitely can receive a connection failure after the mapping expires. Docker and Podman currently advertise only IPv4 egress for this feature, so OpenShell returns an empty successful answer for AAAA queries and lets dual-stack clients use the working A record.

DNS resolution does not authorize a connection by itself. Unknown names, wrong ports, stale mappings, disallowed destination addresses, and binaries outside the matching policy fail closed. Applications cannot inherit access by connecting directly to a real IP returned by an upstream resolver.

Do not combine `protocol: tcp` with L7-only fields such as `path`, `enforcement`, `access`, `rules`, `deny_rules`, request rewriting, or credential signing. Docker and Podman sandboxes support policy DNS and transparent TCP capture. Other compute drivers reject policies containing `protocol: tcp` until they provide the required runtime capability.

Prefer exact hostnames for TCP endpoints. A wildcard authorizes DNS queries for every matching name, which means a compromised process can encode data into matching DNS labels even when the lookup does not return an address. OpenShell records failed eligible lookups for operators, but logging does not remove that exfiltration channel.

The first TCP endpoint is a deliberate exception to ordinary dynamic network policy updates. A sandbox created without any `protocol: tcp` endpoint does not install the DNS and transparent-capture substrate. A hot reload that introduces the first TCP endpoint is rejected atomically and leaves the previous policy active; recreate the sandbox with a TCP endpoint to install the substrate. A sandbox that started with TCP support can remove and later re-add TCP endpoints through normal policy reloads.

## Apply a Custom Policy

Pass a policy YAML file when creating the sandbox:

```shell
openshell sandbox create --policy ./my-policy.yaml -- claude
```

The trailing command is the sandbox's canonical main process. If it exits, the
sandbox enters `Error`; use `sandbox exec` for one-shot commands that should not
define sandbox health.

To avoid passing `--policy` every time, set a default policy with an environment variable:

```shell
export OPENSHELL_SANDBOX_POLICY=./my-policy.yaml
openshell sandbox create -- claude
```

The CLI uses the policy from `OPENSHELL_SANDBOX_POLICY` whenever `--policy` is not explicitly provided.

## Iterate on a Running Sandbox

To change what the sandbox can access, pull the current policy, edit the YAML, and push the update. The workflow is iterative: create the sandbox, monitor logs for denied actions, pull the policy, modify it, push, and verify.

```mermaid
flowchart TD
    A["1. Create sandbox with initial policy"] --> B["2. Monitor logs for denied actions"]
    B --> C["3. Pull current policy"]
    C --> D["4. Modify the policy YAML"]
    D --> E["5. Push updated policy"]
    E --> F["6. Verify the new revision loaded"]
    F --> B

    style A fill:#76b900,stroke:#000000,color:#000000
    style B fill:#76b900,stroke:#000000,color:#000000
    style C fill:#76b900,stroke:#000000,color:#000000
    style D fill:#ffffff,stroke:#000000,color:#000000
    style E fill:#76b900,stroke:#000000,color:#000000
    style F fill:#76b900,stroke:#000000,color:#000000

    linkStyle default stroke:#76b900,stroke-width:2px
```

The following steps outline the hot-reload policy update workflow.

1. Create the sandbox with your initial policy by following [Apply a Custom Policy](#apply-a-custom-policy) above (or set `OPENSHELL_SANDBOX_POLICY`).

2. Monitor denials. Each log entry shows host, port, binary, and reason. Alternatively, use `openshell term` for a live dashboard.

   ```shell
   openshell logs <name> --tail --source sandbox
   ```

3. For additive network changes, use `openshell policy update`. This is the fastest path for adding endpoints, binaries, or REST and WebSocket allow/deny rules without replacing the full policy. The full option and format reference is in [Incremental Policy Updates](#incremental-policy-updates).

   ```shell
   openshell policy update <name> \
     --add-endpoint api.github.com:443:read-only:rest:enforce \
     --binary /usr/bin/gh \
     --wait

   openshell policy update <name> \
     --add-allow 'api.github.com:443:POST:/repos/*/issues' \
     --wait
   ```

   `--add-allow` and `--add-deny` target existing `protocol: rest` or `protocol: websocket` endpoints. If you pass multiple update flags in one command, OpenShell applies them as one atomic merge batch and persists at most one new revision.

4. For larger edits, pull the current base policy and edit the YAML directly. The base policy is the user-authored policy without provider-composed `_provider_*` entries, so it is safe to round-trip through `openshell policy set`. Before reusing the file, strip the metadata header above the `---` line.

   ```shell
   openshell policy get <name> --base > current-policy.yaml
   ```

   To inspect the effective policy that the sandbox enforces, including provider-composed entries, use `openshell policy get <name> --full`. To inspect a stored sandbox-authored revision instead of the current effective policy, pass `--rev <version>`.

5. Edit the YAML: add or adjust `network_policies` entries, binaries, `access`, `rules`, or protocol-specific matchers such as GraphQL operation fields, MCP `method` / `tool` rules, and generic JSON-RPC `method` rules.

6. Push the updated policy when you need a full replacement. Exit codes: 0 = loaded, 1 = validation failed, 124 = timeout.

   ```shell
   openshell policy set <name> --policy current-policy.yaml --wait
   ```

7. Verify the new revision. If status is `loaded`, repeat from step 2 as needed; if `failed`, fix the policy and repeat from step 4.

   ```shell
   openshell policy list <name>
   ```

   Add `--output json` or `--output yaml` for automation. Structured policy
   history is an envelope with `revisions` and `next_page_token` fields. Each
   revision contains the scope, sandbox name when applicable, version, full
   hash, status, and available revision timestamps, load error, and provenance.
   Pass the returned token to `--page-token` to continue. Use
   `openshell policy list --global --output json` for global history.

### Validation failures

OpenShell validates a complete candidate policy before activating any part of it. Endpoints may overlap when their connection and request-processing metadata agree. For example, two `api.example.com:443` REST entries can contribute different allow and deny rules when they use the same TLS, destination, credential, parser, and enforcement settings. A plain L4 endpoint may overlap an L7 endpoint because it authorizes the destination without contributing request-processing metadata. A more-specific path endpoint may override request-processing metadata from a broader endpoint, such as a `/graphql` GraphQL endpoint alongside a general REST endpoint for the same host. OpenShell rejects the candidate when overlapping exact or wildcard host selectors can both contribute equally specific endpoint configuration and disagree on those fields.

Internal policy-advisor provenance does not make otherwise compatible endpoints ambiguous. This lets an advisor proposal extend a provider-covered host without modifying the provider rule. TLS, destination IP constraints, credential handling, protocol, parser, and equally specific enforcement settings must still agree.

When the gateway knows the affected sandbox scope, it validates the complete
effective candidate before persistence. This covers direct policy replacement,
incremental merges and proposal approvals, provider attachment, and
provider-profile updates that fan out to attached sandboxes. An ambiguity
failure returns `FAILED_PRECONDITION`; OpenShell stores no invalid policy
revision and does not partially apply a profile update. Supervisor validation
remains a defense-in-depth boundary for startup, concurrent changes, and policy
sources outside those mutation paths.

A gateway preflight rejection leaves the currently active policy unchanged
regardless of failure mode because the candidate is never persisted or
distributed. If a candidate reaches a supervisor and fails runtime validation,
the gateway's `policy_validation_failure_mode` configuration determines the
supervisor posture. Set it under `[openshell.gateway]` in `gateway.toml`. Its
default is `fail_closed`:

```toml
[openshell.gateway]
policy_validation_failure_mode = "fail_closed"
```

In `fail_closed` mode, the supervisor publishes a quarantine generation, denies new egress, and closes connections pinned to the previous generation. The previous policy is not active. A later valid policy exits quarantine automatically.

Operators that explicitly prioritize availability can retain the previous generation:

```toml
[openshell.gateway]
policy_validation_failure_mode = "retain_last_valid"
```

In `retain_last_valid` mode, the rejected candidate remains inactive and the previous valid generation remains active. If no previous valid generation exists, such as during initial startup, OpenShell still fails closed. Restart the gateway after changing `gateway.toml`; connected sandbox supervisors receive the configured posture from the restarted gateway. Individual sandboxes cannot override it.

OCSF configuration and finding events identify the rejected candidate, validation rationale, configured and effective modes, active generation, and whether the previous policy is active. When `retain_last_valid` is configured without a previous valid generation, the effective mode remains `fail_closed`. Connection denials during quarantine include the validation failure as their policy denial rationale.

## Incremental Policy Updates

Use `openshell policy update` when you want to merge network policy changes into the current live policy instead of replacing the whole YAML document. This command only updates the dynamic `network_policies` section.

`openshell policy update` is useful when you want to:

* add a new endpoint for an existing binary without touching other policy sections.
* add a few REST or WebSocket allow/deny rules after you see a blocked request in the logs.
* remove one endpoint or one named rule without rewriting the rest of the file.
* preview a merged result locally with `--dry-run` before you send it to the gateway.

Use `openshell policy set` instead when you want to replace the full policy, update static sections, or make broader edits that are easier to express in YAML. Use full YAML for GraphQL, MCP, and JSON-RPC rule shapes.

### Update Commands

The incremental update surface is split into endpoint-level operations and method/path rule-level operations for REST and WebSocket endpoints.

| Flag                       | What it changes                                                            | Typical use                                                                                                     |
| -------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `--add-endpoint <SPEC>`    | Creates or merges a network rule and endpoint.                             | Allow a new host and port, optionally with `access`, `protocol`, `enforcement`, endpoint options, and binaries. |
| `--remove-endpoint <SPEC>` | Removes one host and port match from the current policy.                   | Drop a stale endpoint or remove one port from a multi-port endpoint.                                            |
| `--remove-rule <NAME>`     | Deletes a named `network_policies` entry.                                  | Remove a whole rule by name when you no longer need it.                                                         |
| `--add-allow <SPEC>`       | Appends method/path allow rules to an existing REST or WebSocket endpoint. | Permit one additional REST method/path or WebSocket `WEBSOCKET_TEXT` path on an API that is already configured. |
| `--add-deny <SPEC>`        | Appends method/path deny rules to an existing REST or WebSocket endpoint.  | Block a sensitive REST path or WebSocket text-message path under an endpoint that is otherwise allowed.         |
| `--binary <PATH>`          | Adds binaries to every `--add-endpoint` rule in the same command.          | Bind a new endpoint to one or more executables.                                                                 |
| `--rule-name <NAME>`       | Overrides the generated rule name.                                         | Keep a stable human-chosen rule name when adding exactly one endpoint.                                          |
| `--dry-run`                | Shows the merged policy locally and does not call the gateway.             | Review the result before persisting it.                                                                         |
| `--wait`                   | Polls until the sandbox reports that the new revision loaded.              | Confirm the change took effect before continuing.                                                               |
| `--timeout <SECS>`         | Sets the timeout for `--wait`.                                             | Extend the wait window for slower sandboxes.                                                                    |

`--wait` and `--dry-run` cannot be used together.

### Add Endpoint Compared to Allow and Deny

`--add-endpoint` works at the endpoint and rule level. It creates a new `network_policies` entry when needed, or merges into an existing rule that already covers the same host and port. Use it when you define where traffic can go and which binaries can send it.

`--add-allow` and `--add-deny` work at the method/path rule level. They do not create binaries, and they do not create a new endpoint. They modify an existing endpoint that already has `protocol: rest` or `protocol: websocket`.

This is the practical difference:

* Use `--add-endpoint` to say "allow this binary to reach `api.github.com:443`."
* Use `--add-allow` to say "for that existing REST endpoint, also allow `POST /repos/*/issues`."
* Use `--add-deny` to say "for that existing REST endpoint, explicitly deny `POST /admin/**`."
* Use `--add-allow` to say "for that existing WebSocket endpoint, also allow client text messages on `/v1/realtime/**`."

Current constraints:

* `--add-allow` and `--add-deny` work on `protocol: rest` and `protocol: websocket` endpoints.
* GraphQL, MCP, and JSON-RPC fine-grained rules require full policy YAML applied with `openshell policy set`.
* `--add-deny` requires the endpoint to already have an allow base, either an `access` preset or explicit allow `rules`.
* `protocol: sql` is not a practical incremental workflow today. OpenShell does not do full SQL parsing, and SQL enforcement is not meaningfully supported yet.

### Endpoint Specs

`--add-endpoint` uses this format:

```text
host:port[:access[:protocol[:enforcement[:options]]]]
```

Each segment has a fixed meaning:

| Segment       | Required | Meaning                                                                                                                                                                                                                                                                       |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host`        | Yes      | Destination hostname.                                                                                                                                                                                                                                                         |
| `port`        | Yes      | Destination port, `1` through `65535`.                                                                                                                                                                                                                                        |
| `access`      | No       | Access preset for L7 endpoints: `read-only`, `read-write`, or `full`. Incremental updates expand presets into protocol-specific method/path rules for REST and WebSocket endpoints.                                                                                           |
| `protocol`    | No       | Endpoint mode accepted by `openshell policy update`: `tcp`, `rest`, `websocket`, or `sql`. Use `tcp` for native DNS and TCP without L7 inspection. `sql` is audit-only and not a recommended workflow today. Full policy YAML also supports `graphql`, `mcp`, and `json-rpc`. |
| `enforcement` | No       | Enforcement mode for inspected traffic: `enforce` or `audit`.                                                                                                                                                                                                                 |
| `options`     | No       | Comma-separated endpoint options. Use `websocket-credential-rewrite` with `protocol: websocket` or REST compatibility endpoints that perform a WebSocket upgrade. Use `request-body-credential-rewrite` only with `protocol: rest`.                                           |

Examples:

| Example                                                                              | Meaning                                                                                                                                        |
| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `pypi.org:443`                                                                       | Add a plain L4 endpoint. The proxy allows the TCP stream and does not inspect HTTP requests.                                                   |
| `telemetry.example.com:443::::allow-uninspected-credentials`                         | Explicitly allow a provider-credentialed L4 endpoint after accepting that OpenShell cannot inspect or rewrite its traffic.                     |
| `db.internal.example:5432::tcp`                                                      | Add an L4 endpoint for native DNS resolution and transparent TCP capture. The empty `access` segment is required before `tcp`.                 |
| `api.github.com:443:read-only:rest:enforce`                                          | Add a REST endpoint with the `read-only` preset expanded by the policy engine into GET, HEAD, and OPTIONS access.                              |
| `api.example.com:443:read-write:rest:enforce:request-body-credential-rewrite`        | Add a REST endpoint that rewrites credential placeholders in supported text request bodies.                                                    |
| `realtime.example.com:443:read-write:websocket:enforce`                              | Add a WebSocket endpoint with the `read-write` preset expanded by the policy engine into the upgrade `GET` and client `WEBSOCKET_TEXT` access. |
| `realtime.example.com:443:read-write:websocket:enforce:websocket-credential-rewrite` | Add a WebSocket endpoint that rewrites `openshell:resolve:env:*` placeholders in client text frames after an allowed upgrade.                  |

If you set `protocol: rest` or `protocol: websocket`, you also need an allow shape. With incremental updates, that means you should provide an `access` preset on `--add-endpoint`, then use `--add-allow` or `--add-deny` to refine method/path rules later.

Use the `websocket-credential-rewrite` endpoint option with `protocol: websocket` when the sandbox should send credential placeholders in client text frames and have OpenShell resolve them after the allowed upgrade. The option can also be used with `protocol: rest` compatibility endpoints that perform a WebSocket upgrade. It is rejected for plain L4 or `protocol: sql` endpoints.

Use the `request-body-credential-rewrite` endpoint option with `protocol: rest` when an API expects OpenShell-managed credentials in UTF-8 JSON, form, or text request bodies. OpenShell buffers up to 256 KiB, rewrites recognized credential placeholders, updates `Content-Length`, and rejects unresolved placeholders instead of forwarding them. For chunked requests, the 256 KiB limit counts the complete wire representation, including framing, extensions, and trailers. The option is rejected for WebSocket, GraphQL, SQL, and plain L4 endpoints.

Use `allow-uninspected-credentials` only when a provider-credentialed endpoint must remain L4-only, use `tls: skip`, or carry uninspectable WebSocket traffic. Without this explicit opt-in, the gateway rejects credentialed L4-only and `tls: skip` endpoints. When REST body rewriting is disabled, unknown placeholder text and valid issued placeholders pass unchanged; invalid or unavailable references fail closed.

Credential rewrite recognizes the canonical `openshell:resolve:env:KEY` placeholder form and whole-token provider-shaped aliases such as `provider-OPENSHELL-RESOLVE-ENV-API_TOKEN` when the referenced environment key exists in the configured provider credentials.

Static provider placeholders resolve only when the request host, port, and path
also match an endpoint in the provider profile. A sandbox policy allow does not
expand that binding. A mismatch returns HTTP 403 with
`credential_endpoint_mismatch`. Refer to [Static Credential Endpoint Binding](/providers/profiles#understand-static-credential-endpoint-binding).

For example:

* `db.internal.example:5432::tcp` is valid.
* `api.github.com:443:read-only:rest` is valid.
* `realtime.example.com:443:read-write:websocket` is valid.
* `api.github.com:443::rest` is invalid. It does not mean "allow all traffic." An L7 endpoint with `protocol` but no `access` or `rules` is rejected when the policy loads.

Endpoint options belong to the individual `--add-endpoint` spec. When you pass multiple `--add-endpoint` flags in one command, every `--binary` value applies to every added endpoint in that command. If different endpoints need different binaries, use separate `policy update` commands.

If you do not pass `--rule-name`, OpenShell generates one from the host and port, such as `allow_api_github_com_443`.

### Method/Path Rule Specs

`--add-allow` and `--add-deny` use this format:

```text
host:port:METHOD:path_glob
```

This string identifies an existing REST or WebSocket endpoint and the request pattern you want to add.

In shell commands, quote the full `SPEC` when it contains `*` or `**` so your shell passes it literally instead of expanding it as a local file glob.

| Segment     | Meaning                                                                                                                                                                         |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host`      | Existing endpoint host.                                                                                                                                                         |
| `port`      | Existing endpoint port.                                                                                                                                                         |
| `METHOD`    | HTTP method for REST endpoints, or `GET` / `WEBSOCKET_TEXT` for WebSocket endpoints. The CLI normalizes it to uppercase.                                                        |
| `path_glob` | URL path glob. For WebSocket text messages, this still matches the upgraded request path, not message payload content. It must start with `/`, or be `**`, or start with `**/`. |

This example:

```text
api.github.com:443:POST:/repos/*/issues
```

means:

* match the endpoint `api.github.com:443`.
* match HTTP method `POST`.
* match paths like `/repos/acme/issues`.
* also match deeper paths when the surrounding literals align, because `*` may include `/`.

Path globs follow the same semantics as YAML allow and deny rules:

* `*` and `**` match zero or more characters and may cross `/` boundaries.
* `?` matches exactly one character.
* bracket classes such as `[0-9]` and negated classes such as `[!0]` are supported.
* `/repos/*/issues` matches any intervening text, including multiple path segments.
* `/repos/**` matches everything under `/repos/`.

The rule-level commands only modify method and path constraints. They do not change binaries, hostnames, ports, protocol settings, or WebSocket message payload matching.

### Common Workflows

Use these patterns as starting points when you decide whether to update an endpoint or append REST/WebSocket rules.

#### Add a new L4 endpoint

Use `--add-endpoint` when you need a new host and port and do not need REST inspection.

```shell
openshell policy update demo \
  --add-endpoint pypi.org:443 \
  --add-endpoint files.pythonhosted.org:443 \
  --binary /usr/bin/pip \
  --binary /usr/local/bin/uv \
  --wait
```

This creates or merges endpoint entries and binds them to the listed binaries. It does not create inspected method/path rules.

#### Create a REST endpoint with a base allow set

Use `--add-endpoint` first when the endpoint does not exist yet.

```shell
openshell policy update demo \
  --add-endpoint api.github.com:443:read-only:rest:enforce \
  --binary /usr/bin/gh \
  --wait
```

This creates a REST endpoint and sets its base allow behavior through the `read-only` access preset.

#### Add one more REST allow rule

Use `--add-allow` after the REST endpoint already exists.

```shell
openshell policy update demo \
  --add-allow 'api.github.com:443:POST:/repos/*/issues' \
  --wait
```

This keeps the existing endpoint definition and appends one new allow rule. It does not add binaries or change the endpoint host and port.

#### Add a REST deny rule under an allowed endpoint

Use `--add-deny` when you want to carve out a blocked subtree under an existing REST endpoint.

```shell
openshell policy update demo \
  --add-deny 'api.github.com:443:POST:/admin/**' \
  --wait
```

This adds a deny rule to the existing REST endpoint. The endpoint must already have an allow base.

#### Create a WebSocket endpoint with a base allow set

Use `--add-endpoint` with `protocol: websocket` when the destination is an RFC 6455 WebSocket API.

```shell
openshell policy update demo \
  --add-endpoint realtime.example.com:443:read-write:websocket:enforce:websocket-credential-rewrite \
  --binary /usr/bin/node \
  --wait
```

This creates a WebSocket endpoint and sets its base allow behavior through the `read-write` access preset. For WebSocket endpoints, `read-write` expands to the upgrade `GET` and client `WEBSOCKET_TEXT` messages on the upgraded request path. The rewrite option lets the sandbox send `openshell:resolve:env:*` placeholders in client text frames; OpenShell resolves them before forwarding to the upstream service.

#### Add a WebSocket text-message deny rule

Use `WEBSOCKET_TEXT` when you want to refine client-to-server text-frame policy without matching message payload content.

```shell
openshell policy update demo \
  --add-deny 'realtime.example.com:443:WEBSOCKET_TEXT:/v1/admin/**' \
  --wait
```

This adds a deny rule to the existing WebSocket endpoint. The path glob matches the WebSocket upgrade path.

#### Remove one endpoint or rule

Use `--remove-endpoint` to remove one host and port pair, or `--remove-rule` to delete the whole named rule.

```shell
openshell policy update demo --remove-endpoint pypi.org:443 --wait
openshell policy update demo --remove-rule github_repos --wait
```

If the target endpoint is part of a multi-port endpoint, `--remove-endpoint` removes only the specified port and keeps the rest.

### Merge Semantics

OpenShell applies all update flags from one `openshell policy update` command as one merge batch. The gateway validates the full merged result and persists at most one new policy revision.

This means:

* one command is atomic at the revision level.
* multiple flags in one command succeed or fail together.
* concurrent writers do not partially interleave one batch with another.

When two updates race, the gateway uses optimistic retry. It fetches the latest revision, reapplies the full batch, validates the result again, and retries the write. This preserves the intent of each individual command while still allowing concurrent sandbox policy updates.

### Preview and Validation

Use `--dry-run` when you want to inspect the merged YAML before you send it to the gateway.

```shell
openshell policy update demo \
  --add-allow 'api.github.com:443:GET:/repos/**' \
  --dry-run
```

The CLI validates the argument shapes before it sends the request. The gateway then validates the merged policy against the current live policy and returns clear errors when:

* a required segment is missing.
* a port is outside `1` through `65535`.
* `--add-allow` or `--add-deny` points at an endpoint that does not exist.
* `--add-allow` or `--add-deny` targets an endpoint that is neither REST nor WebSocket.
* `--add-allow` or `--add-deny` targets a host and port that resolves to more than one endpoint.
* `--add-deny` targets an endpoint that has no base allow set.
* an update names an existing rule and adds a binary to it without declaring every endpoint and port that rule already authorizes.
* an update names an existing rule and adds or changes an endpoint on it without declaring every binary that rule already authorizes.
* an update widens a rule to any binary without declaring every endpoint that rule already authorizes.
* an update changes an endpoint without declaring every port that endpoint carries.
* an update puts an MCP endpoint on the same host and port as a differently inspected endpoint, or gives one host and port two different MCP inspection contracts, including through separate rules or different paths.

A rule authorizes every listed binary to reach every listed endpoint and port, so merging a binary and an endpoint into the same rule authorizes that pair too. The gateway rejects the whole batch rather than granting a pair the update did not ask for. The error names the binary scope and the ports involved, and lists the binaries you still need to declare. An empty binary list means any binary, so widening a rule to any binary is subject to the same requirement.

You can declare the scope across several `--add-endpoint` arguments. The update is complete as long as every binary-to-port pair the merged rule ends up authorizing appears somewhere in the update.

An endpoint's allow rules, deny rules, and allowed IPs apply to every port that endpoint carries, so an update that changes any of them has to name every one of those ports. Declaring `api.example.com:443` alone on an endpoint that also serves `8443` is rejected, because the change would reach `8443` as well. Declaring every existing binary does not lift this requirement; the two are separate axes of the same product.

The sandbox picks the parser for a request by most-specific path, but it authorizes the request against every endpoint that matches it. A broad REST endpoint and a narrower GraphQL endpoint on one host and port share the same method-and-path rule vocabulary, so that combination stays supported. MCP does not: its rules address JSON-RPC methods and tool names, so a plain REST rule on an overlapping path could authorize a tool call the MCP endpoint denies. An MCP endpoint therefore cannot share a host and port with a differently inspected endpoint, and two MCP endpoints there must agree on their exact revision allowlist, strict-tool-name, method-profile, and body-limit settings, even under different paths or in separate rules. An update creating either situation is rejected. A policy that already contains one is left alone so unrelated updates still apply, but it should be repaired with full YAML replacement.

To grant one binary access to only part of an existing rule's endpoints, send it under its own `--rule-name`. The gateway normally folds an update into an existing rule that shares an endpoint, but it keeps your rule name whenever folding would grant authorization you did not declare, so the narrow grant lands as its own rule authorizing exactly what you asked for. The update reports that it kept your rule name and names the rule it would otherwise have folded into. An MCP contract conflict is the exception: one host and port carry a single MCP contract regardless of which rule holds them, so a conflicting update is rejected rather than moved to a separate rule.

Once a host and port appears in more than one rule, `--add-allow` and `--add-deny` can no longer target it. They select an endpoint by host and port alone, so they cannot say which rule's binary scope to widen, and the gateway rejects the update rather than guessing. The same applies when one rule carries two endpoints on that host and port under different paths. Use full YAML replacement to change L7 rules on an endpoint that appears more than once.

## Global Policy Override

Use a global policy when you want one policy payload to apply to every sandbox.

```shell
openshell policy set --global --policy ./global-policy.yaml
```

When a global policy is configured:

* The global payload is applied in full for all sandboxes.
* Sandbox-level policy updates are rejected until the global policy is removed.

To restore sandbox-level policy control, delete the global policy setting:

```shell
openshell policy delete --global
```

You can inspect a sandbox's effective settings and policy source with:

```shell
openshell settings get <name>
```

## Debug Denied Requests

Check `openshell logs <name> --tail --source sandbox` for the denied host, path, and binary.

For agent-authored draft updates on running sandboxes, enable [Policy Advisor](/sandboxes/policy-advisor). Policy advisor lets the sandboxed agent submit a narrow proposal through `policy.local` while a developer still approves or rejects the structured rule from outside the sandbox.

When triaging denied requests, check:

* Destination host and port to confirm which endpoint is missing.
* Calling binary path to confirm which `binaries` entry needs to be added or adjusted.
* HTTP method and path for REST endpoints, or `GET` / `WEBSOCKET_TEXT` and the upgraded request path for WebSocket endpoints, to confirm which `rules` entry needs to be added or adjusted.
* `credential_endpoint_mismatch` in sandbox logs to confirm that policy admitted the request but the attached provider profile did not authorize its credential for that host, port, and path.
* `request_authority_mismatch` in the response or sandbox logs to confirm that the HTTP request authority differs from the authorized tunnel endpoint. For a CONNECT tunnel to `api.example.com:8443`, send `Host: api.example.com:8443`; omitting the non-default port makes the request authority use the transport default and OpenShell rejects it. Absolute-form request targets must use the same host and port.

Then push the updated policy as described above.

Do not fix `credential_endpoint_mismatch` by widening sandbox policy. Export the
provider profile with `openshell provider profile export <profile-id> -o yaml`.
Update the custom provider profile only when the destination is an intended
credential recipient. Refer to [Static Credential Endpoint Binding](/providers/profiles#understand-static-credential-endpoint-binding)
for the complete authorization model.

For small changes, prefer `openshell policy update` over rewriting the full YAML:

```shell
openshell policy update <name> --add-allow 'api.github.com:443:GET:/repos/**' --wait
```

## Examples

Add these blocks to the `network_policies` section of your sandbox policy. Apply simple endpoints and REST/WebSocket rule additions with `openshell policy update`, or apply any complete YAML block with `openshell policy set <name> --policy <file> --wait`.
Use **Simple endpoint** for host-level allowlists and **Granular rules** for method/path control.

#### Simple endpoint

Allow `pip install` and `uv pip install` to reach PyPI:

```yaml showLineNumbers={false}
  pypi:
    name: pypi
    endpoints:
      - host: pypi.org
        port: 443
      - host: files.pythonhosted.org
        port: 443
    binaries:
      - { path: /usr/bin/pip }
      - { path: /usr/local/bin/uv }
```

Endpoints without `protocol` use explicit-proxy TCP passthrough, where OpenShell allows the stream without inspecting payloads. Use `protocol: tcp` when the application needs ordinary DNS resolution and native TCP connections through transparent capture. Provider-credentialed endpoints cannot use either L4 shape unless `allow_uninspected_credentials: true` records the exception. If an explicit-proxy stream is HTTP and TLS is auto-terminated, the proxy can still rewrite configured credential placeholders and closes keep-alive passthrough tunnels on policy reload before forwarding another request. WebSocket text-frame policy requires an explicit `protocol: websocket` endpoint. WebSocket payload credential rewrite can also be enabled on a `protocol: rest` compatibility endpoint with `websocket_credential_rewrite: true`. REST request body credential rewrite requires an inspected `protocol: rest` endpoint with `request_body_credential_rewrite: true`.

#### Granular rules

Allow Claude and the GitHub CLI to reach `api.github.com` with separate REST and GraphQL endpoint scopes: read-only REST for general API paths, GraphQL operation inspection on `/graphql`, full REST write access for `alpha-repo`, and create/edit issues only for `bravo-repo`. Replace `<org_name>` with your GitHub org or username.

For an end-to-end walkthrough that combines this policy with a GitHub credential provider and sandbox creation, refer to [GitHub Sandbox](/get-started/tutorials/github-sandbox).

```yaml showLineNumbers={false}
  github_repos:
    name: github_repos
    endpoints:
      - host: api.github.com
        port: 443
        path: "/**"
        protocol: rest
        enforcement: enforce
        rules:
          - allow:
              method: GET
              path: "/**"
          - allow:
              method: HEAD
              path: "/**"
          - allow:
              method: OPTIONS
              path: "/**"
          - allow:
              method: "*"
              path: "/repos/<org_name>/alpha-repo/**"
          - allow:
              method: POST
              path: "/repos/<org_name>/bravo-repo/issues"
          - allow:
              method: PATCH
              path: "/repos/<org_name>/bravo-repo/issues/*"
      - host: api.github.com
        port: 443
        path: "/graphql"
        protocol: graphql
        enforcement: enforce
        rules:
          - allow:
              operation_type: query
          - allow:
              operation_type: mutation
              fields: [createIssue, updateIssue, addComment]
        deny_rules:
          - operation_type: mutation
            fields: [deleteRepository, deleteRef, updateBranchProtectionRule]
    binaries:
      - { path: /usr/local/bin/claude }
      - { path: /usr/bin/gh }
```

Endpoints with `protocol: rest` enable HTTP request inspection and can opt in to supported text request body credential rewrite. Endpoints with `protocol: websocket` validate WebSocket upgrades and inspect client text messages on the upgraded request path. WebSocket endpoints can also classify GraphQL-over-WebSocket operation messages with the same operation rules used by GraphQL-over-HTTP. Endpoints with `protocol: graphql` parse GraphQL-over-HTTP payloads before evaluating rules. Endpoints with `protocol: mcp` parse MCP Streamable HTTP request bodies and evaluate `method`, optional `tool`, and supported params rules. Endpoints with `protocol: json-rpc` parse JSON-RPC-over-HTTP request bodies and evaluate `method` rules. The endpoint-level `path` field lets these protocols share `api.github.com:443` without treating GraphQL payloads as plain REST `POST /graphql` requests.

### Query parameter matching

REST rules can also constrain query parameter values:

```yaml showLineNumbers={false}
  download_api:
    name: download_api
    endpoints:
      - host: api.example.com
        port: 443
        protocol: rest
        enforcement: enforce
        rules:
          - allow:
              method: GET
              path: "/api/v1/download"
              query:
                slug: "skill-*"
                version:
                  any: ["1.*", "2.*"]
    binaries:
      - { path: /usr/bin/curl }
```

`query` matchers are case-sensitive and run on decoded values. If a request has duplicate keys (for example, `tag=a&tag=b`), every value for that key must match the configured glob(s).

### MCP and JSON-RPC matching

MCP endpoints use `protocol: mcp`. The proxy parses sandbox-to-server MCP Streamable HTTP request bodies, validates known MCP request and notification params, can evaluate the MCP method against rule `method`, and can match tool calls with the `tool` alias. Unknown extension methods stay addressable as literal method strings. You may omit the entire `mcp` stanza when using its defaults, or omit only `mcp.versions` when setting another MCP option. OpenShell immediately resolves either form to the exact `["2025-11-25"]` allowlist and stores the materialized list in canonical policy data. Adding another supported revision therefore never widens a normalized policy. Defaulting requires the `mcp` or `mcp.versions` key to be absent; explicit `mcp: null`, `versions: null`, and `versions: []` values are invalid. Use an explicit nonempty allowlist only for intentional compatibility or downgrade control. Supported revisions are `2025-03-26`, `2025-06-18`, and `2025-11-25`; the exact support floor is `2025-03-26`, and this is a closed set rather than a date range. Explicit values must be unique and contain no extra whitespace; OpenShell stores them in semantic order. Moving aliases such as `draft` or `latest` are rejected because they could change policy meaning without a policy edit; omission never means all known versions. The sessionless `2026-07-28` revision is not accepted until OpenShell supports its distinct per-request contract. A version identifies a core MCP revision only; there is no policy syntax for separately named SEP overlays. For every MCP HTTP request except a valid standalone `initialize`, OpenShell selects the revision from one `MCP-Protocol-Version` header or, when the header is absent, the MCP specification's `2025-03-26` compatibility fallback. The selected revision must appear in the allowlist. Duplicate, empty, and unsupported header values receive `400 Bad Request`; a supported revision outside the allowlist receives `403 Forbidden`. OpenShell checks the request again after middleware changes it and before forwarding. Generic JSON-RPC endpoints do not use this header and continue to evaluate only `method`. `mcp.allow_all_known_mcp_methods` defaults to `false`, so endpoints require explicit MCP method rules. Set it to `true` to enable the endpoint method profile; in that mode, rules can omit `method`, and tool selectors are normalized to `tools/call` internally. By default, MCP `tools/call` tool names must match `^[A-Za-z0-9_.-]{1,128}$`; set `mcp.strict_tool_names: false` on that endpoint only when a server intentionally uses names outside the MCP-recommended pattern. Wildcard `tool` matchers require `mcp.strict_tool_names` to remain enabled.

The current registry declares these profile facts for later runtime enforcement:

* `2025-03-26` permits nonempty same-side top-level JSON-RPC batches; OpenShell's planned enforcement caps them at 64 members.
* `2025-06-18` prohibits top-level JSON-RPC arrays.
* `2025-11-25` prohibits top-level JSON-RPC arrays.

OpenShell uses the per-request header only to enforce the allowlist; the current parser does not yet apply these version-specific batch rules. OpenShell does not treat the client's `initialize.params.protocolVersion` as the selected revision, inspect the server's initialization response, or bind `MCP-Session-Id`. The version check is stateless and applies independently to each later request, including GET and DELETE.

MCP endpoints must declare a concrete destination with `host` and `port` or `ports`. A policy entry that only sets `protocol: mcp` is invalid and is not treated as a wildcard MCP authorization. Use `path: /mcp` when the server's MCP endpoint is path-scoped; omitting `path` matches every HTTP path on that host and port.

Existing versionless inspected MCP policies remain valid and normalize to the exact `2025-11-25` default; they do not need a migration edit. This is a pinned default, not a moving latest or all-known selection. For an unsupported revision, omit `protocol` and the `mcp` stanza to authorize deliberate, uninspected TCP passthrough only when that weaker L4 boundary is acceptable; OpenShell does not silently downgrade to an older inspected profile.

MCP policy enforcement is directional. It applies to HTTP request bodies sent by the sandboxed process to the configured endpoint. JSON-RPC responses and server-to-client MCP messages carried on response bodies or SSE streams are relayed but are not currently parsed for policy enforcement.

MCP and JSON-RPC endpoint policies currently require full policy YAML applied with `openshell policy set`; the incremental `openshell policy update --add-endpoint` parser does not accept `mcp` or `json-rpc` as protocols.

An MCP client first sends `initialize`. After the server returns a successful response, the client sends `notifications/initialized`. After initialization completes and the server advertises the `tools` capability, the client can call an advertised tool. The response does not need an allow rule because these rules inspect messages sent from the client to the server. This example adds both client initialization messages to the existing tool rules. It omits `tools/list` because it assumes the client already knows the tool names; add that method when the client performs discovery.

```yaml showLineNumbers={false}
  mcp_server:
    name: mcp_server
    endpoints:
      - host: mcp.example.com
        port: 443
        path: /mcp
        protocol: mcp
        enforcement: enforce
        rules:
          - allow:
              method: initialize
          - allow:
              method: notifications/initialized
          - allow:
              method: tools/call
              tool: read_status
          - allow:
              method: tools/call
              tool:
                any: [submit_report, list_reports]
        deny_rules:
          - method: tools/call
            tool: delete_resource
    binaries:
      - { path: /usr/bin/python3 }
```

The example omits the entire `mcp` stanza, so OpenShell uses the exact `2025-11-25` revision and the other MCP defaults. Canonical serialization still shows the materialized revision list. To allow an older server intentionally, add an explicit compatibility or downgrade allowlist:

```yaml showLineNumbers={false}
        mcp:
          versions: ["2025-03-26", "2025-11-25"]
```

OpenShell canonicalizes this list in semantic order. Later runtime negotiation selects one allowed revision and applies only that profile; it does not combine the two profiles. `mcp.max_body_bytes` controls how many MCP-over-HTTP request body bytes OpenShell buffers for inspection and defaults to `65536`. `mcp.strict_tool_names` defaults to `true` for each MCP endpoint. `mcp.allow_all_known_mcp_methods` defaults to `false`; when it is unset or `false`, the endpoint must define explicit MCP method rules. If an MCP endpoint sets `mcp.allow_all_known_mcp_methods: true` and omits `rules`, OpenShell allows all MCP-family methods and all tools, then applies any `deny_rules`. A broad allow or deny rule whose method matcher includes `tools/call` cannot be combined with tool-specific allow rules because it would bypass or erase the tool filter; add `tool` or `params.name` to scope `tools/call`, or remove the tool-specific rules.

Use `protocol: json-rpc` and `method` when you need generic JSON-RPC 2.0 matching for a non-MCP server. Generic JSON-RPC method rules accept exact method names, or `method: "*"` as the all-method sentinel; other wildcard or glob patterns are rejected. `json_rpc.max_body_bytes` controls the generic JSON-RPC inspection buffer.

Generic JSON-RPC policy `params` matchers are not supported. Generic JSON-RPC policy rules match only the JSON-RPC method. For batch requests, OpenShell evaluates each JSON-RPC call independently and denies the whole batch if any call is denied.

For MCP, `tool` accepts a string glob or `{ any: [...] }` matcher for `tools/call` `params.name`. Rules that use `tool` or lower-level `params.name` must set `method: tools/call` unless `mcp.allow_all_known_mcp_methods: true` enables the endpoint method profile. MCP method globs are accepted only for the `tools/` method family, such as `tools/*`; omit `method` instead of writing `method: "*"` only when the endpoint method profile should allow all MCP methods. Omit `tool` to allow all tools for a `tools/call` method rule. OpenShell does not support MCP tool argument matching yet; allowed tools accept all argument payloads by default. Other MCP `params` keys are rejected. For batch requests, OpenShell evaluates each JSON-RPC call independently and denies the whole batch if any call is denied.

### GraphQL matching

GraphQL endpoints use `protocol: graphql`. The proxy parses GraphQL-over-HTTP `GET` and `POST` requests, classifies each operation, and evaluates rules against the operation type, optional operation name, and selected root fields.

GraphQL endpoint policies currently require full policy YAML applied with `openshell policy set`; the incremental `openshell policy update --add-endpoint` parser does not accept `graphql` as a protocol.

```yaml showLineNumbers={false}
  github_graphql:
    name: github_graphql
    endpoints:
      - host: api.github.com
        port: 443
        path: "/graphql"
        protocol: graphql
        enforcement: enforce
        rules:
          - allow:
              operation_type: query
              fields: [viewer, repository]
          - allow:
              operation_type: mutation
              operation_name: Issue*
              fields: [createIssue]
        deny_rules:
          - operation_type: mutation
            fields: [deleteRepository]
    binaries:
      - { path: /usr/bin/gh }
```

For allow rules, every selected root field in an operation must match one of the configured `fields` globs. For deny rules, one matching root field blocks the request. Batched GraphQL requests are fail-closed: if any operation is malformed, denied, or unregistered, the whole HTTP request is denied.

Hash-only persisted queries cannot be classified from the request alone. OpenShell denies them unless the endpoint uses `persisted_queries: allow_registered` and provides a trusted `graphql_persisted_queries` entry keyed by hash or saved-query ID.

### GraphQL-over-WebSocket matching

Some APIs carry GraphQL operations over RFC 6455 WebSockets, commonly for subscriptions and realtime updates. Configure these as `protocol: websocket`, allow the upgrade with a normal `GET` rule, then add GraphQL operation rules for client operation messages. OpenShell recognizes modern `graphql-transport-ws` `subscribe` messages and legacy `graphql-ws` `start` messages.

```yaml showLineNumbers={false}
  realtime_graphql:
    name: realtime_graphql
    endpoints:
      - host: realtime.example.com
        port: 443
        path: "/graphql"
        protocol: websocket
        enforcement: enforce
        rules:
          - allow:
              method: GET
              path: "/graphql"
          - allow:
              operation_type: subscription
              fields: [messageAdded]
          - allow:
              operation_type: query
              fields: [viewer]
        websocket_credential_rewrite: true
    binaries:
      - { path: /usr/bin/node }
```

When a WebSocket endpoint has GraphQL operation policy, client operation messages are fail-closed on malformed JSON, unsupported message types, parse errors, unregistered hash-only persisted queries, or unallowed operations. Use GraphQL operation rules for client messages rather than a raw `WEBSOCKET_TEXT` allow rule. Protocol lifecycle messages such as `connection_init`, `ping`, `pong`, and `complete` are allowed without payload logging; if `websocket_credential_rewrite: true` is set, placeholders inside those text messages are resolved before forwarding.

### GraphQL service policy shapes

GraphQL field names are application-specific, so treat these as starting shapes to review against the actual app schema:

| Service                   | Endpoint shape                              | Starting policy                                                                                                                                                                             |
| ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Railway                   | `backboard.railway.app/graphql/v2`          | Allow `query`; allow only reviewed deployment mutations; deny `volumeDelete`, `projectDelete`, `*Delete`, `*Destroy`.                                                                       |
| GitHub                    | `api.github.com/graphql`                    | Allow `query`; optionally allow low-risk mutations such as reactions; deny broad destructive/admin roots like `deleteRef`, `deleteRepository`, `updateBranchProtectionRule`, and `delete*`. |
| GitLab                    | `/api/graphql`                              | Prefer read-only token scopes where possible; allow `query`; deny mutations by default or allow only reviewed workflow roots.                                                               |
| Shopify Admin             | `*.myshopify.com/admin/api/**/graphql.json` | Allow `query`; allow app-specific mutations only; deny `*Delete`, `bulkOperationRunMutation`, and high-impact inventory/order/customer roots unless approved.                               |
| monday.com                | `api.monday.com/v2`                         | Allow board/item reads; allow tightly scoped create/update mutations only where needed; deny delete/archive roots.                                                                          |
| Salesforce GraphQL        | Salesforce GraphQL endpoint                 | Allow `query`; deny record create/update/delete mutations unless the sandbox is intended to modify CRM data.                                                                                |
| Hygraph                   | Project content API endpoint                | Allow content reads; deny generated destructive content roots such as `delete*`, `deleteMany*`, `unpublish*`, and batch mutations unless a publishing workflow requires them.               |
| Atlassian GraphQL Gateway | `api.atlassian.com/graphql`                 | Allow reads by default; require explicit mutation allowlists because the gateway spans Jira, Confluence, Bitbucket, and admin surfaces.                                                     |

## Next Steps

Explore related topics:

* To learn about the built-in sandbox policy, refer to [Default Policy](/reference/default-policy).
* To view the full field-by-field YAML definition, refer to the [Policy Schema Reference](/reference/policy-schema).
* To review the default policy breakdown, refer to [Default Policy](/reference/default-policy).