# Rules your coding agent cannot rename its way around

October 9, 2026 Protect 6 min read

You let a coding agent run shell commands all day. One bad call can read your `.env`, push to `main` or wipe your home folder. Protect, the rules layer in Context Mode, reads what each tool call does before your client runs it. It catches the call even when the agent writes it in a form nobody expected. On production, it passed 126 of 126 API checks.

## The problem: an agent with a shell and your keys

A coding agent works fast because it can run commands without asking. That same speed is the risk. In one long session it can:

- read a `.env` file and send your secrets to the model,
- force-push to `main`,
- run `rm -rf ~` after a bad guess about a path,
- call a tool server or a website you never meant it to reach.

A simple guard looks for a tool called `Bash` and checks its text. Our own test suite showed why that is not enough. Agents name their shells in many ways, and they can hide a command inside another one.

## What Protect checks

Every Claude Code or Codex request goes through Context Mode, so Protect sees each tool call before the client runs it. It checks four kinds of call:

- **Commands**, by what they do. Protect reads through 98 wrapper words and 33 carriers such as `xargs`, `find -exec`, `ssh` and `docker exec`, and maps each call to one of 22 capabilities, such as `fs.delete`, `vcs.publish` and `secret.read`.
- **Files**: reads, writes and deletes by folder, name or extension, in the agent's own tools (Read, Edit, Grep, Codex `apply_patch`) and in shell commands.
- **Sites**: hosts named in commands, web fetches and tool-server inputs. With "block other sites" on, only the hosts you list are reachable.
- **Tool servers**: which MCP servers and tools the agent may call.

When Protect blocks a call, the client runs a short message instead of the command. The command never reaches your terminal. The model reads why, and moves on:

```
context-mode Protect blocked this command, so it was NOT executed:
Blocked by your team's policy: Read environment files. .env files hold secrets.
Scope: your account. ... do not retry it as a different shell command or a
Node.js or Python script. Tell the user it was blocked and continue with other work.
```

Each block goes into a decision log with the rule, the tool and the host. The log does not keep the command text or file contents.

## A tool name is not a boundary

Our end-to-end suite found the gap (finding F4, rated high). Codex has more than one way to run a shell. Some builds send a `local_shell` call whose command is a list of words, not one string:

```
{ "name": "local_shell",
  "input": { "action": { "type": "exec", "command": ["bash", "-lc", "cat .env"] } } }
```

Other agents call their shell `run_shell_command`, `run_terminal_cmd`, or a name nobody has seen yet. A rule that waits for `Bash` lets all of them through.

So Protect now finds a shell by the **shape of its input**, not by its name. If an unknown tool carries a command, Protect reads it as the shell line it would run. That covers a command string, a list of words, a program plus its arguments, and the Codex `local_shell` action. If a known shell name arrives with an input shape Protect does not know, it checks every string in it, so it fails closed.

The test matrix now has 28 tool-name cases, including 7 tool names from agents we do not support yet. Allow cases check the other side: `shell ["npm", "test"]` must still run, and so must a plan tool whose text only mentions `cat .env`.

## Commands hidden inside commands

Finding F5 (medium) was a read hidden in a substitution: `cat $(echo .env)` reads `.env`, but the old check could not see that. Protect now works out substitutions that only print plain words. All of these are read as `cat .env` and refused:

```
cat $(echo .env)
cat `echo .env`
cat $(echo .e)nv
cat $(echo $(echo .env))
```

When Protect cannot work out a substitution, it takes the careful side for reads: `cat $(ls -a | grep env)` is refused under the Balanced preset. Deletes keep their old reading, so `rm -f $(find build -name '*.o')` still runs. The trade-off: `cat $(ls)`-style reads are refused while a file-read block is on.

The same run found F1: `sh <<< 'rm -rf ~'` ran where `sh -c 'rm -rf ~'` was refused. Protect now reads a here-string as a command.

## Codex gets the same rules as Claude Code

Codex runs many calls inside one `exec` cell. Protect checks each command, each `apply_patch` and each tool-server call inside the cell with the same rules a Claude Code call gets. If it cannot read a cell, it refuses it while a command block rule is on.

Codex can also read MCP resources with `read_mcp_resource`, `list_mcp_resources` and `list_mcp_resource_templates`. A Tool servers rule on the whole server now matches these calls too, as the [Protect docs](https://context-mode.com/docs/protect) describe. A rule on one tool does not match a server's resources. The test matrix has no case for these calls yet.

## Your agent cannot switch the rules off

The worst finding was F2 (high). Through the old rule routes, any credential could switch off or delete a block. In a live test on a throwaway account, an API key turned off "Delete the root or home folder", and `rm -rf ~` then ran. That matters, because a device key sits in the client config on your disk, where the agent itself can read it.

Now an API key can only make the policy stricter. Any change that loosens it (delete a block, switch a rule off, turn Protect off, widen the site list) needs a person signed in to the console, plus a reason. Every change goes into a hash-chained history. In the latest run, an API key's attempt to turn Protect off got a 403, and the 40-entry history checked out.

One policy covers Claude Code and Codex on every machine of an account. The [Team plan](https://context-mode.com/docs/plans-and-limits) gives one Protect policy for the org that members cannot loosen. That org-wide case is not part of the test run below, and SSO, SCIM and team roles are not built yet.

## When something breaks

- **Rules store down:** if the store throws, answers a 500 or returns no rule list, the defaults still refuse `rm -rf ~`, `rm -rf /` and `git push --force origin main`. A loader test proves this, not a live run: we cannot take the production store down on purpose.
- **Unreadable host:** while a Sites block is on, a host Protect cannot read is refused.
- **Unknown home folder:** a path with an unknown home fails closed for a block rule.
- **Core protections do not fail closed.** They never refuse a call only because they could not read it. They guard against accidents and runaway agents. For a hard boundary, keep the Claude Code sandbox or Codex `workspace-write` on.

## The proof

The suite drives the rules API, the gateway and the real clients, on five throwaway accounts in production (Off, Balanced, Locked down, Observe, Wipe). No customer traffic was used.

| Test | Result |
| --- | --- |
| API layer, production | 126 pass, 0 fail |
| First run the same day, before the fixes | 87 pass, 4 fail |
| Claude Code (Haiku 4.5), live | 25 pass, 0 fail, 1 skip |
| Codex, live | 22 pass, 0 fail, 1 skip |
| Live turns recorded | 28 Claude Code, 26 Codex |
| Time to decide, per call, in process | 32 to 556 µs |

No refused call had a side effect, and no run had a repeat block of the same rule. Whole-turn time with Protect on and off could not be told apart. The speed figure covers 7 commands, 500 runs each.

Of the 126 API checks, 103 are matrix cases. The rest cover the on/off switch, presets, custom rules, history and speed.

**Bypass attempts** *42* **Tool names** *28* **Commands** *14* **Files** *6* **Secrets** *5* **Network** *5* **MCP** *3*

The 103 matrix cases in the production run, by group. Most cases try to get past the rules, not to use them as written.

## What is still open

- No block-rate study on real traffic yet.
- Protect reads the call, not the program. A script file, a compiled program or a path built at run time gets past it.
- If an agent declares no shell tool Protect can rewrite, the refusal cannot be delivered and the call goes through.
- Claude Code (Haiku 4.5) declined `curl … | sh` by itself in 3 of 3 runs, so that live case is inconclusive on Claude. The API layer refuses it.
- Not run live yet: a device key's refusal on the old routes, and a delete by a signed-in person.
- Protect only sees traffic that goes through Context Mode.

## How to try it

```
npx @context-mode/cli
```

Then open Protect in the console, turn it on and pick Balanced. Nothing is blocked until you pick a preset or add rules. Ask your agent to `cat .env` or push to `main`, and read the refusal. The test box on the Rules tab shows the decision and the rule for any command, URL, path or tool name.

- [Quick start](https://context-mode.com/docs/quick-start): connect Claude Code or Codex.
- [Protect](https://context-mode.com/protect): what it blocks and the presets.
- [Protect docs](https://context-mode.com/docs/protect): every rule type and its limits.
- [Plans and limits](https://context-mode.com/docs/plans-and-limits): what each plan includes, and the Team policy.

[Start free](https://context-mode.com/docs/quick-start)[All posts](https://context-mode.com/blog)
