The Cartographer · 11 min mission
Gemini CLI Extensions and Custom Commands
Turn repeated workflows into TOML slash commands, then bundle and share them as installable Gemini CLI extensions.
On this page
Gemini CLI has two reuse mechanisms: a custom slash command (a single .toml prompt file invoked as /name) and an extension (a directory with a gemini-extension.json manifest that bundles commands plus MCP servers, context files, themes, hooks, skills, and policies). This guide authors a command, makes it dynamic, wraps it in an extension, and publishes it.
Author a custom command in TOML
A command is one .toml file in a commands directory. The command name is its file path relative to that directory, with each path separator turned into a colon. Two directories are scanned, in this order — when a name collides, the project command wins:
| Scope | Path | Availability |
|---|---|---|
| User / global | ~/.gemini/commands/ | Every project on the machine |
| Project / local | <project-root>/.gemini/commands/ | Checked into git; shared with the team; overrides a same-named user command |
# prompt is required. description is optional; if omitted, the CLI
# generates a generic description from the filename.
description = "Write missing unit tests for the current changes"
prompt = """
Look at the staged changes and write unit tests for any new or
modified functions that lack coverage. Match the existing test
style in this repo. Run the test suite when done and fix failures.
"""| File | Command | Rule |
|---|---|---|
~/.gemini/commands/test.toml | /test | Top-level file |
<project>/.gemini/commands/git/commit.toml | /git:commit | Subdirectory becomes a : namespace |
<project>/.gemini/commands/gcs/sync.toml | /gcs:sync | One nesting level, one colon |
Inject arguments, shell output, and files
Three placeholders splice live context into the prompt. They differ in when they resolve and whether your input is escaped — both are footguns.
{{args}}— replaced with the text typed after the command. Ifpromptcontains no{{args}}but arguments are passed, the CLI appends the full typed command to the end of the prompt, separated by two newlines; with no arguments, the prompt is sent unchanged.!{...}— executes the shell command inside the braces and injects its stdout. Every!{...}execution pops a security confirmation dialog showing the resolved command.@{...}— injects file or directory contents. A directory is traversed (respecting.gitignore/.geminiignorewhen enabled); images, PDFs, audio, and video are encoded as multimodal input. Absolute paths are allowed only within the workspace.
| Placeholder | Resolves to | Order | Escaping |
|---|---|---|---|
@{path} | File / directory contents (multimodal) | 1st (before all others) | n/a |
!{cmd} | stdout of the shell command | 2nd | {{args}} inside it is shell-escaped |
{{args}} | Text typed after the command | 3rd | Raw everywhere except inside !{...} |
description = "Generate a conventional-commit message for the staged diff"
prompt = """
You are writing a git commit message. Here is the staged diff:
!{git diff --staged}
Write a single conventional-commit message describing it. If the
user gave a hint, weight it: {{args}}
"""Classify what you type: /name, @{file}, !{cmd}, or a plain prompt
Command explorer
Every built-in slash command worth knowing, grouped by what it touches. Search by name or purpose, filter by group, and pick one for the full when-to-use and the gotchas the menu never tells you.
32 commands
Show version, model, account, and connectivity.
When: Confirm which model and auth method are active — including the classic case where a stray ANTHROPIC_API_KEY is shadowing your subscription. Works mid-response.
Availability varies by plan, platform, and version — type / in your own session to see exactly what is live for you.
MCP prompts become slash commands
An MCP server can expose prompts that the CLI auto-discovers on connection and surfaces as slash commands: the prompt name becomes the command, its description becomes the help text. Invoke with named or positional arguments; executing one calls the server's prompts/get method and the server returns the filled-in prompt.
# named arguments
/poem-writer --title="Gemini CLI" --mood="reverent"
# positional arguments
/poem-writer "Gemini CLI" reverentWrap a command in an extension: the manifest
An extension is a directory with a required gemini-extension.json at its root. Installed extensions live under ~/.gemini/extensions/<name>/. On startup the CLI loads all of them and merges their configurations; on a conflict, workspace configuration takes precedence. The two required fields are name and version.
{
"name": "acme-review",
"version": "1.2.0",
"description": "ACME's code-review playbook, commands, and linters.",
"contextFileName": "GEMINI.md",
"mcpServers": {
"acme-linter": {
"command": "node",
"args": ["${extensionPath}/server/index.js"]
}
},
"excludeTools": ["run_shell_command(rm -rf)"],
"settings": [
{
"name": "apiKey",
"description": "ACME linter API key",
"envVar": "ACME_API_KEY",
"sensitive": true
}
]
}| Field | What it does | The catch |
|---|---|---|
name | Unique id; should match the directory name | Lowercase letters, numbers, dashes only — no underscores or spaces |
version | Semantic version of the extension | Keep in sync with the release tag |
mcpServers | MCP servers loaded on startup, like settings.json | Same-named server in settings.json wins; all MCP options work except `trust` |
contextFileName | Context file loaded from the extension dir | If omitted but a GEMINI.md exists, that file loads anyway |
excludeTools | Tool names hidden from the model | Supports command-specific bans, e.g. run_shell_command(rm -rf) |
settings | User-configurable values stored in a .env in the extension dir | When sensitive: true, stored in the system keychain and obfuscated |
migratedTo | URL of a new repo source the CLI auto-migrates installs to | Requires ≥1 release on the new repo |
plan.directory | Where planning artifacts go | Fallback only; default ~/.gemini/tmp/<project>/<session-id>/plans/ |
Other directories an extension can ship
Beyond the manifest, specific subfolders carry well-known meaning:
commands/— TOML custom commands, namespaced like the loose ones (commands/deploy.toml→/deploy,commands/gcs/sync.toml→/gcs:sync).hooks/hooks.json— lifecycle hooks (defined here, not in the manifest).skills/<name>/SKILL.md— agent skills (skills/security-audit/SKILL.md→security-audit).agents/*.md— sub-agents. Preview feature under active development; do not build a dependency on it staying stable.policies/*.toml— Policy Engine rules that run at tier 2 (above built-in defaults, below user and admin policy). The CLI ignores anyallowdecision oryolomode in an extension's policies — an extension can tighten rules but never auto-approve.
Precedence when names collide
Commands arrive from three sources with a strict order: project beats user, and extension commands have the lowest precedence. A colliding extension command is not dropped — it is kept and prefixed with the extension name using a dot (an extension gcp shipping deploy that clashes with your /deploy becomes /gcp.deploy). The same merge-with-precedence logic governs MCP servers (a same-named server in settings.json overrides the extension's) and configuration generally (workspace beats extension).
Two precedence rules that look alike
Command namespacing (`:`)
A colon comes from the file path inside a commands directory. commands/git/commit.toml is always /git:commit, in every source. Structural, not a conflict.
Extension conflict prefix (`.`)
A dot appears only when an extension command name collides with a higher-precedence command. /gcp.deploy = "the deploy from the gcp extension, renamed because something already owned /deploy."
Install and manage extensions
The gemini extensions management subcommands (install, update, uninstall, enable, disable, new, link, config) run from your shell, not inside the interactive CLI — inside the CLI only /extensions list works. Any management change takes effect only after you restart the CLI session. install creates a copy (a snapshot), so upstream changes do not reach you until you update (or installed with --auto-update); GitHub installs require git on the machine.
| Flag | Effect |
|---|---|
--ref <ref> | Pin to a git branch, tag, or commit |
--auto-update | Track upstream automatically |
--pre-release | Allow installing pre-release versions |
--consent | Acknowledge the security prompt up front |
--skip-settings | Skip the configure-on-install step |
Install, inspect, and update an extension
Install from the shell
In your terminal — not inside the CLI — run
gemini extensions install https://github.com/gemini-cli-extensions/workspace. Add--ref=stableto pin a release channel or--auto-updateto track upstream.Restart the CLI
Management changes only apply on a fresh session. Quit and relaunch
geminiso the new extension loads.Confirm it loaded
Inside the CLI run
/extensions listto verify the extension is present and enabled. (gemini extensions listdoes the same from the shell.)Pull upstream changes
Because install is a copy, run
gemini extensions update <name>(orgemini extensions update --all) from the shell, then restart again.
| Command | What it does |
|---|---|
gemini extensions uninstall <name...> | Remove one or more extensions |
gemini extensions disable <name> [--scope user\|workspace] | Disable; enabled globally by default |
gemini extensions enable <name> [--scope user\|workspace] | Re-enable |
gemini extensions new <path> [template] | Scaffold from a template (mcp-server, context, custom-commands) |
gemini extensions link <path> | Symlink a dev dir so edits apply without reinstalling |
gemini extensions config <name> [setting] [--scope] | Update an installed extension's settings |
Publish: the gallery and release channels
To get auto-listed at geminicli.com/extensions/ (no submission form), you need three things — and the crawler runs daily. Per Google's launch announcement, the gallery ranks listings by GitHub stars.
Get listed in the gallery
Public repo
Host the extension in a public GitHub repo.
Add the topic
Add the GitHub topic
gemini-cli-extension(exact string) in the repo's About section.Manifest at root
Place
gemini-extension.jsonat the absolute root of the repo. The daily crawler lists the repo once it passes validation.
| Path | How it installs | Pinning |
|---|---|---|
| Plain git repo | Users run gemini extensions install <repo-uri>; pushing to the referenced branch prompts an update; HEAD = latest | --ref=stable pins a branch/tag used as a channel (stable/preview/dev) |
| GitHub Releases | Installs the release marked Latest, skipping a clone (faster) | --ref <tag> grabs a version; --pre-release takes newest even if not Latest |
Knowledge check
You edit a `.toml` file in your project's `.gemini/commands/` and want it available immediately; later you `gemini extensions install` a teammate's extension. What makes each take effect?
Reach the end and this star joins your charted sky.