Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/indexnow.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const URLS = [
`https://${HOST}/guides/claude-code`,
`https://${HOST}/guides/secret-exfiltration`,
`https://${HOST}/guides/ecc`,
`https://${HOST}/guides/cursor`,
`https://${HOST}/guides/codex`,
`https://${HOST}/guides/vscode`,
`https://${HOST}/guides/gemini-cli`,
`https://${HOST}/docs`,
`https://${HOST}/docs/design`,
`https://${HOST}/docs/hooks`,
Expand Down
36 changes: 36 additions & 0 deletions src/data/guides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,42 @@ export const GUIDES: readonly Guide[] = [
blurb:
"ECC checks your setup and makes the agent look before it edits. This checks what each call does and what each result says, and runs next to ECC's hooks without changing them.",
},
{
slug: 'cursor',
h1: 'Screen MCP tool calls in Cursor',
title: 'Screen MCP tool calls in Cursor | agent-chaperone',
description:
"Put Cursor's MCP servers behind agent-chaperone: the mcp.json change, getting the API key to the proxy, checking it works, and what it does not cover.",
blurb:
'Cursor keeps servers in JSON that wrap can edit. The change, how the key reaches the proxy, and what Cursor runs outside MCP.',
},
{
slug: 'codex',
h1: 'Screen MCP tool calls in Codex',
title: 'Screen MCP tool calls in Codex | agent-chaperone',
description:
"Put Codex's MCP servers behind agent-chaperone: the config.toml change, forwarding the API key past a cleared environment, and timeouts.",
blurb:
'Codex keeps servers in TOML and starts them with a cleared environment, so the key has to be forwarded by name. The change, by hand, and the timeouts to set.',
},
{
slug: 'vscode',
h1: 'Screen MCP tool calls in VS Code',
title: 'Screen MCP tool calls in VS Code | agent-chaperone',
description:
"Put VS Code agent mode's MCP servers behind agent-chaperone: the mcp.json change, the API key, when wrap can edit the file, and what it misses.",
blurb:
'VS Code lists servers under servers rather than mcpServers and allows comments in the file. The change, the key, and where wrap has to give way to a hand edit.',
},
{
slug: 'gemini-cli',
h1: 'Screen MCP tool calls in Gemini CLI',
title: 'Screen MCP tool calls in Gemini CLI | agent-chaperone',
description:
"Put Gemini CLI's MCP servers behind agent-chaperone, and get the API key past the filter that strips anything named like a credential.",
blurb:
"Gemini CLI strips variables named like credentials from a server's environment, the API key included. The change, and the env line that gets the key through.",
},
];

export const SITE = 'https://agentchaperone.dev';
Expand Down
2 changes: 1 addition & 1 deletion src/data/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export function guidesIndexSchema(): unknown {
'@id': GUIDES_ID,
name: 'agent-chaperone guides',
description:
"Setting up screening for MCP servers, for a client's own shell and file tools, for prompt injection arriving in tool results, for secrets on their way out, and next to ECC's hooks.",
"Setting up screening for MCP servers, including in Cursor, Codex, VS Code and Gemini CLI, for a client's own shell and file tools, for prompt injection arriving in tool results, for secrets on their way out, and next to ECC's hooks.",
url: `${SITE}/guides`,
isPartOf: { '@id': SITE_ID },
about: { '@id': APP },
Expand Down
182 changes: 182 additions & 0 deletions src/pages/guides/codex.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
import Base from '../../layouts/Base.astro';
import { REPO } from '../../data/benchmark';
import { findGuide, guideUrl } from '../../data/guides';
import { guideSchema } from '../../data/schema';

const guide = findGuide('codex');
---

<Base
title={guide.title}
ogTitle={guide.h1}
description={guide.description}
canonical={guideUrl(guide)}
schema={guideSchema(guide)}
>
<section>
<h1>{guide.h1}</h1>
<p class="lede">
Codex starts each MCP server in its configuration as a local process and talks to it over
stdio. agent-chaperone goes in front of that process. It reads each tool call before the
server gets it and each result before the agent does, and nothing about the server changes.
</p>
<p>
Two things about Codex differ from the JSON-configured clients, and both matter here: the
configuration is TOML, and a server does not see your environment unless you forward it. This
covers MCP servers only. Codex's own shell commands and patches do not go through MCP.
</p>
</section>

<section id="install">
<h2>
Install it{' '}
<a class="anchor" href="#install" aria-label="Link to this section">
#
</a>
</h2>
<pre><code>npm install -g agent-chaperone</code></pre>
<p>
A global install matters more on Codex than elsewhere. Codex gives a server a fixed time to
start, and running the proxy through <code>npx</code> spends part of it looking the package
up.
</p>
</section>

<section id="wrap">
<h2>
Put a server behind it{' '}
<a class="anchor" href="#wrap" aria-label="Link to this section">
#
</a>
</h2>
<p>
Servers live in <code>~/.codex/config.toml</code>, one <code>[mcp_servers.&lt;name&gt;]</code>{' '}
table each, and in <code>.codex/config.toml</code> inside a project Codex trusts. The CLI and
the IDE extension read the same file. <code>agent-chaperone wrap</code> only edits JSON, so on
Codex the change is made by hand. A server before:
</p>
<pre><code>{`[mcp_servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/project"]`}</code></pre>
<p>And after:</p>
<pre><code>{`[mcp_servers.filesystem]
command = "agent-chaperone"
args = ["--server", "filesystem", "--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/me/project"]
env_vars = ["TYPESAFE_API_KEY"]
startup_timeout_sec = 30`}</code></pre>
<p>
The old command moves after <code>--</code>, unchanged. <code>--server</code> names the
section of the policy file that applies, and using the table's own name keeps the two easy to
match. Keep any <code>env</code> or <code>cwd</code> the entry already had: they still reach
the server.
</p>
</section>

<section id="key">
<h2>
Give it the API key{' '}
<a class="anchor" href="#key" aria-label="Link to this section">
#
</a>
</h2>
<p>
Codex starts a server with a cleared environment and passes back only a short list of
variables: <code>HOME</code>, <code>PATH</code>, <code>USER</code>, the locale and a few
others. A <code>TYPESAFE_API_KEY</code> exported in your shell is not on that list, so without
the <code>env_vars</code> line above the proxy never sees it and every judgment records that
no model was asked.
</p>
<p>
<code>env_vars</code> forwards the variable by name from the environment Codex was started in,
so the key is never written into the file. If Codex was started some other way and the key
does not arrive, <code>env</code> sets it directly, at the cost of the key sitting in{' '}
<code>config.toml</code> in plain text:
</p>
<pre><code>{`env = { TYPESAFE_API_KEY = "..." }`}</code></pre>
<p>
The proxy removes its own keys before it starts the server behind it, so the server never
receives <code>TYPESAFE_API_KEY</code> either way.
</p>
</section>

<section id="timeouts">
<h2>
Timeouts{' '}
<a class="anchor" href="#timeouts" aria-label="Link to this section">
#
</a>
</h2>
<p>
<code>startup_timeout_sec</code> is how long Codex waits for a server to start, and{' '}
<code>tool_timeout_sec</code> how long it waits for one call. Codex's documentation and its
source disagree on the defaults, 10 and 60 seconds against 30 and 300, so set the startup one
yourself rather than depend on either.
</p>
<p>
Screening adds at most one request to the model backend per call and one per result, and{' '}
<a href="/docs/benchmark">the benchmark method</a> records how long those took. A tool timeout
that suited the server before still suits it unless its calls were already close to the limit.
</p>
</section>

<section id="check">
<h2>
Check that it is working{' '}
<a class="anchor" href="#check" aria-label="Link to this section">
#
</a>
</h2>
<p>
<code>/mcp</code> inside a Codex session, or <code>codex mcp list</code> from the shell, shows
whether the server started. Then use the server once from the agent and read what was decided:
</p>
<pre><code>agent-chaperone log</code></pre>
<p>
Each line is one call or result, with the decision and, in shadow mode, what enforcing would
have done instead. A line ending in <code>[not screened]</code> was decided without a model,
which means the key did not arrive.
</p>
</section>

<section id="cannot">
<h2>
What this does not cover{' '}
<a class="anchor" href="#cannot" aria-label="Link to this section">
#
</a>
</h2>
<ul class="plain">
<li>
<b>Codex's own tools.</b> Shell commands and file patches that Codex runs itself never cross
an MCP connection. Codex has hooks for them, and an adapter for those{' '}
<a href={`${REPO}/issues/78`}>does not exist yet</a>.
</li>
<li>
<b>Web search.</b> Codex runs it as a hosted tool, not on your machine, so nothing local
sees it.
</li>
<li>
<b>What a server does rather than what the call says.</b> This is not a sandbox. It reads
the call and the result, and a server that does something its arguments did not describe is
outside it.
</li>
</ul>
</section>

<section id="next">
<h2>
Next{' '}
<a class="anchor" href="#next" aria-label="Link to this section">
#
</a>
</h2>
<p>
It starts in shadow mode, which blocks nothing.{' '}
<a href="/guides/mcp-security">The MCP guide</a> covers what each screen asks, how to read a
shadow-mode log, and when to switch to enforce. The exact wording of every question is in{' '}
<a href="/docs/design">the design document</a>.
</p>
</section>
</Base>
Loading
Loading