# Rules and CEL

> Every rule is a sentence over a CEL expression with one setting from Off to Block. How rules are written, tested, rolled out and promoted.

A rule is a condition on a tool call and what to do when it matches. You write it as a sentence or as CEL, test it on the last 30 days of your own traffic, and set it on one dial from Off to Block. The Golem agent on each machine checks every call against the rules that reach it before the call runs, from expressions compiled ahead of time: no model and no network hop in the decision.

Rules live in the console under **Detect & Enforce** → **Protections**. **+ New rule** opens the editor; the **Packs** tab holds the rule packs; **History** lists every change, who made it, and is signed into your record.

## The one setting

Each rule has one setting, the same five steps everywhere in Golem:

| Setting | What happens when it matches |
|---|---|
| Off | Not checked. |
| Watch | The call goes ahead and the agent is told nothing. Use it to see what a rule would catch. |
| Warn | Recorded and an alert is raised. Where the agent's hook can take it, the agent is told why, as context it can act on. The call goes ahead. |
| Ask | The call waits for a decision. You choose who decides when you pick Ask. |
| Block | Recorded and refused before it runs, with the rule's name and a decision id; or, for a rule on a tool's result, the result is withheld before the agent reads it. |

When several rules match one call, the strictest wins. Only a move onto Ask or Block can stop an agent, so only those pause for you to confirm; anything else applies at once, with Undo.

Ask has three deciders:

- **The person at the keyboard.** The agent's own approval prompt asks them.
- **Your team.** The call waits in the Review Queue until someone approves it.
- **Golem's reviewer agent.** A Golem-hosted agent decides, and hands it to a person when it is unsure. Available once an owner or admin turns it on under **Protections** → **Settings** → **Asks for review**.

Which of these can actually hold a call depends on the agent and the path. See [Block, hold and contain](/response) for what each looks like and where a hold becomes a refusal.

## Write a rule

The editor has two tabs that write the same rule.

**Sentence** reads "When an agent *does something* *matching this*, *setting*." The verbs are:

- runs a shell command matching
- calls an MCP tool named
- writes a file at
- fetches a URL matching

**CEL** shows the expression the sentence wrote, with the fields you can use as chips to click in. Anything the sentence cannot say, write here. Once you edit the CEL by hand, the CEL tab is what saves. A rule you open later opens on the CEL tab.

Each rule also has:

- **Message to the agent**, up to 500 characters, which a refused or warned agent sees beside the rule's name. Use it to say what to do instead.
- A severity: Benign, Suspicious, Dangerous or Malicious.
- Under **Advanced**, where it runs: **Inline** (on the Golem agent, as the call happens; the only place a rule can warn, ask or block) and **Periodic** (over the record, about every 30 seconds; it raises alerts only, but also sees calls that reached Golem without passing through a Golem agent). And **Taint the session**, which locks the session's shell down after a match (see [The shell jail](/shield#the-shell-jail)).

A new rule saves in Watch. The console refuses a rule that can never do what it says, such as Block on a field only the record has.

## Fields a rule can read

A rule is one CEL expression over a single variable, `entry`, that returns true or false. It uses standard CEL: `==`, `in`, `&&`, `||`, `contains`, `startsWith`, `endsWith`, `size`, `has`, and `matches` for regular expressions (RE2). There are no custom functions.

| Field | Type | What it is | Where |
|---|---|---|---|
| `entry.tool_name` | string | The tool: `Bash`, `Edit`, `WebFetch`, or an MCP tool's name. Shell calls from Cursor, Gemini CLI and Antigravity arrive as `Bash` too. | Inline, Periodic |
| `entry.bash_command_text` | string | The full command line, for `Bash` calls. Empty for other tools. | Inline, Periodic |
| `entry.tool_arguments` | map | The call's arguments, for example `entry.tool_arguments['file_path']`. | Inline, Periodic |
| `entry.mcp_server_id` | string | The MCP server the tool belongs to. | Inline, Periodic |
| `entry.agent_id` | string | The agent, for example `claude-code-builtin`. Empty on calls through Golem's MCP gateway. | Inline, Periodic |
| `entry.host_id` | string | The machine. | Inline, Periodic |
| `entry.session_id` | string | The session. | Inline, Periodic |
| `entry.permission_mode` | string | How the session runs: `default`, `plan`, `acceptEdits`, `auto`, `dontAsk` or `bypassPermissions`. Cursor's run modes are mapped onto these. | Inline, Periodic |
| `entry.session_tool_calls`, `entry.session_calls_last_60s` | int | How many calls this session has made, in all and in the last minute. | Inline |
| `entry.session_prior_tools`, `entry.session_servers` | list | Tools and MCP servers this session has already used. | Inline |
| `entry.session_tainted` | bool | Whether the session has been locked down. | Inline |
| `entry.session_max_injection_score` | double, 0 to 1 | The highest prompt-injection score of anything this session has read. | Inline |
| `entry.tool_description_changed` | bool | Whether the MCP tool's description changed since Golem last saw it. | Inline |
| `entry.result_injection_score` | double, 0 to 1 | The prompt-injection score of this call's result. Checked when the result arrives. Needs the prompt-injection check on. | Inline |
| `entry.shield_denials` | int | How many operations the shell jail refused for this command. Checked when it finishes. | Inline |

A field a check does not fill is empty, so a rule on it never matches there. The editor shows which fields each place fills.

## Examples

These are rules Golem ships. Copy one as a starting point.

**Shell command with permission prompts off.** Ships at Watch. At Ask, it puts a person back in front of shell commands in a session that runs without its prompts.

```text
entry.permission_mode == 'bypassPermissions' && entry.tool_name == 'Bash'
```

**Force-push or branch delete on a protected branch.** Part of the starter set, at Watch.

```text
entry.tool_name == 'Bash' && (
  entry.bash_command_text.matches('\\bgit\\s+push\\s+(-f|--force(-with-lease)?)\\s+\\S+\\s+(main|master|prod|release/[^\\s]+)') ||
  entry.bash_command_text.matches('\\bgit\\s+(branch\\s+-D|update-ref\\s+-d)\\s+.*\\b(main|master|prod)\\b')
)
```

**Prompt injection in a tool result.** Golem's built-in, at Block but shipped in shadow. With the prompt-injection check on, a result scored at 0.98 or above is withheld from the agent once you enforce it.

```text
entry.result_injection_score >= 0.98
```

**Block catastrophic shell commands.** Golem's built-in, at Block and on from the first install. It matches `rm -rf` against `/`, `~`, `$HOME` or a home directory, `dd`, `mkfs` or `shred` to a raw disk, and the fork bomb, each anchored to the start of a command so a mention inside a string does not match.

A rule of your own, keeping writes away from your CI configuration:

```text
entry.tool_name in ['Edit', 'Write'] &&
  string(entry.tool_arguments['file_path']).contains('/.github/workflows/')
```

## Rule packs, Shadow and Enforce

Rules are grouped in **rule packs**, and packs reach machines through **profiles**: the Default profile covers the workspace, and ranked profiles cover groups or single machines. Under **Protections** → **Profiles**, a profile's **Rule packs** section adds a pack (search as you type) or creates a new one.

Each pack on a profile is **Enforce** or **Shadow**:

- **Enforce.** Rules act on their setting: warn, ask or block.
- **Shadow.** Every rule only records what it would have done ("would have blocked"), and the agent is not warned. Nothing is enforced on those machines.

If any profile reaching a machine carries a pack in Enforce, the machine enforces it. Moving a pack to Enforce asks you to confirm; moving one to Shadow is recorded as loosening a protection and raises an alert. Golem's built-ins appear as **Golem built-ins**, always enforced. To stop one, set it to Off, which hides it in your workspace; an owner or admin can forbid that with a workspace minimum.

### Rolling out

Moving a rule up from Off or Watch to Ask or Block does not take effect at once. The rule watches first for the period set under **Protections** → **Settings** → **Rolling out rules** (**New rules watch first for** 3, 7 or 14 days; 7 by default), then Golem moves it to the step you chose and signs the change.

## Test a rule

**Test on last 30 days** in the editor runs the expression over your own record and reports "Would have matched N of M calls", with up to five example calls and any fields the record does not have. It reads at most the newest 20,000 entries in that window, and saves nothing. Run it before every move to Ask or Block. Testing is not required to save.

An agent connected to Golem's MCP can check a single expression with `test_auto_flag_rule`, against a sample call or one entry from the record. It does not search past traffic.

## From a finding to a rule

When Golem's investigators find something in a session, they can propose a rule that would have caught it. The proposal is never enforced on the investigator's say-so. It has to earn it, and with the default settings a person approves every one:

1. **Replayed against the evidence.** It must match every entry of the evidence it was written for.
2. **Measured in shadow.** It runs in Shadow, in a **Defender proposals** pack, over traffic nobody chose for it: at least 200 calls outside the evidence, for at least a day (**Min shadow time**), matching no more than 5% of them (**Match-rate ceiling**), with no false positives.
3. **Waits for a person.** A rule that would block a tool on the **Tools it may never block on its own** list goes to a person whatever the other settings say. **Require a person to approve every enforce** is on by default, so every proposal does. One that has not qualified after seven days goes to a person too.
4. **Rolled back automatically.** Once enforced, if its matches climb past the **Revert ceiling** (10% of recent traffic over a 15-minute **Revert window**, after at least 10 matches), it goes back to Shadow.

These live under **Detect & Enforce** → **Defender**, which is off until an owner or admin turns it on. See [Session review](/review) for the investigators themselves.

## What rules do not do

- A rule judges one call, with the session facts above. It does not read a payload Golem has not been given: arguments are what the agent sent, and results only where the result passes through Golem.
- A model never makes the decision. The prompt-injection check scores what an agent read, and a rule on that score decides. Golem's reviewer agent decides only the Asks a rule hands to it.
- A rule cannot stop what it never sees. A hook is only as strong as the client that calls it, and a shell command that has started can run other programs without asking again. See [Limits](/limits).
