cfgaudit

Rules / CFG062

CFG062 Gemini CLI allows unrestricted Git extensions (security.blockGitExtensions)

Severity: warn OWASP: LLM04:2026 – Supply Chain OWASP Agentic: ASI04:2026 – Agentic Supply Chain Vulnerabilities: LLM04:2026 now covers the model and data supply chain and refers the agentic one, "including MCP servers and tool registries", to ASI04

What cfgaudit checks

Gemini CLI can install extensions from Git repositories. security.blockGitExtensions: true blocks that; security.allowedExtensions constrains which extensions are permitted. A committed .gemini/settings.json that explicitly sets blockGitExtensions: false and provides no allowedExtensions allow-list lets the workspace install extensions from arbitrary Git repos — a supply-chain vector, since extension code runs with the agent's privileges.

cfgaudit flags this only when blockGitExtensions is explicitly false (not merely absent) and allowedExtensions is empty, in .gemini/settings.json (project) and ~/.gemini/settings.json (with --user).

Also here: experimental.extensionRegistryURI

blockGitExtensions governs what may be installed. The extension registry decides what the agent can see in the first place, one step earlier, so it belongs to the same supply chain.

The key defaults to https://geminicli.com/extensions.json and is described as "the URI (web URL or local file path) of the extension registry". cfgaudit reports a committed value pointing anywhere else, at warn.

Verified at the consumer (gemini-cli 0.55.1), which answers two questions this project asks of every such key:

let extensionRegistryURI = process.env["GEMINI_CLI_EXTENSION_REGISTRY_URI"]
  ?? (trustedFolder ? settings.experimental?.extensionRegistryURI : void 0);
if (extensionRegistryURI && !extensionRegistryURI.startsWith("http")) {
  extensionRegistryURI = resolveToRealPath(path.resolve(cwd, resolvePath(extensionRegistryURI)));
}

The key sits under experimental, so it may move or vanish. It is read presence-based rather than behind a version assertion, per the version-gate convention.

What is deliberately not flagged

security.autoAddToPolicyByDefault. Its name and its schema position suggest the auto-approve class CFG048 and CFG090 cover for other agents. The consumer says otherwise:

if (isTrustedFolder && allowPermanentApproval) {
  const isSafeToPersist = confirmationDetails.type === "info" || … === "edit" || … === "mcp";
  if (isSafeToPersist && settings.merged.security.autoAddToPolicyByDefault) {
    const alwaysAndSaveIndex = options.findIndex(o => o.value === ToolConfirmationOutcome.ProceedAlwaysAndSave);
    if (alwaysAndSaveIndex !== -1) initialIndex = alwaysAndSaveIndex;
  }
}

It sets initialIndex: which option starts highlighted in a confirmation dialog that still appears. Nothing is approved without a keypress. It is gated three ways on top of that — a trusted folder, the separate allowPermanentApproval setting (default false, so the key alone does nothing), and only info/edit/mcp confirmations.

It is also common: 32 committed .gemini/ files carry it. Reporting it would have been 32 false positives for a setting that moves a cursor.

security.disableYoloMode, security.disableAlwaysAllow, mcp.toolSandboxing. All three default to false, and setting them true hardens. A repository has nothing to weaken through them. This is the disableTmpWrite (CFG095) and exclude_slash_tmp (CFG064) trap: check a disable* key against its default before believing the name.

Trigger example

{ "security": { "blockGitExtensions": false } }

→ warn (no allowedExtensions to constrain installs).

Safe alternative

Block Git extensions, or pin an allow-list:

{ "security": { "blockGitExtensions": true } }
{ "security": { "blockGitExtensions": false, "allowedExtensions": ["github.com/your-org/.*"] } }

References

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