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

# Write Network Rules

> Select which Transparent Mode traffic Formal intercepts with CEL network rules

## Overview

Network rules select which traffic Formal Endpoint intercepts with [Transparent Mode](https://docs.formal.ai/docs/guides/client-apps/desktop-app.md#transparent-mode). Intercepted traffic undergoes [policy](https://docs.formal.ai/docs/glossary/index.md#policy) evaluation and logging.

A TCP connection is intercepted when an active network rule matches. All the stages within the [CEL](https://cel.dev/) `condition` block (`pre_tcp`, `pre_tls` and `post_tls`) must evaluate to `true`.

Create rules in the [Control Plane](https://docs.formal.ai/docs/glossary/index.md#control-plane) at [Network Rules](https://app.formal.ai/network-rules). New rules default to **Draft**. Set a rule to **Active** before it intercepts traffic.

## Rule structure

```json theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": "<CEL_CONDITION>",
    "pre_tls": "<CEL_CONDITION>",
    "post_tls": "<CEL_CONDITION>"
  },
  "outputs": {
    "forward_to_connector": true,
    "resource_name": "my_resource"
  }
}
```

`pre_tcp` applies on macOS only. On Linux, `pre_tcp` is ignored.

`outputs` is optional. Use it when the Formal Endpoint should forward traffic to the [Connector](https://docs.formal.ai/docs/glossary/index.md#connector) instead of evaluating policies locally.

| Output | Type | Description |
| - | - | - |
| `forward_to_connector` | boolean | Forward HTTP, MCP, and LLM traffic to the [Connector](https://docs.formal.ai/docs/glossary/index.md#connector). Formal does not evaluate that traffic on the Endpoint. |
| `resource_name` | string | Forward using this Formal [resource](https://docs.formal.ai/docs/glossary/index.md#resource) hostname and port, even if the request technology does not match. |

SSH ignores `outputs`. The Endpoint always connects SSH to the original destination, even when `resource_name` names an SSH resource. See [SSH in Transparent Mode](https://docs.formal.ai/docs/guides/core-concepts/resources/ssh.md#transparent-mode).

## `pre_tcp` conditions

`pre_tcp` runs in the macOS Network Extension. It sees the connecting process, its ancestors, and the destination port.

A matching `pre_tcp` condition does not intercept traffic by itself. Formal logs the flow and evaluates policy only when `pre_tls` and `post_tls` are also true.

See [Network rule inputs](#network-rule-inputs) for the fields each stage can read.

On macOS, this rule diverts traffic from a known agent or its children. It then intercepts MCP and LLM.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": process.is_agent || process.has_agent_ancestor,
    "pre_tls": true,
    "post_tls": technology in ["mcp", "llm"]
  }
}
```

Use `port` in `pre_tcp` to filter destinations before diverting traffic. Put hostname and other destination checks in `pre_tls`. If other rules should see different processes, give each rule its own `pre_tcp` condition. Formal diverts a flow when any one of those conditions matches.

Traffic from a process that matches no `pre_tcp` condition does not appear in [Logs](https://docs.formal.ai/docs/guides/observability/logs.md).

## `pre_tls` conditions

`pre_tls` sees TCP flow metadata that Formal can collect without talking to the client process. That includes the destination hostname and port, the Unix process that opened the connection and its ancestors, and the user identity.

If any `pre_tls` rule matches a TLS flow, Formal terminates TLS so it can inspect and forward plaintext. A TLS flow begins with a TCP ClientHello.

Flows that do not start with a ClientHello, such as SSH, skip TLS termination. `post_tls` then classifies them from their first bytes.

Exclude applications that do not trust the Formal certificate from `pre_tls`. This includes clients that do not use the system CA store, such as some Python scripts. Otherwise those clients raise CA trust errors.

## `post_tls` conditions

`post_tls` receives the same inputs as `pre_tls`. Formal also classifies the plaintext wire protocol. You can include or exclude specific technologies.

If `pre_tls` matches and no rule's `post_tls` matches, Formal forwards the unchanged traffic. It does not log the flow or evaluate policy.

## Process identity

On macOS, `process.is_agent`, the per-agent flags, `process.has_agent_ancestor`, and `process.is_browser` use code-signed process identity.

On Linux, the agent flags match `/proc` comm of the installed binary. `process.is_browser` and `process.signing.*` stay false on Linux. An agent flag with no Linux process name stays false there too.

`os` is `linux` or `macos`. Every stage can read it.

### Known agent identities

Formal recognizes these signed processes as agents. Each row requires an exact match for every listed identity field.

| Agent | Code identifier | Bundle identifier | Team ID |
| - | - | - | - |
| Amp | `com.ampcode.amp.cli` | None | `PZT9BJUAA5` |
| Antigravity | `com.google.antigravity` | `com.google.antigravity` | `EQHXZ8M8AV` |
| Antigravity | `com.google.antigravity-ide` | `com.google.antigravity-ide` | `EQHXZ8M8AV` |
| Antigravity | `cli` | None | `EQHXZ8M8AV` |
| Claude Code | `com.anthropic.claude-code` | None | `Q6L2SF6YDW` |
| Claude Code | `com.anthropic.claude-code` | `com.anthropic.claude-code` | `Q6L2SF6YDW` |
| Claude Desktop | `com.anthropic.claudefordesktop` | `com.anthropic.claudefordesktop` | `Q6L2SF6YDW` |
| Claude Desktop | `com.anthropic.claudefordesktop.helper` | `com.anthropic.claudefordesktop.helper` | `Q6L2SF6YDW` |
| Codex | `codex` | None | `2DC432GLL2` |
| Codex | `codex` | `com.openai.codex.cli` | `2DC432GLL2` |
| Codex | `com.openai.codex.helper` | `com.openai.codex.helper` | `2DC432GLL2` |
| Cursor | `com.todesktop.230313mzl4w4u92` | None | `VDXQ22DGB9` |
| Cursor | `com.todesktop.230313mzl4w4u92` | `com.todesktop.230313mzl4w4u92` | `VDXQ22DGB9` |
| Cursor | `com.todesktop.230313mzl4w4u92.helper` | `com.todesktop.230313mzl4w4u92.helper` | `VDXQ22DGB9` |
| GitHub Copilot CLI | `copilot` | None | `VEKTX9H2N7` |
| GitHub Copilot for Xcode | `com.github.CopilotForXcode` | `com.github.CopilotForXcode` | `VEKTX9H2N7` |
| Crush | `crush` | None | `X3UBLT2DDB` |
| Devin | `devin` | None | `83Z2LHX6XW` |
| Grok | Any | `com.anysphere.sand` | `DCNK4UB866` |
| Hermes Agent | `com.nousresearch.hermes.managed-python` | None | None |
| OpenCode | `ai.opencode.desktop` | `ai.opencode.desktop` | `5NZ4Q7NXJ4` |
| Amazon Q Developer | `q` | None | `94KV3E626L` |
| Windsurf | `com.exafunction.windsurf` | `com.exafunction.windsurf` | `83Z2LHX6XW` |
| Warp Agent CLI | `warp-tui-stable` | None | `2BBY89MBSN` |

`Any` accepts every code identifier. `None` requires an empty bundle identifier or team ID. A process from another signing team is not recognized.

## Examples

This starting rule intercepts MCP and LLM on macOS and Linux. It matches known agents on both. On Linux it also matches known LLM provider hostnames, and `pre_tcp` is ignored.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": process.is_agent,
    "pre_tls": process.is_agent || (is_llm_hostname && os == "linux"),
    "post_tls": technology in ["mcp", "llm"]
  }
}
```

On macOS, this rule intercepts MCP and LLM traffic from a known agent.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": os == "macos" && process.is_agent,
    "pre_tls": true,
    "post_tls": technology in ["mcp", "llm"]
  }
}
```

On macOS, this rule intercepts every flow from a known agent, including child processes it spawned. `has_agent_ancestor` is also true when the connecting process is the agent.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": process.has_agent_ancestor,
    "pre_tls": true,
    "post_tls": true
  }
}
```

To limit that intercept to Codex Desktop, match the signed bundle ID on the process or an ancestor. See [Restrict Codex Desktop to LLM and MCP](https://docs.formal.ai/docs/guides/example-use-cases/restrict-codex-desktop-egress.md).

This rule intercepts AWS API traffic from the Claude Code CLI. It applies only to users in the Infra group.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": process.ancestors.exists(p, p.signing.team_id == "Q6L2SF6YDW" && p.signing.bundle_identifier == "com.anthropic.claude-code"),
    "pre_tls": "Infra" in user.groups,
    "post_tls": technology == "aws"
  }
}
```

On Linux, this rule intercepts traffic to known model-provider hostnames.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": os == "linux",
    "pre_tls": is_llm_hostname,
    "post_tls": technology in ["mcp", "llm"]
  }
}
```

This rule intercepts Postgres and MySQL flows whose destination hostname and port match a Formal resource.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": port in [5432, 3306],
    "pre_tls": resource.name != "",
    "post_tls": true
  },
  "outputs": {
    "forward_to_connector": true,
    "resource_name": resource.name
  }
}
```

This rule intercepts SSH to hosts under `example.com`. The Endpoint evaluates [SSH session policies](https://docs.formal.ai/docs/guides/policies/evaluation.md#ssh-resources) on it.

```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
{
  "condition": {
    "pre_tcp": port == 22,
    "pre_tls": hostname.endsWith(".example.com"),
    "post_tls": technology == "ssh"
  }
}
```

**Verify:**

1. Set the network rule to **Active**.
2. Enable Transparent Mode: `formal transparent-proxy enable`
3. Confirm it is running: `formal transparent-proxy status`
4. Send matching traffic and confirm it appears in [Logs](https://docs.formal.ai/docs/guides/observability/logs.md).

## Technology classification

Formal classifies the plaintext wire protocol after TLS terminates. Use the `technology` input only in the `post_tls` condition. It is not available in `pre_tls` or `pre_tcp`.

`technology` supports these values:

* `http`
* `mcp`
* `llm`
* `aws`
* `gcp`
* `postgres`
* `mysql`
* `mariadb`
* `mongodb`
* `ssh`

> [!WARNING]
>
> Use `technology` only in `post_tls`. A `pre_tls` or `pre_tcp` condition cannot read the classified protocol.

## Network rule inputs

The Stages column lists where each input can be read. `pre_tcp` applies on macOS only.

Prefer `process` for new rules. `connection_process` still works. The connecting process stays nested at `connection_process.process`. Every `process` field is also on `connection_process`.

`user` is the logged-in Endpoint identity. `end_user` is the person the traffic runs for. On a direct login, `end_user` equals `user`. On an OIDC machine session, `user` is the machine user and `end_user` is the human. Both are unset in `pre_tcp`.

An input left out of `pre_tcp` is unset there, except the empty values noted below. A condition that reads an unset input does not match. `is_llm_hostname`, `socket_ecosystem`, and `process.cmdline` are empty in `pre_tcp`. `is_socket_ecosystem` is false in `pre_tcp`. `process.pid_version` is `0` in `pre_tcp`. `technology` is empty in `pre_tcp` and `pre_tls`. Do not negate an empty value. `!is_llm_hostname` matches every process in `pre_tcp`.

| Input | Type | Stages | Notes |
| - | - | - | - |
| `user.id` | string | `pre_tls`, `post_tls` | |
| `user.email` | string | `pre_tls`, `post_tls` | |
| `user.username` | string | `pre_tls`, `post_tls` | `idp:formal:human:example@example.com` or `idp:formal:machine:example` |
| `user.type` | string | `pre_tls`, `post_tls` | Always empty. |
| `user.external_ids` | array | `pre_tls`, `post_tls` | Always an empty string array. |
| `user.groups` | array | `pre_tls`, `post_tls` | String array of group names |
| `user.group_ids` | array | `pre_tls`, `post_tls` | String array of group IDs |
| `end_user.id` | string | `pre_tls`, `post_tls` | |
| `end_user.email` | string | `pre_tls`, `post_tls` | |
| `end_user.username` | string | `pre_tls`, `post_tls` | Same format as `user.username` when the session includes it |
| `end_user.type` | string | `pre_tls`, `post_tls` | Always empty. |
| `end_user.external_ids` | array | `pre_tls`, `post_tls` | Always an empty string array. |
| `end_user.groups` | array | `pre_tls`, `post_tls` | String array of group names |
| `end_user.group_ids` | array | `pre_tls`, `post_tls` | String array of group IDs |
| `hostname` | string | `pre_tcp`, `pre_tls`, `post_tls` | Destination hostname from macOS APIs or the Linux DNS proxy. Otherwise the destination IP address. |
| `port` | int | `pre_tcp`, `pre_tls`, `post_tls` | Destination TCP port. |
| `is_llm_hostname` | bool | `pre_tls`, `post_tls` | True when the hostname is `api.anthropic.com`, `claude.ai`, `chatgpt.com`, `cursor.sh`, or a subdomain of one of those. False in `pre_tcp`. |
| `socket_ecosystem` | string | `pre_tls`, `post_tls` | Package registry family: `npm`, `pypi`, `maven`, `cargo`, `rubygems`, `openvsx`, `nuget`, `go`, or `conda`. Empty for any other host, including in `pre_tcp`. |
| `is_socket_ecosystem` | bool | `pre_tls`, `post_tls` | True when `socket_ecosystem` is set. False in `pre_tcp`. |
| `process.is_amazon_q` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is Amazon Q Developer. |
| `process.is_amp` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is Amp. |
| `process.is_antigravity` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Antigravity. Linux matches process name `agy`, `antigravity`, or `antigravity-ide`. |
| `process.is_claude_cli` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Claude Code. Linux matches process name `claude` or `claude.exe`. |
| `process.is_claude_desktop` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Claude Desktop. Linux matches process name `claude-desktop`. |
| `process.is_codex_cli_rs` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Codex. Linux matches process name `codex`. |
| `process.is_copilot` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is GitHub Copilot CLI. |
| `process.is_copilot_xcode` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is GitHub Copilot for Xcode. |
| `process.is_crush` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is Crush. |
| `process.is_cursor` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Cursor. Linux matches process name `cursor`. |
| `process.is_devin_cli` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Devin. Linux matches process name `devin`. |
| `process.is_goose` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Goose. Linux matches process name `goose`. On macOS this matches an unsigned process named `goose`. |
| `process.is_grok_bot` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is `grok-bot`. |
| `process.is_hermes` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Hermes Agent. Linux matches process name `hermes`. |
| `process.is_kilo` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is Kilo Code. Linux matches process name `kilo`. On macOS this matches an unsigned process named `kilo`. |
| `process.is_opencode` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is OpenCode. Linux matches process name `opencode` or `opencode.exe`. |
| `process.is_pi` | bool | `pre_tcp`, `pre_tls`, `post_tls` | Linux only. True when the connecting process or an ancestor has process name `pi`. |
| `process.is_warp` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is Warp Agent CLI. |
| `process.is_windsurf` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process or an ancestor is Windsurf. |
| `process.is_agent` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process matches any per-agent flag. On Linux this uses the Linux process names in those rows. A macOS-only flag stays false on Linux. |
| `process.has_agent_ancestor` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process is a known agent or has one in its ancestor list. On Linux this uses the Linux process names in the per-agent rows. |
| `process.is_browser` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when the connecting process is a known browser. In `pre_tcp`, Apple WebKit helper processes (such as Safari's networking process) don't inherit their browser's verdict, so Safari traffic can evaluate `false`. |
| `process.is_git` | bool | `pre_tcp`, `pre_tls`, `post_tls` | True when the connecting process or an ancestor is the git CLI, which covers the `git-remote-https` and `ssh` processes git starts. On macOS this matches Apple's Xcode git (`/usr/bin/git`) by code signature and unsigned builds such as Homebrew git by process name `git`. On Linux this matches process name `git`. |
| `process.pid` | int | `pre_tcp`, `pre_tls`, `post_tls` | PID of the immediate connecting process. |
| `process.pid_version` | int | `pre_tls`, `post_tls` | macOS audit-token PID version of the immediate process. `0` on Linux and in `pre_tcp`. |
| `process.name` | string | `pre_tcp`, `pre_tls`, `post_tls` | Process name of the immediate connecting process, for example `claude` |
| `process.cmdline` | string | `pre_tls`, `post_tls` | Process command line of the immediate connecting process, for example `claude -p "echo hello"`. Empty in `pre_tcp`. |
| `process.signing.team_id` | string | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. Team ID of the signed immediate connecting process, for example `Q6L2SF6YDW` for Anthropic. |
| `process.signing.bundle_identifier` | string | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. Bundle ID of the signed immediate process, for example `com.anthropic.claude-code`. |
| `process.signing.code_identifier` | string | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. Code ID of the signed immediate process, for example `codex` for the Codex CLI. |
| `process.signing.apple_signed` | bool | `pre_tcp`, `pre_tls`, `post_tls` | macOS only. True when Apple signed the immediate connecting process. |
| `process.ancestors` | array | `pre_tcp`, `pre_tls`, `post_tls` | Parent processes. Each object has `pid`, `pid_version`, `name`, `cmdline`, and `signing` (`code_identifier`, `bundle_identifier`, `team_id`, `apple_signed`). Ancestor `cmdline` is empty and `pid_version` is `0` in `pre_tcp`. |
| `os` | string | `pre_tcp`, `pre_tls`, `post_tls` | `linux` or `macos` |
| `resource.id` | string | `pre_tls`, `post_tls` | Resource ID when a Formal resource matches the hostname and port |
| `resource.name` | string | `pre_tls`, `post_tls` | Resource name when a Formal resource matches the hostname and port |
| `resource.hostname` | string | `pre_tls`, `post_tls` | Hostname of that Formal resource |
| `resource.hostname_name` | string | `pre_tls`, `post_tls` | Always empty. |
| `resource.environment` | string | `pre_tls`, `post_tls` | Always empty. |
| `resource.port` | int | `pre_tls`, `post_tls` | Port of that Formal resource |
| `resource.provider` | string | `pre_tls`, `post_tls` | `aws-ec2` or `aws-ecs` when the resource sets a provider. Empty otherwise. |
| `resource.space_id` | string | `pre_tls`, `post_tls` | Always empty. |
| `resource.technology` | string | `pre_tls`, `post_tls` | Technology, such as `mcp`, when a Formal resource matches the hostname and port |
| `technology` | string | `post_tls` | Classified wire protocol: `http`, `mcp`, `llm`, `aws`, `gcp`, `postgres`, `mysql`, `mariadb`, `mongodb`, or `ssh`. Empty in `pre_tcp` and `pre_tls`. |

## Next Steps

> **[Formal Endpoint](https://docs.formal.ai/docs/guides/client-apps/desktop-app.md#transparent-mode)**
>
> Install and enable Transparent Mode

> **[Write Policies](https://docs.formal.ai/docs/guides/policies/introduction.md)**
>
> Enforce rules on intercepted traffic


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.