Rules your coding agent cannot rename its way around

Protect6 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:

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:

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 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 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

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.

TestResult
API layer, production126 pass, 0 fail
First run the same day, before the fixes87 pass, 4 fail
Claude Code (Haiku 4.5), live25 pass, 0 fail, 1 skip
Codex, live22 pass, 0 fail, 1 skip
Live turns recorded28 Claude Code, 26 Codex
Time to decide, per call, in process32 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 attempts42
Tool names28
Commands14
Files6
Secrets5
Network5
MCP3
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

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.