

Welcome to Part 11 of the Gemini CLI Tutorial series.
Gemini CLI Tutorial Series:
Part 1 : Installation and Getting Started
Part 2 : Gemini CLI Command line options
Part 3 : Configuration settings via settings.json and .env files
Part 4 : Built-in Tools
Part 5: Using Github MCP Server
Part 6: More MCP Servers : Firebase, Google Workspace, Google Gen AI Media Services and MCP Toolbox for Databases
Part 7: Custom slash commands
Part 8: Building your own MCP Server
Part 9: Understanding Context, Memory and Conversational Branching
Part 10: Gemini CLI and VS Code Integration
Part 11: Gemini CLI Extensions (this post)➡️ Codelab : Hands-on Gemini CLI
Before we start: Update your Gemini CLI version
If it’s been a while that you used Gemini CLI, make sure that you are running the latest version of the tool. Here’s a handy command to upgrade your Gemini CLI to the latest version (stable).
npm install -g @google/gemini-cli@latest
Do a gemini -v
command and ensure that you are seeing atleast 0.4.0
or above.
Thinking of sharing your Gemini CLI Prompts, MCP Servers, Custom Commands?
If you’ve been following along, you’ve already seen how Gemini CLI can become a powerful partner in your terminal, from understanding code to using built-in tools, writing your own Custom Slash Commands, connecting to external services with MCP servers, and even writing and integrating your own MCP Servers.
If you step back a bit, you would have noticed that if someone else asked for this custom stuff, you had to work on packaging some files or some snippets of JSON configuration code that would need to go into settings.json
file. Then give them the instructions to set that up in their Gemini CLI environment.
So the question is this: “How can I easily share a custom command or a MCP server that I have built, with my team?”
In this part of the tutorial series, you will learn:
- What extensions are and why they might just be the new best practice for packaging and sharing custom functionality.
- The technical anatomy of an extension, from its directory structure to its manifest file.
- Look at the new
gemini extensions
command line options and how they help to manage (install, uninstall, update, enable, disable) extensions. - Look at two new extensions announced: Cloud Run extension and Security Analysis extension. We will cover both of them right from installing them to a practical hands-on example.
- We’ve covered writing custom commands (.TOML files), MCP Server configurations, etc in previous parts of this series. We will discuss how to “upgrade” the custom commands and MCP servers from previous tutorials into a distributable extension.
Introducing Gemini CLI Extensions
Gemini CLI was designed to be extensible by default through emerging standards like Model Context Protocol (MCP), markdown-based instructions, and custom configurations.
To truly appreciate the power of extensions, it helps to understand the evolution of customization within Gemini CLI.
From Standalone to Sharable: The Evolution of Customization
From the beginning, Gemini CLI has offered powerful ways to tailor its behavior to your needs. Custom slash commands, defined in simple .toml
files, are an easy and effective way to creating personal, reusable prompts that save you from repetitive typing.
Next up are MCP servers, which give Gemini CLI entirely new skills, allowing it to connect to external APIs, databases, or proprietary internal tools.
However, this created a “last-mile problem”: How do you efficiently and reliably share these powerful customizations? The process often involved manually sharing .toml
files, sending snippets of settings.json
configurations, and writing up complex setup instructions. This approach lacks versioning, is prone to user error, and doesn’t scale well across a team or an open-source community.
Extensions are the natural next step in this evolution, providing a practical way to package and distribute your custom workflows.
What is an Extension? The “Packaging Layer” Explained
An extension is a self-contained, versionable, and easily distributable package. Think of it as the “shipping container” for your Gemini CLI customizations, bundling everything needed for a specific workflow into a single, neat package.
An extension can bundle any combination of:
- Custom slash commands (your
.toml
files). - MCP server configurations (which previously lived in
settings.json
). - Context files (
GEMINI.md
) to provide specific instructions and guidelines to the model. - Tool restrictions (
excludeTools
) to create a safer, more focused environment.
Why Use Extensions? The Core Benefits
Adopting extensions for your customizations provides several powerful advantages:
- One-Command Installation: This is key. Instead of a multi-step manual setup, a user can install a complete, complex toolset with a single command:
gemini extensions install
orgemini extensions install --path=some/local/path
The
in the above command could be the Github URL where you have hosted the extension.
- Simplified Distribution: Sharing your work becomes as easy as sharing a single Git repository URL. No more passing around individual files and configuration snippets.
- Versioning and Dependency Management: Because extensions are typically hosted in Git repositories, you get version control for free. There are
gemini extensions update
command to update an extension to a latest version. - Discoverability and Ecosystem: Extensions are the foundation for a rich and open ecosystem, much like the marketplaces for VS Code or Chrome. The Extension mechanism could form the foundation of a future marketplace where these extensions are available for review, download and more, in true community style.
The introduction of the extension framework is a clear signal that Gemini CLI is evolving from a powerful standalone tool into a true extensible platform.
The table below is an attempt to help understand Extensions and how to think of them vis-a-vis Custom Slash commands and MCP Servers.
At the moment, I do not see anything that indicates that the previous ways in which we were packaging or distributing things will not work. In the later section of this article, we will also higlight how we could possibly migrate some of the customizations that we have done using older ways.
Gemini CLI extensions
command
Ensure that you have upgraded Gemini CLI to the latest stable version at the time of this writing (0.4.0
). At the terminal, give the following command:
$ gemini extensions
This will provide a list of commands that are now available to manage Gemini CLI extensions.
If you try out gemini extensions list
and this is the first time that you are learning about extensions and have not installed any, you should see a message that says that there are No extensions installed.
The Directory Structure for an Extension
At its core, a typical extension is simply a directory with a specific structure and a manifest file, that Gemini CLI knows how to interpret. Let’s break down its components.
my-extension/
├── gemini-extension.json # The manifest file (REQUIRED)
├── GEMINI.md # Optional: Context/instructions for the model
└── commands/
├── deploy.toml # Becomes /deploy or /my-extension.deploy
└── gcs/
└── sync.toml # Becomes /gcs:sync or /my-extension.gcs:sync
The gemini-extension.json
file is the heart of the extension. It’s a manifest that tells Gemini CLI what the extension is, what version it is, and what capabilities it provides.
It has two mandatory keys :
name
: A unique identifier for the extension and used for namespacing and conflict resolutionversion
: A version number for the extension (e.g. 1.0.0)
There are 3 optional keys, which give you a hint in terms of what you can have extensions for:
mcpServers
: A map of the MCP Servers to configure. This uses the same format as the one that you have been using forsettings.json
.contextFileName
: A string type value that is the name of the context file to load (e.g.GEMINI.md
). If not specified, defaults to a GEMINI.md in the extensions root.excludeTools
: An array of strings that lists built-in tools that you want to disable when this extention is active. Can be used to prevent conflicts or enforce security policies (e.g."run_shell_commands(rm -rf)"
).
Creating Extensions from templates
At the moment, you can have extensions for:
- Context
- Custom Commands
- MCP Servers
- Exclude Tools
Let’s see an example of each of them and the best part, we will use the gemini extensions new
command to generate the required files for us. This is very thoughtful on part of the team to help all of us get a headstart and not make small mistakes while wrangling with the JSON elements.
Ensure you are in a folder on your machine. In my case, I am in ~/gemini-cli-projects/gemini-cli-extensions
folder.
In my terminal, I check that I do not have any extensions installed via the gemini extensions list
command.
We will now use the gemini extensions new
command to create extensions from boilerplate examples. The command requires two parameters:
path
: The path to create the extension intemplate
: The boilerplate template to use (“context
”, “exclude-tools
”, “custom-commands
” and “mcp-server
”)
Let’s go for it.
Context
You can create a new context extension from a boilerplate example by giving the following command:
gemini extensions new context1 context
On successful creation, you should see a message as shown below:
Successfully created new extension from template "context" at context1.
You can install this using "gemini extensions link context1" to test it out.
Let’s understand what it has done. The first statement it makes is Successfully created new extension from templatate “context” at context1.
If you take a look at the current directory in which you ran the gemini extensions new
command, you will see that a folder named context1
has been created. The folder contents are given below:
The gemini-extension.json
file is given below and its straightforward. It has the name
, version
and a contestFileName
key that points to GEMINI.md
file.
{
"name": "context-example",
"version": "1.0.0",
"contextFileName": "GEMINI.md"
}
The GEMINI.md
file is a sample context file, the contents of which are listed below:
# Ink Library Screen Reader GuidanceWhen building custom components, it's important to keep accessibility in mind. While Ink provides the building blocks, ensuring your components are accessible will make your CLIs usable by a wider audience.
## General Principles
Provide screen reader-friendly output: Use the useIsScreenReaderEnabled hook to detect if a screen reader is active. You can then render a more descriptive output for screen reader users.
Leverage ARIA props: For components that have a specific role (e.g., a checkbox or a button), use the aria-role, aria-state, and aria-label props on and to provide semantic meaning to screen readers.
You could now take this GEMINI.md
file and modify it as needed as per your requirements.
The next statement was You can install this using “gemini extensions link context1” to test it out.
The interesting thing is that it has given us a command to install it too.
Let’s run that and ideally you will find that the installation succeeds with the message: Extension “context-example” linked successfully and enabled.
Now, if you do a gemini extensions list
you should see this extension shown among the list of installed extensions:
$ gemini extensions listcontext-example (1.0.0)
Path: /Users/romin/gemini-cli-projects/gemini-cli-extensions/context1
Source: /Users/romin/gemini-cli-projects/gemini-cli-extensions/context1 (Type: link)
Context files:
/Users/romin/gemini-cli-projects/gemini-cli-extensions/context1/GEMINI.md
You will also notice that in the ~/.gemini/extensions
, it has not created a context-example
folder that has a file named .gemini-extension-install.json
, the contents are given below:
{
"source": "/Users/romin/gemini-cli-projects/gemini-cli-extensions/context1",
"type": "link"
}
Custom Commands
Let’s see an example of generating an extension that packages a custom command from the boilerplate code.
$ gemini extensions new command1 "custom-commands"Successfully created new extension from template "custom-commands" at command1.
You can install this using "gemini extensions link command1" to test it out.
This has created a folder structure that looks like this:
command1/
└── commands/
└── gemini-extension.json # The manifest file
└── fs/
└── grep-code.toml # Becomes /fs:grep-code
The gemini-extension.json
file is shown below:
{
"name": "custom-commands",
"version": "1.0.0"
}
The grep-code.toml
file is familiar to us and is shown below:
prompt = """
Please summarize the findings for the pattern `{{args}}`.Search Results:
!{grep -r {{args}} .}
"""
We can also install this via the gemini extensions link command1
and we get the following output: Extension “custom-commands” linked successfully and enabled
.
We can now see that our ~/.gemini/extensions
folder now has two folders for the extensions that we have installed:
Remember that this would not make the /fs:grep-code
command now available inside of Gemini CLI. So if you launch Gemini CLI now, you should see the command available:
If we run the gemini extensions list
command, we will see the 2 extensions that we just installed:
$ gemini extensions listcontext-example (1.0.0)
Path: /Users/romin/gemini-cli-projects/gemini-cli-extensions/context1
Source: /Users/romin/gemini-cli-projects/gemini-cli-extensions/context1 (Type: link)
Context files:
/Users/romin/gemini-cli-projects/gemini-cli-extensions/context1/GEMINI.md
custom-commands (1.0.0)
Path: /Users/romin/gemini-cli-projects/gemini-cli-extensions/command1
Source: /Users/romin/gemini-cli-projects/gemini-cli-extensions/command1 (Type: link)
MCP Servers
Let’s see an example of generating an extension that packages a MCP Server from the boilerplate code.
$ gemini extensions new mcpserver1 "mcp-server"Successfully created new extension from template "mcp-server" at mcpserver1.
You can install this using "gemini extensions link mcpserver1" to test it out.
This has created a folder structure that looks like this:
The key file to look at is gemini-extension.json
since the other files are that of the MCP Server logic. Notice that it has exact command to start the server locally. You will notice that it uses one of the substitution variables supported ${extensionPath}
, which is the the fully-qualified path of the extension in the user’s filesystem e.g. '/Users/username/.gemini/extensions/example-extension'.
{
"name": "mcp-server",
"version": "1.0.0",
"mcpServers": {
"nodeServer": {
"command": "node",
"args": ["${extensionPath}${/}example.ts"]
}
}
}
We can conclude this section here and it should give you a good start in generating boilerplate code that you can then modify.
Installing the Extensions
At this point, you saw how we generated the extensions and installed them locally via the link
command. We mentioned earlier in the article about gemini extensions install
, which means that we could have hosted the extension in our Github folder and simply told anyone who wants our extension to install it via the URL provided.
To try this, check out the following Github repository that I have:
It has a gemini-extension.json
that specifies that it is a Context file.
{
"name": "say-my-name-context-example",
"version": "1.0.0",
"contextFileName": "GEMINI.md"
}
And it has a simple GEMINI.md
listed below:
# Remember to call me RominAlways call me "Romin" and acknowledge every action from your side with the statement "Romin - Thank you for using Gemini CLI"
So this is all you will need to do to package your extension, host it on your Github and simply ask interested folks who would like to get your extension installed to use the gemini extensions install
command.
In our case, we will do the following:
gemini extensions install https://github.com/rominirani/say-my-name-extension
If all goes well, you should see the message : “Extension “say-my-name-context-example” installed successfully and enabled.”
In the ~/.gemini/extensions
folder, you will find the folder created as shown below:
If we list our extensions, we will get the 3rd extension that we just installed:
$ gemini extensions listcontext-example (1.0.0)
Path: /Users/romin/gemini-cli-projects/gemini-cli-extensions/context1
Source: /Users/romin/gemini-cli-projects/gemini-cli-extensions/context1 (Type: link)
Context files:
/Users/romin/gemini-cli-projects/gemini-cli-extensions/context1/GEMINI.md
custom-commands (1.0.0)
Path: /Users/romin/gemini-cli-projects/gemini-cli-extensions/command1
Source: /Users/romin/gemini-cli-projects/gemini-cli-extensions/command1 (Type: link)
say-my-name-context-example (1.0.0)
Path: /Users/romin/.gemini/extensions/say-my-name-context-example
Source: https://github.com/rominirani/say-my-name-extension (Type: git)
Context files:
/Users/romin/.gemini/extensions/say-my-name-context-example/GEMINI.md
Managing Extensions
You will notice that the gemini extensions
command also has various other commands like uninstall
, disable
, enable
, update
,etc. Feel free to try them out.
Loading and Precedence: Staying in Control
On startup, Gemini CLI automatically scans for extensions in two locations:
- Global:
~/.gemini/extensions
(for extensions available across all your projects) - Project-local (Workspace):
(for extensions specific to the current project)./.gemini/extensions
To manage potential conflicts and give you full control, Gemini CLI uses a clear set of precedence rules:
- Workspace over Global: If an extension with the same name exists in both your workspace and your global directory, the workspace version will be used. This allows you to test a new version of an extension on a specific project without affecting your global setup.
- User/Project over Extension: Your personal or project-specific custom commands always win. If you have a command at
~/.gemini/commands/deploy.toml
, it will take precedence over a/deploy
command provided by any extension. - Conflict Resolution: If a user command already exists with the same name as an extension command (e.g.,
/deploy
), the extension’s command is not lost. It is automatically renamed using the extension’s name as a prefix, such as/gcp.deploy
. This intelligent namespacing prevents collisions and ensures all functionality remains accessible.
This architecture enables the creation of composable and secure workflows. A team can distribute a standard, company-wide extension, and individual developers can still safely override specific commands for their local projects without breaking the shared toolset.
Furthermore, an extension can use the excludeTools
property to define its own security boundaries, for example, by disabling shell access to guarantee it only performs read-only operations.
Google Cloud Gemini CLI Extensions
Google Cloud just announced 2 new Gemini CLI Extensions. As the announcement blog post states:
- Find and fix security vulnerabilities.
- Deploy your app to the cloud.
These are provided via two simple Gemini CLI commands, as given below:
1) /security:analyze performs a comprehensive scan right in your local repository, with support for GitHub pull requests coming soon. This makes security a natural part of your development cycle.
2) /deploy deploys your application to Cloud Run, a fully managed serverless platform, in just a few minutes.
All of this without leaving your command-line.
Before you jump into your Gemini CLI and try to do a
/security:analyze
or/deploy
, keep in mind that you need to install them first.
Let’s see both of them in action.
One-Command Deployment with the Cloud Run Extension
Let’s build out a web application first and once that’s done, you often have the tedious process of packaging this in a form that is fit for the runtime environment. In our case, it is Cloud Run and hence it expects a container image. Well, the whole process of containering, deploying can be shrunk to a single command now.
Step 1: Vibe Coding a Deployable Flask App with Gemini CLI
First, we need an application to deploy. Let’s start in an empty directory and ask Gemini CLI to create a simple, production-ready Python Flask app.
Prompt 1: Create the application logic. Launch gemini
and enter the following prompt:
Create a simple Python Flask web application in a file named
app.py
. It should have a single route/
that returns ‘Hello from Gemini and Cloud Run!’.
Gemini CLI prompts me with the following:
I go ahead and let it create it. So the app.py
file is created and for your reference it is shown below:
from flask import Flaskapp = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello from Gemini and Cloud Run!'
if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0', port=8080)
Prompt 2: Create the dependencies file. Next, let’s define the dependencies:
Now, create a
requirements.txt
file for this Flask app. IncludeFlask
for the web framework andgunicorn
as the production WSGI server.
This will generate requirements.txt
:
Flask
gunicorn
Step 2: Installation and Prerequisites
Before we can deploy, we need to set up our environment and install the extension.
- Prerequisites: Make sure you have the
gcloud
CLI installed and have a Google Cloud project with billing enabled. - Authentication: From your regular terminal (not inside Gemini CLI), run the following commands to authenticate your local environment with Google Cloud:
gcloud auth login
gcloud auth application-default login
Install the Extension: Now, let’s install the Cloud Run extension. This command fetches the extension from its official GitHub repository and configures it automatically.
gemini extensions install https://github.com/GoogleCloudPlatform/cloud-run-mcp
If you launch Gemini CLI, you will find Cloud Run is available as a MCP Server, if you do a /mcp list
command:
Additionally, you will see a /deploy
command is now available:
The interesting thing to note in the installation is to visit ~/.gemini/extensions/cloudrun
and study the folder. The key files to study are the gemini-extension.json
file first.
{
"name": "cloud-run",
"version": "1.0.0",
"mcpServers": {
"cloud-run": {
"command": "npx",
"args": ["-y", "@google-cloud/cloud-run-mcp"]
}
},
"contextFileName": "gemini-extension/GEMINI.md"
}
It also points to a contextFileName
i.e. gemini-extension/GEMINI.md
. The contents are shown below:
# Cloud Run MCP Server## Code that can be deployed
Only web servers can be deployed using this MCP server.
The code needs to listen for HTTP reqeusts on the port defined by the $PORT environment variable or 8080.
### Supported languages
- If the code is in Node.js, Python, Go, Java, .NET, PHP, Ruby, a Dockerfile is not needed.
- If the code is in another language, or has any custom dependency needs, a Dockerfile is needed.
### Static-only apps
To deploy static-only applications, create a Dockerfile that serves these static files. For example using `nginx`:
`Dockerfile`
```
FROM nginx:stable
COPY ./static /var/www
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
```
`nginx.conf`:
```
server {
listen 8080;
server_name _;
root /var/www/;
index index.html;
# Force all paths to load either itself (js files) or go through index.html.
location / {
try_files $uri /index.html;
}
}
```
## Google Cloud pre-requisities
The user must have an existing Google Cloud account with billing set up, and ideally an existing Google Cloud project.
If deployment fails because of an access or IAM error, it is likely that the user doesn't have Google Cloud credentials on the local machine.
The user must follow these steps:
1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with their Google account.
2. Set up application credentials using the command:
```bash
gcloud auth application-default login
```
If you are familiar with Cloud Run, this should be straightforward but a good way for us to learn how this extension was packaged and what kind of instructions are being given to Gemini CLI.
Step 3: Deploy!
With our app built and the extension installed, the final step is remarkably simple.
- Navigate to the root directory of your Flask project (where
app.py
,requirements.txt
are located). - Launch Gemini CLI by typing
gemini
. - Execute the deploy command:
/deploy --project="" --location="" --name=""
. You will need to specify the project id (Google Cloud project ID), the location and the name of the service. If you do not give this, Cloud Run command invokes the appropriate tools to help you choose them.
That’s it! Gemini CLI will now execute the entire deployment pipeline: building the Docker image, pushing it to Artifact Registry, configuring and deploying a new Cloud Run service.
After a few moments (actually make than 2 minutes), you will see a success message with a Service URL
. Go ahead and click that link.
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ deploy_local_folder (cloud-run MCP Server) {"folderPath":"/Users/romin/gemini-cli-projects/cloud-run-deployment","project… │
│ │
│ Cloud Run service geminicliserver deployed from folder /Users/romin/gemini-cli-projects/cloud-run-deployment in │
│ project │
│ Cloud Console: │
│ https://console.cloud.google.com/run/detail/europe-west1/geminicliserver?project= │
│ Service URL: https://.a.run.app │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ Romin - Thank you for using Gemini CLI. The deployment was successful.Cloud Console: https://console.cloud.google.com/run/detail/europe-west1/geminicliserver?project=
Service URL: https://.a.run.app
Your groundbreaking Flask Application has been unleashed to the world.
Proactive Security with the Security Extension
Deploying code is one thing, but deploying secure code is another. Security scanning is often an afterthought, but with the Security extension, we can “shift left” and make it a seamless part of our local development loop.
Step 1: Installation
First, install the Security extension from its GitHub repository:
gemini extensions install https://github.com/google-gemini/gemini-cli-security
Step 2: Running a Scan
The security extension works by analyzing your local git diff
—that is, the changes you’ve made but haven’t yet committed.
- Initialize Git: If you haven’t already, initialize a git repository in your project folder and commit the files we created.
git init
git add.
git commit -m "Initial commit of Flask app"
- Simulate a Vulnerability: Now, let’s introduce a common security mistake. Open
app.py
and add a hardcoded API key.
# In app.py, inside the hello_cloud function
def hello_cloud():
api_key = "AIzaSy...fake...key...for...demo" # TODO: Move to a secret manager
return 'Hello from Gemini and Cloud Run!'
- Run the Analysis: Launch Gemini CLI again from your project root and run the analysis command:
/security:analyze
Because you have uncommitted changes, the extension will automatically detect and scan them.
Step 3: Understanding the Report
After a moment, Gemini CLI gets to work. I was first presented with the following:
Then it is doing a git diff
to understand the changes:
It then figured out that the app.py
is the only file that needs to be audited.
And then it asked for writing the Security Analysis Report:
It then presentes a detailed security report directly in your terminal.
Security Analysis ReportNewly Introduced VulnerabilitiesVulnerability: Hardcoded API Key
Severity: High
Location: app.py
Line Content: api_key = "AIzaSy...fake...key...for...demo" # TODO: Move to a secret manager
Description: A hardcoded API key is present in the source code. This is a security risk as it exposes the key to anyone with access to the codebase.
Recommendation: Store the API key in a secure secret manager and retrieve it at runtime.
This report is incredibly valuable. It doesn’t just flag an issue; it tells you the severity, the exact location, the potential business risk, and provides a clear, actionable recommendation for remediation. This empowers you to fix security issues quickly and correctly, long before they ever reach a production environment.
If you’d like to see more security issues highlighted, consider this sample app.py
file, as shown below:
import os
from flask import Flask, request
import sqlite3app = Flask(__name__)
@app.route("/")
def hello_cloud():
# VULNERABILITY 1: Hardcoded Secret
api_key = "AIzaSy...fake...key...for...demo" # TODO: Move to a secret manager
return f'Hello from Gemini and Cloud Run! Using key: {api_key}'
@app.route("/user/")
def get_user(username):
# VULNERABILITY 2: SQL Injection
db = sqlite3.connect('users.db')
cursor = db.cursor()
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
user = cursor.fetchone()
return f"User details: {user}"
@app.route("/search")
def search():
# VULNERABILITY 3: Cross-Site Scripting (XSS)
query = request.args.get('q', '')
return f"
"if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
When I ran the /security:analyze
this time, it gave me a report that showed all these vulnerabilities:
I have completed the final review of the security findings. The draft report is accurate and adheres to the principle of
minimizing false positives. I will now present the final security report.Here is the summary of the security audit:
Vulnerability: Hardcoded Secret
Severity: Critical
Location: app.py
Line Content: api_key = "AIzaSy...fake...key...for...demo" # TODO: Move to a secret manager
Description: A hardcoded API key was found in the source code. This key could be compromised if the source code is exposed,
leading to unauthorized access to the service it protects.
Recommendation: Store the API key in a secure secret management system (e.g., Google Secret Manager, AWS Secrets Manager,
HashiCorp Vault) and retrieve it at runtime. Do not store secrets in version control.
---
Vulnerability: SQL Injection
Severity: High
Location: app.py
Line Content: query = f"SELECT * FROM users WHERE username = '{username}'"
Description: The application constructs a SQL query by directly embedding user-provided input (username) into the query
string. A malicious user can supply crafted input to manipulate the query's logic, leading to unauthorized data access,
modification, or deletion.
Recommendation: Use parameterized queries (also known as prepared statements) to separate the SQL query logic from the data.
This ensures that user input is treated as data and not as executable code. For example, in sqlite3, use
cursor.execute("SELECT * FROM users WHERE username = ?", (username,)).
---
Vulnerability: Cross-Site Scripting (XSS)
Severity: Medium
Location: app.py
Line Content: return f"
"
Description: User-supplied input from the 'q' parameter is rendered directly into the HTML response without proper
sanitization or output encoding. This allows an attacker to inject malicious scripts into the response, which will be
executed in the user's browser.
Recommendation: Implement context-aware output encoding for all user-supplied data before it is rendered in the browser. Use
a library like MarkupSafe or a templating engine like Jinja2 (which is part of Flask and does auto-escaping) to render HTML
content safely. For example, return render_template_string("", query=query).
Try this Gemini CLI Extension with your codebase or introduce some intentional errors that you expect this tool to catch and let me know about your findings in the comments.
The Evolution of Customization: Updating Previous Guides
Now that we’ve seen the power of extensions, it’s clear they represent the recommended approach for creating robust and shareable tools. If you’ve followed previous parts in this tutorial series on creating custom commands or MCP servers, here is how you can “upgrade” them into a proper extension.
Recommendation for “Writing Custom Commands” Article
Let’s say you’ve created a collection of useful .toml
files in your ~/.gemini/commands/
directory. Packaging them into an extension for your team is simple.
New Best Practice: Packaging Your Commands into an Extension
- Create a new directory for your extension (e.g.,
my-command-pack
). - Inside that directory, create the
gemini-extension.json
manifest file. All you need is a name and version:
{ "name": "my-command-pack", "version": "1.0.0" }
3. Create a commands/
subdirectory inside my-command-pack
.
4. Move your existing .toml
files (like plan.toml
or git-commit.toml
) from ~/.gemini/commands/
into this new my-command-pack/commands/
directory.
5. Install it locally to test: from outside the directory, run gemini extensions install --path ./my-command-pack
.
6. Once you’re happy, push the my-command-pack
directory to a new GitHub repository. Now you can share the repository URL with your team, and they can install your entire command set with one command.
Recommendation for “Writing MCP Servers” Article
Similarly, if you’ve built a custom MCP server and configured it in your global ~/.gemini/settings.json
, you can bundle it into an extension to make the distribution as per the recommended way to package them into Extensions.
New Best Practice: Bundling Your MCP Server into an Extension
- Create a new directory for your extension (e.g.,
my-mcp-extension
). - Place your MCP server’s source code (e.g., your Python scripts) inside this directory.
- Create the
gemini-extension.json
manifest. - Copy the
mcpServers
configuration block from your~/.gemini/settings.json
file and paste it intogemini-extension.json
. - Crucially, adjust any paths in the configuration to be relative to the extension’s directory. This makes the extension fully self-contained. For example, if your
settings.json
had"command": "python /path/to/your/server.py"
, you would change it ingemini-extension.json
to"command": "python server.py"
, assumingserver.py
is in the root of the extension directory.
Now, anyone who installs your extension via its Git URL will automatically get the MCP server configured and running. They never need to manually edit their settings.json
file, which is a massive improvement in user experience and reliability.
Conclusion
Today, we’ve taken a deep dive into what makes Gemini CLI a truly extensible platform. We’ve seen that extensions are more than just a new feature; they are the key to building shareable, versionable, and secure workflows.
We went from an empty directory to a live, deployed web application using the Cloud Run extension, and then immediately scanned that application for vulnerabilities with the Security extension. This practical, end-to-end workflow highlights how extensions can integrate complex processes directly into your terminal, saving time and reducing friction.
Let me know what Gemini CLI extensions you build and make available to everyone.
May a thousand Gemini CLI Extensions bloom ….
Source Credit: https://medium.com/google-cloud/gemini-cli-tutorial-series-part-11-gemini-cli-extensions-69a6f2abb659?source=rss—-e52cf94d98af—4