⌂ Home
Gemini CLI: From Zero to Production
Track 4 · Integrations
Module 12 of 17 ~50 min Intermediate
Module 11 · Track 4 — Integrations

Extensions — Packaged Superpowers for Gemini CLI

An extension bundles an MCP server, slash commands, persistent context, and agent skills into a single installable unit. One gemini extensions install command and your CLI gains an entirely new capability domain — from Obsidian vault RAG to network packet analysis. This module shows you how extensions work, how to build one from scratch, and the community extensions worth installing today.

What You'll Learn

  • Understand what an extension packages and how it integrates with the Gemini CLI runtime
  • Install, list, and remove extensions with the gemini extensions subcommand
  • Read the anatomy of an extension: file structure and package.json config schema
  • Build a complete "GitHub PR Assistant" extension with skill files and a live MCP server
  • Publish your extension to the geminicli.com gallery
  • Know which community extensions are worth installing for real-world workflows

What Are Extensions?

Analogy — VS Code Extensions, But for Your Terminal AI

Before VS Code extensions, you had a capable editor — but to add Git lens, a Docker sidebar, or a spell checker, you had to manually configure each tool, write custom keybindings, and wire up language servers yourself. The pain was that every developer had a different local setup, and sharing a "good configuration" meant emailing a dotfile. VS Code extensions solved this by packaging everything together: the UI, the commands, the language server, the settings defaults.

Gemini CLI extensions follow the exact same model. The mapping: an extension is a self-contained directory published to GitHub that bundles a MCP serverModel Context Protocol server — a process that exposes tools (functions Gemini can call) and resources (data Gemini can read) over a standardized JSON-RPC protocol. MCP is what lets Gemini "reach outside" the conversation to interact with real systems., slash command skill files, a gemini.md context document, and optional theme overrides. One install command and your CLI gains a fully wired capability domain.

Technical Definition — Gemini CLI Extension

A Gemini CLI extensionA versioned npm-style package (published to GitHub or geminicli.com) that Gemini CLI can install. At install time, the CLI clones the repo, reads package.json for the gemini.extension config block, registers MCP servers, copies skill files into the session skill registry, and merges gemini.md into the context stack. is a versioned directory with a package.json containing a gemini.extension config block. When installed, the CLI:

  • Clones the GitHub repo to ~/.gemini/extensions/<name>/
  • Registers any declared MCP servers so they auto-start with each session
  • Adds skill .md files to the slash command registry (e.g., /pr-review)
  • Merges the extension's gemini.md at the bottom of the context stackThe ordered set of gemini.md files that Gemini CLI loads at session start: global (~/.gemini/GEMINI.md) → project (.gemini/GEMINI.md) → extension gemini.md files. All are concatenated into the system prompt.
Animation — Extension Install Pipeline
command
gemini ext install
action
git clone
reads
package.json
registers
MCP servers
adds
skills + context
result
ready to use
Why It Matters

Without extensions, adding a new MCP server to your workflow means editing ~/.gemini/settings.json manually, figuring out start commands, and writing skill files by hand. A single developer spending 30 minutes on this configuration can package it as an extension so every teammate gets it in under 10 seconds. The Obsidian extension, for example, eliminates the manual setup of a local RAG pipeline — something that previously took 2+ hours of configuration.

Installing, Listing, and Removing Extensions

The gemini extensions subcommand handles the full extension lifecycle. Extensions are installed globally (available in all projects) but can also be pinned per-project via .gemini/settings.json.

Core Commands

# Install from GitHub URL
gemini extensions install https://github.com/example/gemini-ext-obsidian

# List all installed extensions
gemini extensions list

# Remove an extension by name
gemini extensions remove gemini-ext-obsidian

# Update all extensions to latest
gemini extensions update

# Show details about a specific extension
gemini extensions info gemini-ext-obsidian
# Install from GitHub URL
gemini extensions install https://github.com/example/gemini-ext-obsidian

# List all installed extensions
gemini extensions list

# Remove an extension by name
gemini extensions remove gemini-ext-obsidian

# Update all extensions to latest
gemini extensions update

# Show details about a specific extension
gemini extensions info gemini-ext-obsidian
WHAT: install clones the repo to ~/.gemini/extensions/ and registers everything declared in package.json. Subsequent sessions automatically start the extension's MCP servers.
WHY: The GitHub URL format means any public repo can be an extension — no central registry required. You can also use private repos if your Git credentials allow the clone.
GOTCHA: Extensions run code on your machine (their MCP servers). Always review the extension source before installing. Treat a gemini extensions install with the same caution as npm install -g.

Walkthrough: Installing the Obsidian Extension

The Obsidian extension turns your Obsidian vaultA local directory of Markdown files managed by the Obsidian note-taking application. Vaults can contain thousands of interconnected notes with backlinks, tags, and metadata. into a queryable RAG knowledge base without any configuration.

1

Install the extension

terminal
gemini extensions install https://github.com/example/gemini-ext-obsidian
2

Point it at your vault — the extension adds a config prompt on first run:

terminal
gemini
# Extension: Obsidian RAG
# Vault path not configured. Enter path: ~/Documents/MyVault
# Indexing 2,341 notes... done (14s)
3

Use the new slash commands and resource type:

Gemini CLI session
/vault-search kubernetes networking notes
@vault://Projects/Q3-planning.md Summarize the key decisions

What Just Happened?

After install, Gemini CLI's session startup automatically launched the Obsidian MCP server in the background. The /vault-search slash command calls that server's search_notes tool. The @vault:// resource prefix is registered by the extension as a custom resource handler — it resolves to the file contents of the matching vault note. None of this required manual JSON configuration.

What an Extension Adds to Your Session

ContributionExampleRegistered As
Slash commands/vault-search, /pr-reviewSkill .md files → skill registry
@ resource types@vault://note-titleCustom resource handler in MCP server
Toolscreate_issue, search_notesMCP server tool declarations
System contextDomain knowledge, preferencesMerged into context stack via gemini.md
Now that you know how to use extensions, let's look inside one — understanding the file structure is the prerequisite for building your own.

Extension Anatomy

File Structure

Every extension follows a predictable directory layout. The CLI looks for each component at a known path — nothing is discovered dynamically.

Animation — Extension Directory Walkthrough
my-extension/
  ├── package.json       # name, version, gemini.extension config
  ├── gemini.md          # context merged into every session
  ├── skills/            # .md skill files → slash commands
  │   └── my-skill.md
  ├── mcp/               # bundled MCP server(s)
  │   └── server.js
  └── README.md          # shown on geminicli.com gallery

The package.json gemini.extension Config Block

The gemini.extension key in package.json is what the CLI reads to understand what the extension provides. Every field is optional except name.

package.json
{
  "name": "gemini-ext-github-pr",
  "version": "1.2.0",
  "description": "GitHub PR review and issue triage skills for Gemini CLI",
  "gemini.extension": {
    "displayName": "GitHub PR Assistant",
    "description": "AI-powered PR review, descriptions, and issue triage via GitHub API",
    "icon": "assets/icon.png",
    "mcpServers": [
      {
        "name": "github-pr-mcp",
        "command": "node",
        "args": ["mcp/server.js"],
        "env": {
          "GITHUB_TOKEN": "${env:GITHUB_TOKEN}"
        }
      }
    ],
    "skills": [
      "skills/pr-review.md",
      "skills/pr-description.md",
      "skills/issue-triage.md"
    ],
    "contextFile": "gemini.md",
    "permissions": ["network", "env:GITHUB_TOKEN"]
  }
}
WHAT: mcpServers tells the CLI how to start the bundled MCP server. The ${env:GITHUB_TOKEN} syntax safely injects environment variables without hardcoding secrets.
WHY: skills is an array of relative paths to .md files. Each file becomes a slash command whose name matches the filename (e.g., pr-review.md/pr-review).
GOTCHA: The permissions array is displayed to the user during install — clearly declaring required permissions (especially env:* and network) prevents install-time rejections and builds user trust.
With the anatomy clear, let's put it into practice by building a real extension from scratch.

Build: GitHub PR Assistant Extension

This walkthrough builds a complete, working extension. By the end you'll have three slash commands (/pr-review, /pr-description, /issue-triage) backed by a lightweight MCP server that calls the GitHub REST API.

Step 1 — Write the Skill Files

Skill files are plain Markdown. The YAML frontmatter declares the command name and description; the body is the prompt template Gemini executes when the command is invoked.

skills/pr-review.md
---
name: pr-review
description: Review a GitHub PR for correctness, security, and test coverage
arguments:
  - name: pr_number
    description: GitHub PR number (e.g. 142)
    required: true
---

You are a senior engineer performing a thorough code review.

Use the `get_pull_request` tool to fetch PR #{{pr_number}} including the diff,
title, description, and list of changed files.

Review the changes for:
1. **Correctness** — logic errors, off-by-one bugs, null pointer risks
2. **Security** — OWASP Top 10, injection risks, hardcoded secrets
3. **Breaking changes** — API surface changes, database migrations
4. **Test coverage** — are new code paths exercised by tests?

Output your review as structured Markdown with a summary section first,
then per-file findings. Use GitHub Flavored Markdown so the output can be
pasted directly as a PR comment.
WHAT: The YAML frontmatter's arguments array defines what the user can pass when invoking the command: /pr-review 142. Arguments are injected as Mustache-style {{pr_number}} tokens.
skills/issue-triage.md
---
name: issue-triage
description: Classify a GitHub issue and suggest labels + response
arguments:
  - name: issue_number
    description: Issue number to triage
    required: true
---

Use `get_issue` to fetch issue #{{issue_number}}.

Classify it as one of: bug / feature-request / question / documentation / duplicate.

Then:
- Suggest the appropriate GitHub labels (e.g., "bug", "enhancement", "needs-info")
- If the issue lacks steps to reproduce, draft a clarifying comment asking for them
- If it's a duplicate, identify the likely original issue number if possible
- Assign a priority: P0 (blocking), P1 (high), P2 (normal), P3 (low)

Output the classification, labels, priority, and any draft comment as JSON
so the CLI can apply them automatically via the GitHub API.

Step 2 — Write the MCP Server

The MCP server exposes the GitHub API as tools that Gemini can call. It's a Node.js process that communicates over stdioStandard input/output — the simplest MCP server transport. The CLI launches the server as a child process and sends JSON-RPC messages over stdin/stdout. No network ports needed..

// mcp/server.ts — GitHub PR Assistant MCP server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN) {
  console.error("GITHUB_TOKEN environment variable is required");
  process.exit(1);
}

// ── Tool definitions ──────────────────────────────────────────────────
const TOOLS = [
  {
    name: "get_pull_request",
    description: "Fetch a GitHub pull request including diff and file list",
    inputSchema: {
      type: "object",
      properties: {
        owner: { type: "string", description: "Repository owner" },
        repo: { type: "string", description: "Repository name" },
        pr_number: { type: "number", description: "Pull request number" },
      },
      required: ["owner", "repo", "pr_number"],
    },
  },
  {
    name: "get_issue",
    description: "Fetch a GitHub issue by number",
    inputSchema: {
      type: "object",
      properties: {
        owner: { type: "string" },
        repo: { type: "string" },
        issue_number: { type: "number" },
      },
      required: ["owner", "repo", "issue_number"],
    },
  },
  {
    name: "post_pr_comment",
    description: "Post a comment on a pull request",
    inputSchema: {
      type: "object",
      properties: {
        owner: { type: "string" },
        repo: { type: "string" },
        pr_number: { type: "number" },
        body: { type: "string", description: "Markdown comment body" },
      },
      required: ["owner", "repo", "pr_number", "body"],
    },
  },
];

// ── GitHub API helper ─────────────────────────────────────────────────
async function githubFetch(path: string, options: RequestInit = {}) {
  const res = await fetch(`https://api.github.com${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${GITHUB_TOKEN}`,
      Accept: "application/vnd.github.v3+json",
      "User-Agent": "gemini-ext-github-pr/1.0",
      ...((options.headers as Record) || {}),
    },
  });
  if (!res.ok) {
    throw new Error(`GitHub API error ${res.status}: ${await res.text()}`);
  }
  return res.json();
}

// ── Server setup ──────────────────────────────────────────────────────
const server = new Server(
  { name: "github-pr-mcp", version: "1.2.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    if (name === "get_pull_request") {
      const { owner, repo, pr_number } = args as any;
      const [pr, diff] = await Promise.all([
        githubFetch(`/repos/${owner}/${repo}/pulls/${pr_number}`),
        githubFetch(`/repos/${owner}/${repo}/pulls/${pr_number}/files`),
      ]);
      return {
        content: [{ type: "text", text: JSON.stringify({ pr, files: diff }) }],
      };
    }

    if (name === "get_issue") {
      const { owner, repo, issue_number } = args as any;
      const issue = await githubFetch(
        `/repos/${owner}/${repo}/issues/${issue_number}`
      );
      return { content: [{ type: "text", text: JSON.stringify(issue) }] };
    }

    if (name === "post_pr_comment") {
      const { owner, repo, pr_number, body } = args as any;
      await githubFetch(
        `/repos/${owner}/${repo}/issues/${pr_number}/comments`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ body }),
        }
      );
      return { content: [{ type: "text", text: "Comment posted successfully." }] };
    }

    throw new Error(`Unknown tool: ${name}`);
  } catch (err: any) {
    return {
      content: [{ type: "text", text: `Error: ${err.message}` }],
      isError: true,
    };
  }
});

// ── Start server ──────────────────────────────────────────────────────
const transport = new StdioServerTransport();
await server.connect(transport);
# mcp/server.py — GitHub PR Assistant MCP server (Python variant)
import asyncio
import json
import os
import sys
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
if not GITHUB_TOKEN:
    print("GITHUB_TOKEN environment variable is required", file=sys.stderr)
    sys.exit(1)

HEADERS = {
    "Authorization": f"Bearer {GITHUB_TOKEN}",
    "Accept": "application/vnd.github.v3+json",
    "User-Agent": "gemini-ext-github-pr/1.0",
}

server = Server("github-pr-mcp")

# ── Tool definitions ──────────────────────────────────────────────────
@server.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="get_pull_request",
            description="Fetch a GitHub PR including diff and file list",
            inputSchema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "repo": {"type": "string"},
                    "pr_number": {"type": "integer"},
                },
                "required": ["owner", "repo", "pr_number"],
            },
        ),
        types.Tool(
            name="get_issue",
            description="Fetch a GitHub issue by number",
            inputSchema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "repo": {"type": "string"},
                    "issue_number": {"type": "integer"},
                },
                "required": ["owner", "repo", "issue_number"],
            },
        ),
        types.Tool(
            name="post_pr_comment",
            description="Post a Markdown comment on a pull request",
            inputSchema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "repo": {"type": "string"},
                    "pr_number": {"type": "integer"},
                    "body": {"type": "string"},
                },
                "required": ["owner", "repo", "pr_number", "body"],
            },
        ),
    ]

# ── Tool implementations ──────────────────────────────────────────────
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    async with httpx.AsyncClient() as client:
        try:
            if name == "get_pull_request":
                owner, repo, pr_num = (
                    arguments["owner"],
                    arguments["repo"],
                    arguments["pr_number"],
                )
                pr_resp = await client.get(
                    f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_num}",
                    headers=HEADERS,
                )
                pr_resp.raise_for_status()
                files_resp = await client.get(
                    f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_num}/files",
                    headers=HEADERS,
                )
                files_resp.raise_for_status()
                result = {"pr": pr_resp.json(), "files": files_resp.json()}
                return [types.TextContent(type="text", text=json.dumps(result))]

            elif name == "get_issue":
                owner, repo, issue_num = (
                    arguments["owner"],
                    arguments["repo"],
                    arguments["issue_number"],
                )
                resp = await client.get(
                    f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_num}",
                    headers=HEADERS,
                )
                resp.raise_for_status()
                return [types.TextContent(type="text", text=json.dumps(resp.json()))]

            elif name == "post_pr_comment":
                owner, repo, pr_num, body = (
                    arguments["owner"],
                    arguments["repo"],
                    arguments["pr_number"],
                    arguments["body"],
                )
                resp = await client.post(
                    f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_num}/comments",
                    headers={**HEADERS, "Content-Type": "application/json"},
                    json={"body": body},
                )
                resp.raise_for_status()
                return [types.TextContent(type="text", text="Comment posted successfully.")]

            else:
                raise ValueError(f"Unknown tool: {name}")

        except Exception as e:
            return [types.TextContent(type="text", text=f"Error: {e}")]

# ── Start server ──────────────────────────────────────────────────────
async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())
WHAT (chunks 1-2): We define the tool schemas upfront so the CLI can show them in /help. The inputSchema is standard JSON Schema — the MCP SDK validates inputs against it automatically.
WHY (chunk 3): Each tool handler wraps its GitHub API call in try/catch and returns isError: true on failure. This lets Gemini decide whether to retry, ask for clarification, or surface the error to the user — rather than crashing the server.

What Just Happened?

You now have a complete extension: three skill files that give the CLI /pr-review, /pr-description, and /issue-triage commands, and an MCP server that executes real GitHub API calls when Gemini invokes those tools. The skill files contain the reasoning instructions; the MCP server handles the I/O. This is the clean separation that makes extensions composable.

Testing Your Extension Locally

# Install from local path during development
gemini extensions install ./my-extension --dev

# Verify the extension is loaded
gemini extensions list
# Output:
#   gemini-ext-github-pr  v1.2.0  (dev)  3 skills  1 MCP server

# Test a skill
$env:GITHUB_TOKEN = "ghp_yourtoken"
gemini
# > /pr-review 142
# > /issue-triage 56
# Install from local path during development
gemini extensions install ./my-extension --dev

# Verify the extension is loaded
gemini extensions list
# Output:
#   gemini-ext-github-pr  v1.2.0  (dev)  3 skills  1 MCP server

# Test a skill
export GITHUB_TOKEN="ghp_yourtoken"
gemini
# > /pr-review 142
# > /issue-triage 56

Publishing Your Extension

Once your extension works locally, publishing it makes it installable by anyone with a gemini extensions install <url> command.

Analogy — Publishing a VS Code Extension to the Marketplace

When you publish a VS Code extension, you're not moving your code — you're registering a pointer in the marketplace that tells others "this GitHub repo contains a valid extension." The pain without a registry is that sharing means emailing a Git URL and hoping the other person knows how to install it. The mapping: gemini extensions publish is the equivalent of running vsce publish — it submits your repo URL, name, and README to the geminicli.com gallery so others can find it by keyword search.

# From your extension root directory
cd my-extension

# Validate package.json structure before submitting
gemini extensions validate

# Publish to geminicli.com gallery
# (Requires GitHub auth — opens browser for OAuth)
gemini extensions publish

# Output:
# Validating package.json... OK
# Checking README.md... OK (1,240 words)
# Submitting to gallery...
# Published: https://geminicli.com/extensions/gemini-ext-github-pr
# Install: gemini extensions install https://github.com/yourname/gemini-ext-github-pr
# From your extension root directory
cd my-extension

# Validate package.json structure before submitting
gemini extensions validate

# Publish to geminicli.com gallery
gemini extensions publish

# Output:
# Validating package.json... OK
# Checking README.md... OK (1,240 words)
# Submitting to gallery...
# Published: https://geminicli.com/extensions/gemini-ext-github-pr
# Install: gemini extensions install https://github.com/yourname/gemini-ext-github-pr
README Requirements for Gallery Listing

The gallery renders your README.md directly. A complete README should include: a one-paragraph description, the list of slash commands the extension adds, required environment variables (e.g., GITHUB_TOKEN), an install command, and a usage example for each major command. Extensions without a README fail validation.

Community Extensions Worth Installing

The awesome-gemini-cli-extensionsA community-curated GitHub repo (similar to awesome-* lists) that catalogs high-quality Gemini CLI extensions with descriptions, install commands, and ratings. Maintained by community contributors. list is the fastest way to find extensions for your domain. Here are the standouts:

Animation — Community Extensions by Domain
gemini-ext-obsidian
/vault-search @vault:// search_notes
gemini-ext-network
/packet-analyze /subnet-calc parse_pcap
gemini-ext-postgres
/query @db:// execute_sql
gemini-ext-jira
/sprint-summary /ticket-create search_issues
gemini-ext-obsidian

RAG over your Obsidian vault. Full-text + semantic search, backlink traversal, note creation. Eliminates manual copy-paste from notes to prompts.

Knowledge
Packet Buddy

Network analysis: parse pcap files, explain packet flows, diagnose latency issues. Adds /packet-analyze and /subnet-calc.

Networking
gemini-ext-postgres

Natural language queries over PostgreSQL. Schema-aware — loads your schema into context so Gemini generates correct SQL on the first try.

Database
gemini-ext-jira

Sprint summaries, ticket creation, backlog triage. Reads your sprint board and generates standup updates or retrospective notes.

Project Mgmt
gemini-ext-k8s

Kubernetes cluster assistant. /pod-debug, /resource-audit, and /helm-review commands with live cluster access via kubectl.

DevOps
gemini-ext-stripe

Stripe payments debugging. Fetch payment intents, explain webhook events, and draft Stripe API integration code against live API responses.

Payments
Why Extensions Beat Copy-Paste Workflows

A developer without the Obsidian extension spends 5-10 minutes per session manually finding relevant notes and pasting them into prompts. With the extension, /vault-search kubernetes networking finds and injects the 3 most relevant notes in 2 seconds. Across a 40-hour work week, that's 2-4 hours recovered. The Packet Buddy extension eliminates the need to export pcap files, open Wireshark, and manually describe packet flows — the extension does all three steps in one command.

Section Complete — Extensions

You know what extensions are, how to install them, their file anatomy, and how to build and publish one. You've also seen the MCP server pattern that powers every extension's real-world tool calls. The next module extends this into CI/CD pipelines with GitHub Actions.

Knowledge Check

Five questions to confirm the key concepts from this module.

1. When you run gemini extensions install <url>, where does the CLI clone the extension to?

A
The current project directory under .gemini/extensions/
B
The global home directory under ~/.gemini/extensions/
C
The npm global packages directory
D
The current working directory

2. Which file in an extension's directory is merged into the Gemini CLI session's context stack?

A
README.md
B
package.json
C
gemini.md
D
mcp/server.js

3. What determines the slash command name for a skill file? For example, what slash command does skills/pr-review.md create?

A
The name field in the gemini.extension block of package.json
B
The name field in the skill file's YAML frontmatter (and the filename as default)
C
The first H1 heading in the skill file's Markdown body
D
A random UUID assigned at install time

4. You're building an extension that connects to an internal company API. Which field in package.json's gemini.extension block should you use to declare the required API token environment variable?

A
secrets
B
env inside mcpServers only — no top-level declaration needed
C
permissions (e.g., "env:COMPANY_API_TOKEN") so it's shown to the user at install time
D
You should hardcode the token directly in the MCP server source for simplicity

5. What is the security risk of running gemini extensions install without reviewing the extension source?

A
The extension could override your ~/.gemini/GEMINI.md global config silently
B
The extension could consume your Gemini API quota by sending extra prompts
C
The extension's MCP server runs as a child process on your machine with access to environment variables, so malicious code could exfiltrate secrets
D
There is no security risk because Gemini CLI sandboxes all extension processes