cfgaudit

Rules / CFG011

CFG011 MCP server alwaysAllow is too broad

Severity: warn OWASP: LLM03:2026 – Excessive Agency OWASP MCP: MCP02:2025 – Privilege Escalation via Scope Creep — provisional (MCP Top 10 v0.1)

What cfgaudit checks

cfgaudit inspects each mcpServers.*.alwaysAllow list — in both settings.json and the project's .mcp.json — and flags when it is too permissive. alwaysAllow skips the per-call confirmation prompt — that is only appropriate for read-only, low-risk tools.

A finding is emitted when the list:

  1. Contains the wildcard "*" — auto-approves every tool the server exposes, including ones added in future updates.
  2. Includes a state-mutating tool name (substring match, case-insensitive): write, delete, remove, edit, exec, run_command, shell, bash, kill, patch, move_file, rename, create_file, create_directory.
  3. Has 10 or more entries — even when each entry is benign, blanket auto-approval at this scale is rarely intentional.

The first matching condition reports; once the wildcard fires there is no point listing individual tools.

Trigger example

{
  "mcpServers": {
    "shell": {
      "command": "mcp-server-shell",
      "alwaysAllow": ["*"]
    },
    "fs": {
      "command": "mcp-server-filesystem",
      "alwaysAllow": ["read_file", "write_file", "delete_file"]
    }
  }
}

Safe alternative

Limit alwaysAllow to truly read-only tools and let the user confirm everything else:

{
  "mcpServers": {
    "fs": {
      "command": "mcp-server-filesystem",
      "alwaysAllow": ["read_file", "list_directory"]
    }
  }
}

References

Codex spells the same decision as an approval mode

OpenAI Codex has no alwaysAllow list. A [mcp_servers.<name>] table sets default_tools_approval_mode for the whole server, and a [mcp_servers.<name>.tools.<tool>] table sets approval_mode for one tool. Both take auto | prompt | writes | approve, and only one of those values removes the prompt. From codex-rs/core/src/mcp_tool_call.rs:

match approval_mode {
    AppToolApproval::Auto => requires_mcp_tool_approval(annotations),
    AppToolApproval::Prompt => true,
    AppToolApproval::Writes => !annotations.and_then(|a| a.read_only_hint).unwrap_or(false),
    AppToolApproval::Approve => false,
}

Approve never asks; Auto is the default and Prompt / Writes narrow. cfgaudit therefore reports only approve:

mcp_servers is not on Codex's project-layer denylist, so a committed .codex/config.toml sets both. Project layers are loaded but disabled while the directory is untrusted, the same caveat CFG063 and CFG064 carry.

Plugin MCP servers

The same two keys also exist as an overlay for an MCP server that a Codex plugin brings: [plugins."<plugin>@<marketplace>".mcp_servers.<srv>] with default_tools_approval_mode, and [...mcp_servers.<srv>.tools.<tool>] with approval_mode (#647). The repository does not declare the server; the overlay pre-approves it for everyone who has that plugin installed, and plugin ids are predictable. Measured with codex 0.162.1 app-server config/read: in a trusted directory both values came back in the effective config unchanged, and the same file in an untrusted directory contributed an empty plugins table. They are reported like the [mcp_servers] form.

The same decision again under [apps]

Codex's app/connector controls repeat the approval keys at four more positions, and none of them is the [mcp_servers] table:

Position Key What it covers
[apps._default] default_tools_approval_mode every tool of every connector
[apps.<id>] default_tools_approval_mode one connector, including tools it gains later
[apps.<id>.links.<link>] default_tools_approval_mode one connected account inside a connector
[apps.<id>.tools.<tool>] approval_mode one tool

The value is the same AppToolApproval enum, so the reading above carries over unchanged. restrict_to is what fixes its ordering (codex-rs/config/src/mcp_types.rs):

(Self::Prompt, _) | (_, Self::Prompt) => Self::Prompt,
(Self::Approve, mode) | (mode, Self::Approve) => mode,

Approve is the identity of the restriction lattice, so it is the value that never adds an approval requirement. The chain that consumes these positions is managed → tool → link → app → _default → Auto (codex-rs/connectors/src/app_tool_policy.rs, app_tool_policy_from_apps_config), and Auto is the fallback, which is strictly stricter than Approve.

cfgaudit reports the three blanket positions on sight. The per-tool position gets the same judgement as its MCP twin: a state-mutating name is the finding, and a single read-only tool is not, so the two spellings of one decision do not have two different noise levels.

apps is not on PROJECT_LOCAL_CONFIG_DENYLIST (the denylisted apps_mcp_product_sku is a different root key) and sanitize_project_config does not touch the table, so a committed value crosses into the project layer like mcp_servers.

Where these values come from

The per-tool position is one Codex writes itself. Approving an app tool permanently calls persist_codex_app_tool_approval (codex-rs/core/src/mcp_tool_call.rs), which writes exactly apps.<connector_id>.tools.<tool_name>.approval_mode = "approve". It writes through ConfigEditsBuilder::for_config, which resolves to the user config file, and unlike the MCP path next to it there is no project-folder branch. So a value at that position in a repository is a user config that was committed, or a hand-authored one; the other three positions have no writer in the code base at all.

Either way the finding stands: a committed file applies to everyone who clones the repository, which is the same argument CFG067 makes for hooks.

Deliberately not reported

destructive_enabled and open_world_enabled read like grants, but they resolve with .unwrap_or(true) at both the app and the _default level, so a committed true restates the default and only false changes anything, in the hardening direction. default_tools_enabled and enabled are capability rather than posture.

[apps._default] is also never reported for a tools or links table under it. Upstream models the defaults as a distinct type with deny_unknown_fields, so a file that writes one does not load at all, and a finding there would name configuration Codex never reads.

Documentation for cfgaudit 1.18.1. Source of this page · Rule implementation