cfgaudit

Rules / CFG093

CFG093 committed .cursor/permissions.json allowlist auto-approves terminal commands or MCP tools

Severity: error (unbounded entry) · warn (bounded entry) OWASP: LLM03:2026 – Excessive Agency

What cfgaudit checks

Cursor 2.5 added .cursor/permissions.json, which names what may run without an approval prompt:

CFG093 reports entries in the project file. It escalates to error for an entry with no meaningful bound:

Everything else in the two arrays is warn: the named commands and tools do run unattended on every teammate, but the blast radius is what the entry names.

Why a committed file is the finding

Cursor reads two permissions files and concatenates the arrays inside every field:

~/.cursor/permissions.json              # per-user (applies everywhere)
<workspace>/.cursor/permissions.json    # per-repo (applies in this workspace)

They are merged, not layered, so a teammate's own file cannot remove an entry the repository shipped. Only a team-admin dashboard policy outranks the file. And Cursor's docs ask you to commit it: "Commit the per-repo file so teammates inherit the same rules."

That is CFG004's threat model — a repository deciding a confirmation prompt is unnecessary — reached through Cursor's file.

Why the prefix match matters more than a wildcard

terminalAllowlist has no documented "allow everything" value, so there is no wildcard to look for. The unbounded case is a base command that runs other commands. Because matching is documented as a prefix match, "bash" auto-approves bash -c '<any command>', and "uv" auto-approves uv run <anything>.

This needs no assumption beyond the documented behaviour. cfgaudit deliberately makes no claim about chained commands, pipes or subshells (git status && rm -rf /): Cursor's reference does not describe how those are split, and asserting a swallow behaviour that turned out to be wrong is exactly how a rule ends up resting on a false premise.

Conditions stated in the message

Cursor's docs: "permissions.json only takes effect when Run Mode is enabled in Cursor Settings" (Auto-review, Allowlist, or Run Everything). The findings say so rather than implying the entries always apply.

Fix

Replace an unbounded entry with the specific commands or server:tool pairs the project needs, and narrow terminal entries with the base:args-glob form:

{
  // instead of "bash" / "npx" / "*:*"
  "terminalAllowlist": ["git status", "npm:test*", "cargo:build*"],
  "mcpAllowlist": ["linear:list_issues", "sentry:get_issue"]
}

Scope

Project scope only. A user's own ~/.cursor/permissions.json is self-intentional — the same reason CFG067 stays off user-global hooks.

References

.cursor/cli.json (Cursor CLI)

The Cursor CLI reads permission tokens from <project>/.cursor/cli.json ("Only permissions can be configured at the project level", CLI configuration reference; #649), after its workspace trust prompt. Shell(commandBase) allows that command with any subcommand or argument, and Shell(base:args) narrows the arguments. Only unbounded allow entries are reported:

Entry Severity
Shell(*); a shell (bash, sh, pwsh, cmd, …), launcher (env, xargs, eval), sudo or ssh with no argument pattern or *; any base with an inline-code pattern (python3:-c*, node:-e*) error
Mcp(*:*), Mcp(<server>:*) error
an interpreter, package runner or container runtime (node, npx, python3, bun, uv, docker, …) with no argument pattern or * warn

Bounded entries (Shell(git), Shell(node:--test*), Shell(uv run tools/x.py), Mcp(datadog:search)) are not reported. CFG093 reports bounded entries in permissions.json because Cursor documents that file as concatenated with the user's own; how the CLI merges cli.json is not documented.

The interpreter row is warn rather than error by measurement: on 124 real cli.json files, 53 allowed such a base, overwhelmingly Shell(node), Shell(npx) or Shell(python3) as everyday tooling, the same call CFG104 made for Devin's bare interpreters. On the same files the error rows fire on 35.

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