Compare commits

..

17 Commits

Author SHA1 Message Date
gemini-cli-robot 83a3b07050 chore(release): v0.25.2 2026-01-23 23:51:33 +00:00
gemini-cli-robot 18e854c330 fix(patch): cherry-pick 9866eb0 to release/v0.25.1-pr-17166 to patch version v0.25.1 and create version 0.25.2 (#17419)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-01-23 15:32:39 -08:00
gemini-cli-robot 29d4b1e6b8 chore(release): v0.25.1 2026-01-22 19:12:45 +00:00
gemini-cli-robot 2a8e1a8cc1 fix(patch): cherry-pick 87a0db2 to release/v0.25.0-pr-17308 [CONFLICTS] (#17316)
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
2026-01-22 18:31:29 +00:00
gemini-cli-robot c9dbf70043 chore(release): v0.25.0 2026-01-21 05:09:32 +00:00
gemini-cli-robot eb88343419 chore(release): v0.25.0-preview.4 2026-01-20 18:09:08 +00:00
Adam Weidman 217f277580 fix: update currentSequenceModel when modelChanged (#17051) 2026-01-20 09:40:43 -08:00
Pyry Takala 02e68e4554 Fix: Process all parts in response chunks when thought is first (#13539) 2026-01-20 09:40:41 -08:00
gemini-cli-robot 982fd1fc29 chore(release): v0.25.0-preview.3 2026-01-20 17:21:59 +00:00
gemini-cli-robot 6289c3ee3f fix(patch): cherry-pick 3b55581 to release/v0.25.0-preview.2-pr-16506 to patch version v0.25.0-preview.2 and create version 0.25.0-preview.3 (#17098)
Co-authored-by: christine betts <chrstn@uw.edu>
2026-01-20 08:56:21 -08:00
gemini-cli-robot b1f7a7e6f7 chore(release): v0.25.0-preview.2 2026-01-20 05:59:38 +00:00
N. Taylor Mullen ea0e3de430 fix(core): deduplicate ModelInfo emission in GeminiClient (#17075) 2026-01-19 21:26:09 -08:00
N. Taylor Mullen f6a5fa0e03 fix(ui): ensure rationale renders before tool calls (#17043) 2026-01-19 21:25:49 -08:00
gemini-cli-robot de86bccd0d chore(release): v0.25.0-preview.1 2026-01-19 01:50:22 +00:00
christine betts 46079d9daa Patch #16730 into v0.25.0 preview (#16882)
Co-authored-by: Sehoon Shon <sshon@google.com>
2026-01-16 20:36:17 +00:00
gemini-cli-robot 1d5e792a41 fix(patch): cherry-pick cfdc4cf to release/v0.25.0-preview.0-pr-16759 to patch version v0.25.0-preview.0 and create version 0.25.0-preview.1 (#16866)
Co-authored-by: christine betts <chrstn@uw.edu>
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-01-16 11:20:33 -08:00
gemini-cli-robot cd7a5c9604 chore(release): v0.25.0-preview.0 2026-01-14 13:13:18 +00:00
551 changed files with 12263 additions and 35495 deletions
-16
View File
@@ -1,16 +0,0 @@
description = "Analyze the influence of system instructions on a specific action."
prompt = """
# Introspection Task
Take a step back and analyze your own system instructions and internal logic.
The user is curious about the reasoning behind a specific action or decision you've made.
**Specific point of interest:** {{args}}
Please provide a detailed breakdown of:
1. Which parts of your system instructions (global, workspace-specific, or provided via GEMINI.md) influenced this behavior?
2. What was your internal thought process leading up to this action?
3. Are there any ambiguities or conflicting instructions that played a role?
Your goal is to provide transparency into your underlying logic so the user can potentially improve the instructions in the future.
"""
-65
View File
@@ -1,65 +0,0 @@
---
name: code-reviewer
description:
Use this skill to review code. It supports both local changes (staged or working tree)
and remote Pull Requests (by ID or URL). It focuses on correctness, maintainability,
and adherence to project standards.
---
# Code Reviewer
This skill guides the agent in conducting professional and thorough code reviews for both local development and remote Pull Requests.
## Workflow
### 1. Determine Review Target
* **Remote PR**: If the user provides a PR number or URL (e.g., "Review PR #123"), target that remote PR.
* **Local Changes**: If no specific PR is mentioned, or if the user asks to "review my changes", target the current local file system states (staged and unstaged changes).
### 2. Preparation
#### For Remote PRs:
1. **Checkout**: Use the GitHub CLI to checkout the PR.
```bash
gh pr checkout <PR_NUMBER>
```
2. **Preflight**: Execute the project's standard verification suite to catch automated failures early.
```bash
npm run preflight
```
3. **Context**: Read the PR description and any existing comments to understand the goal and history.
#### For Local Changes:
1. **Identify Changes**:
* Check status: `git status`
* Read diffs: `git diff` (working tree) and/or `git diff --staged` (staged).
2. **Preflight (Optional)**: If the changes are substantial, ask the user if they want to run `npm run preflight` before reviewing.
### 3. In-Depth Analysis
Analyze the code changes based on the following pillars:
* **Correctness**: Does the code achieve its stated purpose without bugs or logical errors?
* **Maintainability**: Is the code clean, well-structured, and easy to understand and modify in the future? Consider factors like code clarity, modularity, and adherence to established design patterns.
* **Readability**: Is the code well-commented (where necessary) and consistently formatted according to our project's coding style guidelines?
* **Efficiency**: Are there any obvious performance bottlenecks or resource inefficiencies introduced by the changes?
* **Security**: Are there any potential security vulnerabilities or insecure coding practices?
* **Edge Cases and Error Handling**: Does the code appropriately handle edge cases and potential errors?
* **Testability**: Is the new or modified code adequately covered by tests (even if preflight checks pass)? Suggest additional test cases that would improve coverage or robustness.
### 4. Provide Feedback
#### Structure
* **Summary**: A high-level overview of the review.
* **Findings**:
* **Critical**: Bugs, security issues, or breaking changes.
* **Improvements**: Suggestions for better code quality or performance.
* **Nitpicks**: Formatting or minor style issues (optional).
* **Conclusion**: Clear recommendation (Approved / Request Changes).
#### Tone
* Be constructive, professional, and friendly.
* Explain *why* a change is requested.
* For approvals, acknowledge the specific value of the contribution.
### 5. Cleanup (Remote PRs only)
* After the review, ask the user if they want to switch back to the default branch (e.g., `main` or `master`).
-63
View File
@@ -1,63 +0,0 @@
---
name: docs-writer
description:
Use this skill when asked to write documentation (`/docs` directory)
for Gemini CLI.
---
# `docs-writer` skill instructions
As an expert technical writer for the Gemini CLI project, your goal is to
produce documentation that is accurate, clear, and consistent with the project's
standards. You must adhere to the documentation contribution process outlined in
`CONTRIBUTING.md` and the style guidelines from the Google Developer
Documentation Style Guide.
## Step 1: Understand the goal and create a plan
1. **Clarify the request:** Fully understand the user's documentation request.
Identify the core feature, command, or concept that needs to be documented.
2. **Ask questions:** If the request is ambiguous or lacks detail, ask
clarifying questions. Don't invent or assume. It's better to ask than to
write incorrect documentation.
3. **Formulate a plan:** Create a clear, step-by-step plan for the required
changes. If requested or necessary, store this plan in a temporary file or
a file identified by the user.
## Step 2: Investigate and gather information
1. **Read the code:** Thoroughly examine the relevant codebase, primarily within
the `packages/` directory, to ensure your writing is backed by the
implementation.
2. **Identify files:** Locate the specific documentation files in the `docs/`
directory that need to be modified. Always read the latest
version of a file before you begin to edit it.
3. **Check for connections:** Consider related documentation. If you add a new
page, check if `docs/sidebar.json` needs to be updated. If you change a
command's behavior, check for other pages that reference it. Make sure links
in these pages are up to date.
## Step 3: Draft the documentation
1. **Follow the style guide:**
- Text must be wrapped at 80 characters. Exceptions are long links or
tables, unless otherwise stated by the user.
- Use sentence case for headings, titles, and bolded text.
- Address the reader as "you".
- Use contractions to keep the tone more casual.
- Use simple, direct, and active language and the present tense.
- Keep paragraphs short and focused.
- Always refer to Gemini CLI as `Gemini CLI`, never `the Gemini CLI`.
2. **Use `replace` and `write_file`:** Use the file system tools to apply your
planned changes precisely. For small edits, `replace` is preferred. For new
files or large rewrites, `write_file` is more appropriate.
## Step 4: Verify and finalize
1. **Review your work:** After making changes, re-read the files to ensure the
documentation is well-formatted, content is correct and based on existing
code, and that all new links are valid.
2. **Offer to run npm format:** Once all changes are complete and the user
confirms they have no more requests, offer to run the project's formatting
script to ensure consistency. Propose the following command:
`npm run format`
+5 -27
View File
@@ -14,25 +14,16 @@ repository's standards.
Follow these steps to create a Pull Request:
1. **Branch Management**: Check the current branch to avoid working directly
on `main`.
- Run `git branch --show-current`.
- If the current branch is `main`, create and switch to a new descriptive
branch:
```bash
git checkout -b <new-branch-name>
```
2. **Locate Template**: Search for a pull request template in the repository.
1. **Locate Template**: Search for a pull request template in the repository.
- Check `.github/pull_request_template.md`
- Check `.github/PULL_REQUEST_TEMPLATE.md`
- If multiple templates exist (e.g., in `.github/PULL_REQUEST_TEMPLATE/`),
ask the user which one to use or select the most appropriate one based on
the context (e.g., `bug_fix.md` vs `feature.md`).
3. **Read Template**: Read the content of the identified template file.
2. **Read Template**: Read the content of the identified template file.
4. **Draft Description**: Create a PR description that strictly follows the
3. **Draft Description**: Create a PR description that strictly follows the
template's structure.
- **Headings**: Keep all headings from the template.
- **Checklists**: Review each item. Mark with `[x]` if completed. If an item
@@ -44,22 +35,9 @@ Follow these steps to create a Pull Request:
- **Related Issues**: Link any issues fixed or related to this PR (e.g.,
"Fixes #123").
5. **Preflight Check**: Before creating the PR, run the workspace preflight
script to ensure all build, lint, and test checks pass.
4. **Create PR**: Use the `gh` CLI to create the PR.
```bash
npm run preflight
```
If any checks fail, address the issues before proceeding to create the PR.
6. **Create PR**: Use the `gh` CLI to create the PR. To avoid shell escaping
issues with multi-line Markdown, write the description to a temporary file
first.
```bash
# 1. Write the drafted description to a temporary file
# 2. Create the PR using the --body-file flag
gh pr create --title "type(scope): succinct description" --body-file <temp_file_path>
# 3. Remove the temporary file
rm <temp_file_path>
gh pr create --title "type(scope): succinct description" --body "..."
```
- **Title**: Ensure the title follows the
[Conventional Commits](https://www.conventionalcommits.org/) format if the
+2
View File
@@ -1,5 +1,7 @@
name: 'Bug Report'
description: 'Report a bug to help us improve Gemini CLI'
labels:
- 'status/need-triage'
body:
- type: 'markdown'
attributes:
+20 -18
View File
@@ -1,33 +1,35 @@
# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
open-pull-requests-limit: 10
interval: 'daily'
target-branch: 'main'
commit-message:
prefix: 'chore(deps)'
include: 'scope'
reviewers:
- 'joshualitt'
- 'google-gemini/gemini-cli-askmode-approvers'
groups:
npm-dependencies:
patterns:
- '*'
# Group all non-major updates together.
# This is to reduce the number of PRs that need to be reviewed.
# Major updates will still be created as separate PRs.
npm-minor-patch:
applies-to: 'version-updates'
update-types:
- 'minor'
- 'patch'
open-pull-requests-limit: 0
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
open-pull-requests-limit: 10
interval: 'daily'
target-branch: 'main'
commit-message:
prefix: 'chore(deps)'
include: 'scope'
reviewers:
- 'joshualitt'
groups:
actions-dependencies:
patterns:
- '*'
update-types:
- 'minor'
- 'patch'
- 'google-gemini/gemini-cli-askmode-approvers'
open-pull-requests-limit: 0
-138
View File
@@ -1,138 +0,0 @@
/* eslint-disable */
/* global require, console, process */
/**
* Script to backfill the 'status/need-triage' label to all open issues
* that are NOT currently labeled with '🔒 maintainer only' or 'help wanted'.
*/
const { execFileSync } = require('child_process');
const isDryRun = process.argv.includes('--dry-run');
const REPO = 'google-gemini/gemini-cli';
/**
* Executes a GitHub CLI command safely using an argument array to prevent command injection.
* @param {string[]} args
* @returns {string|null}
*/
function runGh(args) {
try {
// Using execFileSync with an array of arguments is safe as it doesn't use a shell.
// We set a large maxBuffer (10MB) to handle repositories with many issues.
return execFileSync('gh', args, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
} catch (error) {
const stderr = error.stderr ? ` Stderr: ${error.stderr.trim()}` : '';
console.error(
`❌ Error running gh ${args.join(' ')}: ${error.message}${stderr}`,
);
return null;
}
}
async function main() {
console.log('🔐 GitHub CLI security check...');
const authStatus = runGh(['auth', 'status']);
if (authStatus === null) {
console.error('❌ GitHub CLI (gh) is not installed or not authenticated.');
process.exit(1);
}
if (isDryRun) {
console.log('🧪 DRY RUN MODE ENABLED - No changes will be made.\n');
}
console.log(`🔍 Fetching and filtering open issues from ${REPO}...`);
// We use the /issues endpoint with pagination to bypass the 1000-result limit.
// The jq filter ensures we exclude PRs, maintainer-only, help-wanted, and existing status/need-triage.
const jqFilter =
'.[] | select(.pull_request == null) | select([.labels[].name] as $l | (any($l[]; . == "🔒 maintainer only") | not) and (any($l[]; . == "help wanted") | not) and (any($l[]; . == "status/need-triage") | not)) | {number: .number, title: .title}';
const output = runGh([
'api',
`repos/${REPO}/issues?state=open&per_page=100`,
'--paginate',
'--jq',
jqFilter,
]);
if (output === null) {
process.exit(1);
}
const issues = output
.split('\n')
.filter((line) => line.trim())
.map((line) => {
try {
return JSON.parse(line);
} catch (_e) {
console.error(`⚠️ Failed to parse line: ${line}`);
return null;
}
})
.filter(Boolean);
console.log(`✅ Found ${issues.length} issues matching criteria.`);
if (issues.length === 0) {
console.log('✨ No issues need backfilling.');
return;
}
let successCount = 0;
let failCount = 0;
if (isDryRun) {
for (const issue of issues) {
console.log(
`[DRY RUN] Would label issue #${issue.number}: ${issue.title}`,
);
}
successCount = issues.length;
} else {
console.log(`🏷️ Applying labels to ${issues.length} issues...`);
for (const issue of issues) {
const issueNumber = String(issue.number);
console.log(`🏷️ Labeling issue #${issueNumber}: ${issue.title}`);
const result = runGh([
'issue',
'edit',
issueNumber,
'--add-label',
'status/need-triage',
'--repo',
REPO,
]);
if (result !== null) {
successCount++;
} else {
failCount++;
}
}
}
console.log(`\n📊 Summary:`);
console.log(` - Success: ${successCount}`);
console.log(` - Failed: ${failCount}`);
if (failCount > 0) {
console.error(`\n❌ Backfill completed with ${failCount} errors.`);
process.exit(1);
} else {
console.log(`\n🎉 ${isDryRun ? 'Dry run' : 'Backfill'} complete!`);
}
}
main().catch((error) => {
console.error('❌ Unexpected error:', error);
process.exit(1);
});
@@ -1,190 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable */
/* global require, console, process */
/**
* Script to backfill a process change notification comment to all open PRs
* not created by members of the 'gemini-cli-maintainers' team.
*
* Skip PRs that are already associated with an issue.
*/
const { execFileSync } = require('child_process');
const isDryRun = process.argv.includes('--dry-run');
const REPO = 'google-gemini/gemini-cli';
const ORG = 'google-gemini';
const TEAM_SLUG = 'gemini-cli-maintainers';
const DISCUSSION_URL =
'https://github.com/google-gemini/gemini-cli/discussions/16706';
/**
* Executes a GitHub CLI command safely using an argument array.
*/
function runGh(args, options = {}) {
const { silent = false } = options;
try {
return execFileSync('gh', args, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
} catch (error) {
if (!silent) {
const stderr = error.stderr ? ` Stderr: ${error.stderr.trim()}` : '';
console.error(
`❌ Error running gh ${args.join(' ')}: ${error.message}${stderr}`,
);
}
return null;
}
}
/**
* Checks if a user is a member of the maintainers team.
*/
const membershipCache = new Map();
function isMaintainer(username) {
if (membershipCache.has(username)) return membershipCache.get(username);
// GitHub returns 404 if user is not a member.
// We use silent: true to avoid logging 404s as errors.
const result = runGh(
['api', `orgs/${ORG}/teams/${TEAM_SLUG}/memberships/${username}`],
{ silent: true },
);
const isMember = result !== null;
membershipCache.set(username, isMember);
return isMember;
}
async function main() {
console.log('🔐 GitHub CLI security check...');
if (runGh(['auth', 'status']) === null) {
console.error('❌ GitHub CLI (gh) is not authenticated.');
process.exit(1);
}
if (isDryRun) {
console.log('🧪 DRY RUN MODE ENABLED\n');
}
console.log(`📥 Fetching open PRs from ${REPO}...`);
// Fetch number, author, and closingIssuesReferences to check if linked to an issue
const prsJson = runGh([
'pr',
'list',
'--repo',
REPO,
'--state',
'open',
'--limit',
'1000',
'--json',
'number,author,closingIssuesReferences',
]);
if (prsJson === null) process.exit(1);
const prs = JSON.parse(prsJson);
console.log(`📊 Found ${prs.length} open PRs. Filtering...`);
let targetPrs = [];
for (const pr of prs) {
const author = pr.author.login;
const issueCount = pr.closingIssuesReferences
? pr.closingIssuesReferences.length
: 0;
if (issueCount > 0) {
// Skip if already linked to an issue
continue;
}
if (!isMaintainer(author)) {
targetPrs.push(pr);
}
}
console.log(
`✅ Found ${targetPrs.length} PRs from non-maintainers without associated issues.`,
);
const commentBody =
"\nHi @{AUTHOR}, thank you so much for your contribution to Gemini CLI! We really appreciate the time and effort you've put into this.\n\nWe're making some updates to our contribution process to improve how we track and review changes. Please take a moment to review our recent discussion post: [Improving Our Contribution Process & Introducing New Guidelines](${DISCUSSION_URL}).\n\nKey Update: Starting **January 26, 2026**, the Gemini CLI project will require all pull requests to be associated with an existing issue. Any pull requests not linked to an issue by that date will be automatically closed.\n\nThank you for your understanding and for being a part of our community!\n ".trim();
let successCount = 0;
let skipCount = 0;
let failCount = 0;
for (const pr of targetPrs) {
const prNumber = String(pr.number);
const author = pr.author.login;
// Check if we already commented (idempotency)
// We use silent: true here because view might fail if PR is deleted mid-run
const existingComments = runGh(
[
'pr',
'view',
prNumber,
'--repo',
REPO,
'--json',
'comments',
'--jq',
`.comments[].body | contains("${DISCUSSION_URL}")`,
],
{ silent: true },
);
if (existingComments && existingComments.includes('true')) {
console.log(
`⏭️ PR #${prNumber} already has the notification. Skipping.`,
);
skipCount++;
continue;
}
if (isDryRun) {
console.log(`[DRY RUN] Would notify @${author} on PR #${prNumber}`);
successCount++;
} else {
console.log(`💬 Notifying @${author} on PR #${prNumber}...`);
const personalizedComment = commentBody.replace('{AUTHOR}', author);
const result = runGh([
'pr',
'comment',
prNumber,
'--repo',
REPO,
'--body',
personalizedComment,
]);
if (result !== null) {
successCount++;
} else {
failCount++;
}
}
}
console.log(`\n📊 Summary:`);
console.log(` - Notified: ${successCount}`);
console.log(` - Skipped: ${skipCount}`);
console.log(` - Failed: ${failCount}`);
if (failCount > 0) process.exit(1);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+33 -44
View File
@@ -1,58 +1,51 @@
#!/usr/bin/env bash
# @license
# Copyright 2026 Google LLC
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail
# Initialize a comma-separated string to hold PR numbers that need a comment
PRS_NEEDING_COMMENT=""
# Global cache for issue labels (compatible with Bash 3.2)
# Stores "|ISSUE_NUM:LABELS|" segments
ISSUE_LABELS_CACHE_FLAT="|"
# Stores "ISSUE_NUM:LABELS" pairs separated by spaces
ISSUE_LABELS_CACHE_FLAT=""
# Function to get labels from an issue (with caching)
# Function to get area and priority labels from an issue (with caching)
get_issue_labels() {
local ISSUE_NUM="${1}"
local ISSUE_NUM=$1
if [[ -z "${ISSUE_NUM}" || "${ISSUE_NUM}" == "null" || "${ISSUE_NUM}" == "" ]]; then
return
fi
# Check cache
case "${ISSUE_LABELS_CACHE_FLAT}" in
*"|${ISSUE_NUM}:"*)
local suffix="${ISSUE_LABELS_CACHE_FLAT#*|${ISSUE_NUM}:}"
echo "${suffix%%|*}"
case " ${ISSUE_LABELS_CACHE_FLAT} " in
*" ${ISSUE_NUM}:"*)
local suffix="${ISSUE_LABELS_CACHE_FLAT#* ${ISSUE_NUM}:}"
echo "${suffix%% *}"
return
;;
*)
# Cache miss, proceed to fetch
;;
esac
echo " 📥 Fetching labels from issue #${ISSUE_NUM}" >&2
echo " 📥 Fetching area and priority labels from issue #${ISSUE_NUM}" >&2
local gh_output
if ! gh_output=$(gh issue view "${ISSUE_NUM}" --repo "${GITHUB_REPOSITORY}" --json labels -q '.labels[].name' 2>/dev/null); then
echo " ⚠️ Could not fetch issue #${ISSUE_NUM}" >&2
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT}${ISSUE_NUM}:|"
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT} ${ISSUE_NUM}:"
return
fi
local labels
labels=$(echo "${gh_output}" | grep -x -E '(area|priority)/.*|help wanted|🔒 maintainer only' | tr '\n' ',' | sed 's/,$//' || echo "")
labels=$(echo "${gh_output}" | grep -E "^(area|priority)/" | tr '\n' ',' | sed 's/,$//' || echo "")
# Save to flat cache
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT}${ISSUE_NUM}:${labels}|"
echo "${labels}"
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT} ${ISSUE_NUM}:${labels}"
echo "$labels"
}
# Function to process a single PR with pre-fetched data
process_pr_optimized() {
local PR_NUMBER="${1}"
local IS_DRAFT="${2}"
local ISSUE_NUMBER="${3}"
local CURRENT_LABELS="${4}" # Comma-separated labels
local PR_NUMBER=$1
local IS_DRAFT=$2
local ISSUE_NUMBER=$3
local CURRENT_LABELS=$4 # Comma-separated labels
echo "🔄 Processing PR #${PR_NUMBER}"
@@ -62,13 +55,13 @@ process_pr_optimized() {
if [[ -z "${ISSUE_NUMBER}" || "${ISSUE_NUMBER}" == "null" || "${ISSUE_NUMBER}" == "" ]]; then
if [[ "${IS_DRAFT}" == "true" ]]; then
echo " 📝 PR #${PR_NUMBER} is a draft and has no linked issue"
if [[ ",${CURRENT_LABELS}," == *",status/need-issue,"* ]]; then
if [[ ",${CURRENT_LABELS}," == ",status/need-issue,"* ]]; then
echo " Removing status/need-issue label"
LABELS_TO_REMOVE="status/need-issue"
fi
else
echo " ⚠️ No linked issue found for PR #${PR_NUMBER}"
if [[ ",${CURRENT_LABELS}," != *",status/need-issue,"* ]]; then
if [[ ",${CURRENT_LABELS}," != ",status/need-issue,"* ]]; then
echo " Adding status/need-issue label"
LABELS_TO_ADD="status/need-issue"
fi
@@ -82,7 +75,7 @@ process_pr_optimized() {
else
echo " 🔗 Found linked issue #${ISSUE_NUMBER}"
if [[ ",${CURRENT_LABELS}," == *",status/need-issue,"* ]]; then
if [[ ",${CURRENT_LABELS}," == ",status/need-issue,"* ]]; then
echo " Removing status/need-issue label"
LABELS_TO_REMOVE="status/need-issue"
fi
@@ -91,7 +84,7 @@ process_pr_optimized() {
ISSUE_LABELS=$(get_issue_labels "${ISSUE_NUMBER}")
if [[ -n "${ISSUE_LABELS}" ]]; then
local IFS_OLD="${IFS}"
local IFS_OLD=$IFS
IFS=','
for label in ${ISSUE_LABELS}; do
if [[ -n "${label}" ]] && [[ ",${CURRENT_LABELS}," != *",${label},"* ]]; then
@@ -101,8 +94,8 @@ process_pr_optimized() {
LABELS_TO_ADD="${LABELS_TO_ADD},${label}"
fi
fi
done
IFS="${IFS_OLD}"
done
IFS=$IFS_OLD
fi
if [[ -z "${LABELS_TO_ADD}" && -z "${LABELS_TO_REMOVE}" ]]; then
@@ -121,7 +114,7 @@ done
EDIT_CMD+=("--remove-label" "${LABELS_TO_REMOVE}")
fi
("${EDIT_CMD[@]}" || true)
("${EDIT_CMD[@]}" 2>/dev/null || true)
fi
}
@@ -142,7 +135,7 @@ JQ_EXTRACT_FIELDS='{
labels: [.labels[].name] | join(",")
}'
JQ_TSV_FORMAT='"\((.number | tostring))\t\(.isDraft)\t\((.issue // null) | tostring)\t\(.labels)"'
JQ_TSV_FORMAT='"\((.number | tostring))\t\(.isDraft)\t\((.issue // "null") | tostring)\t\(.labels)"'
if [[ -n "${PR_NUMBER:-}" ]]; then
echo "🔄 Processing single PR #${PR_NUMBER}"
@@ -151,9 +144,9 @@ if [[ -n "${PR_NUMBER:-}" ]]; then
exit 1
}
line=$(echo "${PR_DATA}" | jq -r "${JQ_EXTRACT_FIELDS} | ${JQ_TSV_FORMAT}")
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "${line}"
process_pr_optimized "${pr_num}" "${is_draft}" "${issue_num}" "${current_labels}"
line=$(echo "$PR_DATA" | jq -r "$JQ_EXTRACT_FIELDS | $JQ_TSV_FORMAT")
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "$line"
process_pr_optimized "$pr_num" "$is_draft" "$issue_num" "$current_labels"
else
echo "📥 Getting all open pull requests..."
PR_DATA_ALL=$(gh pr list --repo "${GITHUB_REPOSITORY}" --state open --limit 1000 --json number,closingIssuesReferences,isDraft,body,labels 2>/dev/null) || {
@@ -164,15 +157,11 @@ else
PR_COUNT=$(echo "${PR_DATA_ALL}" | jq '. | length')
echo "📊 Found ${PR_COUNT} open PRs to process"
# Use a temporary file to avoid masking exit codes in process substitution
tmp_file=$(mktemp)
echo "${PR_DATA_ALL}" | jq -r ".[] | ${JQ_EXTRACT_FIELDS} | ${JQ_TSV_FORMAT}" > "${tmp_file}"
while read -r line; do
[[ -z "${line}" ]] && continue
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "${line}"
process_pr_optimized "${pr_num}" "${is_draft}" "${issue_num}" "${current_labels}"
done < "${tmp_file}"
rm -f "${tmp_file}"
[[ -z "$line" ]] && continue
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "$line"
process_pr_optimized "$pr_num" "$is_draft" "$issue_num" "$current_labels"
done < <(echo "${PR_DATA_ALL}" | jq -r ".[] | $JQ_EXTRACT_FIELDS | $JQ_TSV_FORMAT")
fi
if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then
@@ -181,4 +170,4 @@ else
echo "prs_needing_comment=[${PRS_NEEDING_COMMENT}]" >> "${GITHUB_OUTPUT}"
fi
echo "✅ PR triage completed"
echo "✅ PR triage completed"
-355
View File
@@ -1,355 +0,0 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* global process, console, require */
const { Octokit } = require('@octokit/rest');
/**
* Sync Maintainer Labels (Recursive with strict parent-child relationship detection)
* - Uses Native Sub-issues.
* - Uses Markdown Task Lists (- [ ] #123).
* - Filters for OPEN issues only.
* - Skips DUPLICATES.
* - Skips Pull Requests.
* - ONLY labels issues in the PUBLIC (gemini-cli) repo.
*/
const REPO_OWNER = 'google-gemini';
const PUBLIC_REPO = 'gemini-cli';
const PRIVATE_REPO = 'maintainers-gemini-cli';
const ALLOWED_REPOS = [PUBLIC_REPO, PRIVATE_REPO];
const ROOT_ISSUES = [
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15374 },
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15456 },
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15324 },
];
const TARGET_LABEL = '🔒 maintainer only';
const isDryRun =
process.argv.includes('--dry-run') || process.env.DRY_RUN === 'true';
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
/**
* Extracts child issue references from markdown Task Lists ONLY.
* e.g. - [ ] #123 or - [x] google-gemini/gemini-cli#123
*/
function extractTaskListLinks(text, contextOwner, contextRepo) {
if (!text) return [];
const childIssues = new Map();
const add = (owner, repo, number) => {
if (ALLOWED_REPOS.includes(repo)) {
const key = `${owner}/${repo}#${number}`;
childIssues.set(key, { owner, repo, number: parseInt(number, 10) });
}
};
// 1. Full URLs in task lists
const urlRegex =
/-\s+\[[ x]\].*https:\/\/github\.com\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\/issues\/(\d+)\b/g;
let match;
while ((match = urlRegex.exec(text)) !== null) {
add(match[1], match[2], match[3]);
}
// 2. Cross-repo refs in task lists: owner/repo#123
const crossRepoRegex =
/-\s+\[[ x]\].*([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)#(\d+)\b/g;
while ((match = crossRepoRegex.exec(text)) !== null) {
add(match[1], match[2], match[3]);
}
// 3. Short refs in task lists: #123
const shortRefRegex = /-\s+\[[ x]\].*#(\d+)\b/g;
while ((match = shortRefRegex.exec(text)) !== null) {
add(contextOwner, contextRepo, match[1]);
}
return Array.from(childIssues.values());
}
/**
* Fetches issue data via GraphQL with full pagination for sub-issues, comments, and labels.
*/
async function fetchIssueData(owner, repo, number) {
const query = `
query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
issue(number:$number) {
state
title
body
labels(first: 100) {
nodes { name }
pageInfo { hasNextPage endCursor }
}
subIssues(first: 100) {
nodes {
number
repository {
name
owner { login }
}
}
pageInfo { hasNextPage endCursor }
}
comments(first: 100) {
nodes {
body
}
}
}
}
}
`;
try {
const response = await octokit.graphql(query, { owner, repo, number });
const data = response.repository.issue;
if (!data) return null;
const issue = {
state: data.state,
title: data.title,
body: data.body || '',
labels: data.labels.nodes.map((n) => n.name),
subIssues: [...data.subIssues.nodes],
comments: data.comments.nodes.map((n) => n.body),
};
// Paginate subIssues if there are more than 100
if (data.subIssues.pageInfo.hasNextPage) {
const moreSubIssues = await paginateConnection(
owner,
repo,
number,
'subIssues',
'number repository { name owner { login } }',
data.subIssues.pageInfo.endCursor,
);
issue.subIssues.push(...moreSubIssues);
}
// Paginate labels if there are more than 100 (unlikely but for completeness)
if (data.labels.pageInfo.hasNextPage) {
const moreLabels = await paginateConnection(
owner,
repo,
number,
'labels',
'name',
data.labels.pageInfo.endCursor,
(n) => n.name,
);
issue.labels.push(...moreLabels);
}
// Note: Comments are handled via Task Lists in body + first 100 comments.
// If an issue has > 100 comments with task lists, we'd need to paginate those too.
// Given the 1,100+ issue discovery count, 100 comments is usually sufficient,
// but we can add it for absolute completeness.
// (Skipping for now to avoid excessive API churn unless clearly needed).
return issue;
} catch (error) {
if (error.errors && error.errors.some((e) => e.type === 'NOT_FOUND')) {
return null;
}
throw error;
}
}
/**
* Helper to paginate any GraphQL connection.
*/
async function paginateConnection(
owner,
repo,
number,
connectionName,
nodeFields,
initialCursor,
transformNode = (n) => n,
) {
let additionalNodes = [];
let hasNext = true;
let cursor = initialCursor;
while (hasNext) {
const query = `
query($owner:String!, $repo:String!, $number:Int!, $cursor:String) {
repository(owner:$owner, name:$repo) {
issue(number:$number) {
${connectionName}(first: 100, after: $cursor) {
nodes { ${nodeFields} }
pageInfo { hasNextPage endCursor }
}
}
}
}
`;
const response = await octokit.graphql(query, {
owner,
repo,
number,
cursor,
});
const connection = response.repository.issue[connectionName];
additionalNodes.push(...connection.nodes.map(transformNode));
hasNext = connection.pageInfo.hasNextPage;
cursor = connection.pageInfo.endCursor;
}
return additionalNodes;
}
/**
* Validates if an issue should be processed (Open, not a duplicate, not a PR)
*/
function shouldProcess(issueData) {
if (!issueData) return false;
if (issueData.state !== 'OPEN') return false;
const labels = issueData.labels.map((l) => l.toLowerCase());
if (labels.includes('duplicate') || labels.includes('kind/duplicate')) {
return false;
}
return true;
}
async function getAllDescendants(roots) {
const allDescendants = new Map();
const visited = new Set();
const queue = [...roots];
for (const root of roots) {
visited.add(`${root.owner}/${root.repo}#${root.number}`);
}
console.log(`Starting discovery from ${roots.length} roots...`);
while (queue.length > 0) {
const current = queue.shift();
const currentKey = `${current.owner}/${current.repo}#${current.number}`;
try {
const issueData = await fetchIssueData(
current.owner,
current.repo,
current.number,
);
if (!shouldProcess(issueData)) {
continue;
}
// ONLY add to labeling list if it's in the PUBLIC repository
if (current.repo === PUBLIC_REPO) {
// Don't label the roots themselves
if (
!ROOT_ISSUES.some(
(r) => r.number === current.number && r.repo === current.repo,
)
) {
allDescendants.set(currentKey, {
...current,
title: issueData.title,
labels: issueData.labels,
});
}
}
const children = new Map();
// 1. Process Native Sub-issues
if (issueData.subIssues) {
for (const node of issueData.subIssues) {
const childOwner = node.repository.owner.login;
const childRepo = node.repository.name;
const childNumber = node.number;
const key = `${childOwner}/${childRepo}#${childNumber}`;
children.set(key, {
owner: childOwner,
repo: childRepo,
number: childNumber,
});
}
}
// 2. Process Markdown Task Lists in Body and Comments
let combinedText = issueData.body || '';
if (issueData.comments) {
for (const commentBody of issueData.comments) {
combinedText += '\n' + (commentBody || '');
}
}
const taskListLinks = extractTaskListLinks(
combinedText,
current.owner,
current.repo,
);
for (const link of taskListLinks) {
const key = `${link.owner}/${link.repo}#${link.number}`;
children.set(key, link);
}
// Queue children (regardless of which repo they are in, for recursion)
for (const [key, child] of children) {
if (!visited.has(key)) {
visited.add(key);
queue.push(child);
}
}
} catch (error) {
console.error(`Error processing ${currentKey}: ${error.message}`);
}
}
return Array.from(allDescendants.values());
}
async function run() {
if (isDryRun) {
console.log('=== DRY RUN MODE: No labels will be applied ===');
}
const descendants = await getAllDescendants(ROOT_ISSUES);
console.log(
`\nFound ${descendants.length} total unique open descendant issues in ${PUBLIC_REPO}.`,
);
for (const issueInfo of descendants) {
const issueKey = `${issueInfo.owner}/${issueInfo.repo}#${issueInfo.number}`;
try {
// Data is already available from the discovery phase
const hasLabel = issueInfo.labels.some((l) => l === TARGET_LABEL);
if (!hasLabel) {
if (isDryRun) {
console.log(
`[DRY RUN] Would label ${issueKey}: "${issueInfo.title}"`,
);
} else {
console.log(`Labeling ${issueKey}: "${issueInfo.title}"...`);
await octokit.rest.issues.addLabels({
owner: issueInfo.owner,
repo: issueInfo.repo,
issue_number: issueInfo.number,
labels: [TARGET_LABEL],
});
}
}
} catch (error) {
console.error(`Error processing label for ${issueKey}: ${error.message}`);
}
}
}
run().catch((error) => {
console.error(error);
process.exit(1);
});
+2 -27
View File
@@ -9,10 +9,6 @@ on:
description: 'Run all evaluations (including usually passing)'
type: 'boolean'
default: true
test_name_pattern:
description: 'Test name pattern or file name'
required: false
type: 'string'
permissions:
contents: 'read'
@@ -26,12 +22,6 @@ jobs:
strategy:
fail-fast: false
matrix:
model:
- 'gemini-3-pro-preview'
- 'gemini-3-flash-preview'
- 'gemini-2.5-pro'
- 'gemini-2.5-flash'
- 'gemini-2.5-flash-lite'
run_attempt: [1, 2, 3]
steps:
- name: 'Checkout'
@@ -53,31 +43,16 @@ jobs:
run: 'mkdir -p evals/logs'
- name: 'Run Evals'
continue-on-error: true
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_MODEL: '${{ matrix.model }}'
RUN_EVALS: "${{ github.event.inputs.run_all != 'false' }}"
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
run: |
CMD="npm run test:all_evals"
PATTERN="${{ env.TEST_NAME_PATTERN }}"
if [[ -n "$PATTERN" ]]; then
if [[ "$PATTERN" == *.ts || "$PATTERN" == *.js || "$PATTERN" == */* ]]; then
$CMD -- "$PATTERN"
else
$CMD -- -t "$PATTERN"
fi
else
$CMD
fi
run: 'npm run test:all_evals'
- name: 'Upload Logs'
if: 'always()'
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'eval-logs-${{ matrix.model }}-${{ matrix.run_attempt }}'
name: 'eval-logs-${{ matrix.run_attempt }}'
path: 'evals/logs'
retention-days: 7
@@ -95,8 +95,7 @@ jobs:
id: 'generate_token'
env:
APP_ID: '${{ secrets.APP_ID }}'
if: |-
${{ env.APP_ID != '' }}
if: "${{ env.APP_ID != '' }}"
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ secrets.APP_ID }}'
@@ -306,6 +305,22 @@ jobs:
});
core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`);
// Remove the 'status/need-triage' label
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: 'status/need-triage'
});
core.info(`Successfully removed 'status/need-triage' label.`);
} catch (error) {
// If the label doesn't exist, the API call will throw a 404. We can ignore this.
if (error.status !== 404) {
core.warning(`Failed to remove 'status/need-triage': ${error.message}`);
}
}
- name: 'Post Issue Analysis Failure Comment'
if: |-
${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
@@ -0,0 +1,127 @@
name: 'Gemini Automated PR Labeler'
on:
pull_request_target:
types: ['opened', 'reopened', 'synchronize']
jobs:
label-pr:
timeout-minutes: 10
if: |-
${{ github.repository == 'google-gemini/gemini-cli' }}
permissions:
pull-requests: 'write'
contents: 'read'
id-token: 'write'
concurrency:
group: '${{ github.workflow }}-${{ github.event.pull_request.number }}'
cancel-in-progress: true
runs-on: 'ubuntu-latest'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # v2
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
permission-pull-requests: 'write'
- name: 'Run Gemini PR size and complexity labeller'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # Use the specific commit SHA
env:
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
PR_NUMBER: '${{ github.event.pull_request.number }}'
REPOSITORY: '${{ github.repository }}'
with:
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |
{
"coreTools": [
"run_shell_command(gh pr diff)",
"run_shell_command(gh pr edit)",
"run_shell_command(gh pr comment)",
"run_shell_command(gh pr view)"
],
"telemetry": {
"enabled": true,
"target": "gcp"
},
"sandbox": false
}
prompt: |
You are a Pull Request labeller and Feedback Assistant. Your primary goal is to improve review velocity and help maintainers prioritize their work by automatically labeling pull requests based on size and complexity, and providing guidance for overly large PRs.
Steps:
1. Retrieve Pull Request Information:
- Use `gh pr diff ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }}` to get the diff content.
- Parse the output from `gh pr diff` to determine the total lines of code added and deleted. Calculate `TOTAL_LINES_CHANGED`.
2. Determine Pull Request Size:
- Use `gh pr view ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --json labels` to get the current labels on the PR.
- Check the current labels and identify if any `size/*` labels already exist (e.g., `size/xs`, `size/s`, etc.).
- If an old `size/*` label is found and it is different from the newly calculated size, remove it using:
`gh pr edit ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --remove-label "size-label-to-remove"`
- Based on `TOTAL_LINES_CHANGED`, select the appropriate new size label:
- `size/xs`: < 10 lines changed
- `size/s`: 10-50 lines changed
- `size/m`: 51-200 lines changed
- `size/l`: 201-1000 lines changed
- `size/xl`: > 1000 lines changed
- Do not invent new size labels.
- Apply the newly determined `size/*` label to the pull request using:
`gh pr edit ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --add-label "your-new-size-label"`
3. Analyze Pull Request Complexity:
- Perform Code Change Analysis: Examine the content of the code changes obtained from `gh pr diff ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }}`. Look for indicators of complexity such as:
- Number of files changed (can be inferred from the diff headers).
- Diversity of file types (e.g., changes across different languages, configuration files, documentation).
- Presence of new external dependencies.
- Introduction of new architectural components or significant refactoring.
- Complexity of individual code changes (e.g., deeply nested logic, complex algorithms, extensive conditional statements).
- Apply Heuristic-based Complexity Assessment:
- If the PR touches a small number of files with minor changes (e.g., typos, simple bug fixes, small feature additions), categorize it as `review/quick`.
- If the PR involves changes across multiple files, introduces new features, significantly refactors existing code, or has a high line count (even within `size/l`), categorize it as `review/involved`.
- Pay close attention to changes in critical or core modules as these inherently increase complexity.
- **Only use the labels `review/quick` or `review/involved` for complexity. Do not invent new complexity labels.**
- **Remove any previous `review/*` labels if they no longer apply, similar to the size label process.**
- Apply the determined `review/*` label to the pull request using:
`gh pr edit ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --add-label "your-complexity-label"`
4. Handle Overly Large Pull Requests (`size/xl`):
- **Conditional Check:** If the pull request has been labeled `size/xl` (i.e., > 1000 lines of code changed) in Step 2, proceed to the next sub-step.
- **Post Constructive Comment:** Post a polite and helpful comment on the pull request using:
`gh pr comment ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --body "Your comment here"`
The comment body should be:
"""
Thanks for your hard work on this pull request!
This pull request is quite large, which can make it challenging and time-consuming for reviewers to go through thoroughly.
To help us review it more efficiently and get your changes merged faster, we kindly request you consider breaking this into smaller, more focused pull requests. Each smaller PR should ideally address a single logical change or a small set of related changes.
For example, you could separate out refactoring, new feature additions, and bug fixes into individual PRs. This makes it easier to understand, review, and test each component independently.
We appreciate your understanding and cooperation. Feel free to reach out if you need any assistance with this!
"""
Guidelines:
- Automation Focus: All actions should be automated and not require manual intervention.
- Non-intrusive: The system should add labels and comments but not modify the code or close the pull request.
- Polite and Constructive: All communication, especially for large PRs, must be polite, encouraging, and constructive.
- Prioritize Clarity: The labels applied should clearly convey the PR's size and complexity to reviewers.
- Adhere to Defined Labels: Only use the specified `size/*` and `review/*` labels. Do not create or apply any other labels.
- Utilize `gh CLI`: Interact with GitHub using the `gh` command-line tool for diffing, label management, and commenting.
- Execute commands strictly as described in the steps. Do not invent new commands.
- In no case should you change other pull request that are not the one you are working on. Which can be found by using env.PR_NUMBER
- Execute each step that is defined in the steps section.
- In no case should you execute code from the pull request because this could be malicious code.
- If you fail to do this step log the errors you received
@@ -40,8 +40,7 @@ jobs:
permission-issues: 'write'
- name: 'Get issue from event'
if: |-
${{ github.event_name == 'issues' }}
if: "github.event_name == 'issues'"
id: 'get_issue_from_event'
env:
ISSUE_EVENT: '${{ toJSON(github.event.issue) }}'
@@ -52,8 +51,7 @@ jobs:
echo "✅ Found issue #${{ github.event.issue.number }} from event to triage! 🎯"
- name: 'Find untriaged issues'
if: |-
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
if: "github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'"
id: 'find_issues'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
@@ -61,26 +59,22 @@ jobs:
run: |-
set -euo pipefail
echo '🔍 Finding issues missing area labels...'
NO_AREA_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/unknown' --limit 100 --json number,title,body)"
echo '🔍 Finding issues without labels...'
NO_LABEL_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue no:label' --limit 10 --json number,title,body)"
echo '🔍 Finding issues missing kind labels...'
NO_KIND_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 100 --json number,title,body)"
echo '🏷️ Finding issues missing priority labels...'
NO_PRIORITY_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 100 --json number,title,body)"
echo '🏷️ Finding issues that need triage...'
NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search "is:open is:issue label:\"status/need-triage\"" --limit 10 --json number,title,body)"
echo '🔄 Merging and deduplicating issues...'
ISSUES="$(echo "${NO_AREA_ISSUES}" "${NO_KIND_ISSUES}" "${NO_PRIORITY_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
echo '📝 Setting output for GitHub Actions...'
echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}"
ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')"
echo "✅ Found ${ISSUE_COUNT} unique issues to triage! 🎯"
echo "✅ Found ${ISSUE_COUNT} issues to triage! 🎯"
- name: 'Get Repository Labels'
id: 'get_labels'
@@ -139,32 +133,38 @@ jobs:
1. You are only able to use the echo command. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
2. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
3. Review the issue title, body and any comments provided in the environment variables.
4. Identify the most relevant labels from the existing labels, specifically focusing on area/*, kind/* and priority/*.
5. Label Policy:
- If the issue already has a kind/ label, do not change it.
- If the issue already has a priority/ label, do not change it.
- If the issue already has an area/ label, do not change it.
- If any of these are missing, select exactly ONE appropriate label for the missing category.
4. Identify the most relevant labels from the existing labels, focusing on kind/* and priority/*.
5. If the issue already has a kind/ label don't change it. And if the issue already has a priority/ label do not change it for example:
If an issue has kind/bug you will only add a priority/ label.
Instead if an issue has no labels, you could add one label of each kind.
6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc.
7. Give me a single short explanation about why you are selecting each label in the process.
8. Output a JSON array of objects, each containing the issue number
7. For kind/* limit yourself to only the single most applicable label.
8. Give me a single short explanation about why you are selecting each label in the process.
9. Output a JSON array of objects, each containing the issue number
and the labels to add and remove, along with an explanation. For example:
```
[
{
"issue_number": 123,
"labels_to_add": ["area/core", "kind/bug", "priority/p2"],
"labels_to_add": ["kind/bug", "priority/p2"],
"labels_to_remove": ["status/need-triage"],
"explanation": "This issue is a UI bug that needs to be addressed with medium priority."
"explanation": "This issue is a bug that needs to be addressed with medium priority."
},
{
"issue_number": 456,
"labels_to_add": ["kind/enhancement"],
"labels_to_remove": [],
"explanation": "This issue is an enhancement request that could improve the user experience."
}
]
```
If an issue cannot be classified, do not include it in the output array.
9. For each issue please check if CLI version is present, this is usually in the output of the /about command and will look like 0.1.5
10. For each issue please check if CLI version is present, this is usually in the output of the /about command and will look like 0.1.5
- Anything more than 6 versions older than the most recent should add the status/need-retesting label
10. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below.
11. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation.
12. If you are uncertain about a category, use the area/unknown, kind/question, or priority/unknown labels as appropriate. If you are extremely uncertain, apply the status/manual-triage label.
11. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below.
- After identifying appropriate labels to an issue, add "status/need-triage" label to labels_to_remove in the output.
12. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation.
13. If you are uncertain and have not been able to apply one each of kind/ and priority/ , apply the status/manual-triage label.
## Guidelines
@@ -174,46 +174,54 @@ jobs:
- Do not add comments or modify the issue content.
- Do not remove the following labels maintainer, help wanted or good first issue.
- Triage only the current issue.
- Identify only one area/ label.
- Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue)
- Identify only one priority/ label.
- Identify all applicable priority/* labels based on the issue content. It's ok to have multiple of these.
- Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario.
Categorization Guidelines (Priority):
P0 - Urgent Blocking Issues:
- DO NOT APPLY THIS LABEL AUTOMATICALLY. Use status/manual-triage instead.
- Definition: Urgent, block a significant percentage of the user base, and prevent frequent use of the Gemini CLI.
- This includes core stability blockers (e.g., authentication failures, broken upgrades), critical crashes, and P0 security vulnerabilities.
- Impact: Blocks development or testing for the entire team; Major security vulnerability; Causes data loss or corruption with no workaround; Crashes the application or makes a core feature completely unusable for all or most users.
- Qualifier: Is the main function of the software broken?
P1 - High-Impact Issues:
- Definition: Affect a large number of users, blocking them from using parts of the Gemini CLI, or make the CLI frequently unusable even with workarounds available.
- Impact: A core feature is broken or behaving incorrectly for a large number of users or use cases; Severe performance degradation; No straightforward workaround exists.
- Qualifier: Is a key feature unusable or giving very wrong results?
P2 - Significant Issues:
- Definition: Affect some users significantly, such as preventing the use of certain features or authentication types.
- Can also be issues that many users complain about, causing annoyance or hindering daily use.
- Impact: Affects a non-critical feature or a smaller, specific subset of users; An inconvenient but functional workaround is available; Noticeable UI/UX problems that look unprofessional.
- Qualifier: Is it an annoying but non-blocking problem?
P3 - Low-Impact Issues:
- Definition: Typically usability issues that cause annoyance to a limited user base.
- Includes feature requests that could be addressed in the near future and may be suitable for community contributions.
- Impact: Minor cosmetic issues; An edge-case bug that is very difficult to reproduce and affects a tiny fraction of users.
- Qualifier: Is it a "nice-to-fix" issue?
Categorization Guidelines (Area):
area/agent: Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality
area/core: User Interface, OS Support, Core Functionality
area/enterprise: Telemetry, Policy, Quota / Licensing
area/extensions: Gemini CLI extensions capability
area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation
area/platform: Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt
area/security: security related issues
Categorization Guidelines:
P0: Critical / Blocker
- A P0 bug is a catastrophic failure that demands immediate attention.
- To be a P0 it means almost all users are running into this issue and it is blocking users from being able to use the product.
- You would see this in the form of many comments from different developers on the bug.
- It represents a complete showstopper for a significant portion of users or for the development process itself.
Impact:
- Blocks development or testing for the entire team.
- Major security vulnerability that could compromise user data or system integrity.
- Causes data loss or corruption with no workaround.
- Crashes the application or makes a core feature completely unusable for all or most users in a production environment. Will it cause severe quality degration?
- Is it preventing contributors from contributing to the repository or is it a release blocker?
Qualifier: Is the main function of the software broken?
Example: The gemini auth login command fails with an unrecoverable error, preventing any user from authenticating and using the rest of the CLI.
P1: High
- A P1 bug is a serious issue that significantly degrades the user experience or impacts a core feature.
- While not a complete blocker, it's a major problem that needs a fast resolution. Feature requests are almost never P1.
- Once again this would be affecting many users.
- You would see this in the form of comments from different developers on the bug.
Impact:
- A core feature is broken or behaving incorrectly for a large number of users or large number of use cases.
- Review the bug details and comments to try figure out if this issue affects a large set of use cases or if it's a narrow set of use cases.
- Severe performance degradation making the application frustratingly slow.
- No straightforward workaround exists, or the workaround is difficult and non-obvious.
Qualifier: Is a key feature unusable or giving very wrong results?
Example: Gemini CLI enters a loop when making read-many-files tool call. I am unable to break out of the loop and gemini doesn't follow instructions subsequently.
P2: Medium
- A P2 bug is a moderately impactful issue. It's a noticeable problem but doesn't prevent the use of the software's main functionality.
Impact:
- Affects a non-critical feature or a smaller, specific subset of users.
- An inconvenient but functional workaround is available and easy to execute.
- Noticeable UI/UX problems that don't break functionality but look unprofessional (e.g., elements are misaligned or overlapping).
Qualifier: Is it an annoying but non-blocking problem?
Example: An error message is unclear or contains a typo, causing user confusion but not halting their workflow.
P3: Low
- A P3 bug is a minor, low-impact issue that is trivial or cosmetic. It has little to no effect on the overall functionality of the application.
Impact:
- Minor cosmetic issues like color inconsistencies, typos in documentation, or slight alignment problems on a non-critical page.
- An edge-case bug that is very difficult to reproduce and affects a tiny fraction of users.
Qualifier: Is it a "nice-to-fix" issue?
Example: Spelling mistakes etc.
Additional Context:
- If users are talking about issues where the model gets downgraded from pro to flash then i want you to categorize that as a performance issue.
- This product is designed to use different models eg.. using pro, downgrading to flash etc.
- When users report that they dont expect the model to change those would be categorized as feature requests.
- If users are talking about issues where the model gets downgraded from pro to flash then i want you to categorize that as a performance issue
- This product is designed to use different models eg.. using pro, downgrading to flash etc.
- When users report that they dont expect the model to change those would be categorized as feature requests.
- name: 'Apply Labels to Issues'
if: |-
@@ -230,16 +238,23 @@ jobs:
core.info(`Raw labels JSON: ${rawLabels}`);
let parsedLabels;
try {
// First, try to parse the raw output as JSON.
parsedLabels = JSON.parse(rawLabels.trim());
} catch (jsonError) {
// If that fails, check for a markdown code block.
core.info(`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`);
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
if (!jsonMatch || !jsonMatch[1]) {
throw new Error("Could not find a ```json ... ``` block in the output.");
if (jsonMatch && jsonMatch[1]) {
try {
parsedLabels = JSON.parse(jsonMatch[1].trim());
} catch (markdownError) {
core.setFailed(`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawLabels}`);
return;
}
} else {
core.setFailed(`Output is not valid JSON and does not contain a JSON markdown block.\nRaw output: ${rawLabels}`);
return;
}
const jsonString = jsonMatch[1].trim();
parsedLabels = JSON.parse(jsonString);
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
} catch (err) {
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
return;
}
for (const entry of parsedLabels) {
@@ -263,6 +278,24 @@ jobs:
core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`);
}
if (entry.labels_to_remove && entry.labels_to_remove.length > 0) {
for (const label of entry.labels_to_remove) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
core.info(`Successfully removed labels for #${issueNumber}: ${entry.labels_to_remove.join(', ')}`);
}
if (entry.explanation) {
await github.rest.issues.createComment({
owner: context.repo.owner,
@@ -1,46 +0,0 @@
name: '🏷️ Issue Opened Labeler'
on:
issues:
types:
- 'opened'
jobs:
label-issue:
runs-on: 'ubuntu-latest'
if: |-
${{ github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli' }}
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
env:
APP_ID: '${{ secrets.APP_ID }}'
if: |-
${{ env.APP_ID != '' }}
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
- name: 'Add need-triage label'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const hasLabel = issue.labels.some(l => l.name === 'status/need-triage');
if (!hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['status/need-triage']
});
} else {
core.info('Issue already has status/need-triage label. Skipping.');
}
@@ -3,55 +3,38 @@ name: 'Label Child Issues for Project Rollup'
on:
issues:
types: ['opened', 'edited', 'reopened']
schedule:
- cron: '0 * * * *' # Run every hour
workflow_dispatch:
permissions:
issues: 'write'
contents: 'read'
jobs:
# Event-based: Quick reaction to new/edited issues in THIS repo
labeler:
if: "github.event_name == 'issues'"
runs-on: 'ubuntu-latest'
permissions:
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@v4'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v4'
- name: 'Check for Parent Workstream and Apply Label'
uses: 'actions/github-script@v7'
with:
node-version: '20'
cache: 'npm'
script: |
const issue = context.payload.issue;
const labelToAdd = 'workstream-rollup';
- name: 'Install Dependencies'
run: 'npm ci'
// --- Define the FULL URLs of the allowed parent workstreams ---
const allowedParentUrls = [
'https://api.github.com/repos/google-gemini/gemini-cli/issues/15374',
'https://api.github.com/repos/google-gemini/gemini-cli/issues/15456',
'https://api.github.com/repos/google-gemini/gemini-cli/issues/15324'
];
- name: 'Run Multi-Repo Sync Script'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: 'node .github/scripts/sync-maintainer-labels.cjs'
# Scheduled/Manual: Recursive sync across multiple repos
sync-maintainer-labels:
if: "github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'"
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@v4'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v4'
with:
node-version: '20'
cache: 'npm'
- name: 'Install Dependencies'
run: 'npm ci'
- name: 'Run Multi-Repo Sync Script'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: 'node .github/scripts/sync-maintainer-labels.cjs'
// Check if the issue has a parent_issue_url and if it's in our allowed list.
if (issue && issue.parent_issue_url && allowedParentUrls.includes(issue.parent_issue_url)) {
console.log(`SUCCESS: Issue #${issue.number} is a child of a target workstream (${issue.parent_issue_url}). Adding label.`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [labelToAdd]
});
} else if (issue && issue.parent_issue_url) {
console.log(`FAILURE: Issue #${issue.number} has a parent, but it's not a target workstream. Parent URL: ${issue.parent_issue_url}`);
} else {
console.log(`FAILURE: Issue #${issue.number} is not a child of any issue. No action taken.`);
}
-119
View File
@@ -1,119 +0,0 @@
name: '🏷️ Enforce Restricted Label Permissions'
on:
issues:
types:
- 'labeled'
- 'unlabeled'
jobs:
enforce-label:
# Run this job only when restricted labels are changed
if: |-
${{ (github.event.label.name == 'help wanted' || github.event.label.name == 'status/need-triage' || github.event.label.name == '🔒 maintainer only') &&
(github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli') }}
runs-on: 'ubuntu-latest'
permissions:
issues: 'write'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
env:
APP_ID: '${{ secrets.APP_ID }}'
if: |-
${{ env.APP_ID != '' }}
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
- name: 'Check if user is in the maintainers team'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
const org = context.repo.owner;
const username = context.payload.sender.login;
const team_slug = 'gemini-cli-maintainers';
const action = context.payload.action; // 'labeled' or 'unlabeled'
const labelName = context.payload.label.name;
// Skip if the change was made by a bot to avoid infinite loops
if (username === 'github-actions[bot]' || username === 'gemini-cli[bot]' || username.endsWith('[bot]')) {
core.info('Change made by a bot. Skipping.');
return;
}
try {
// Check repository permission level directly.
// This is more robust than team membership as it doesn't require Org-level read permissions
// and correctly handles Repo Admins/Writers who might not be in the specific team.
const { data: { permission } } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: org,
repo: context.repo.repo,
username,
});
if (permission === 'admin' || permission === 'write') {
core.info(`${username} has '${permission}' permission. Allowed.`);
return;
}
core.info(`${username} has '${permission}' permission (needs 'write' or 'admin'). Reverting '${action}' action for '${labelName}' label.`);
} catch (error) {
core.error(`Failed to check permissions for ${username}: ${error.message}`);
// Fall through to revert logic if we can't verify permissions (fail safe)
}
// If we are here, the user is NOT authorized.
if (true) { // wrapping block to preserve variable scope if needed
if (action === 'labeled') {
// 1. Remove the label if added by a non-maintainer
await github.rest.issues.removeLabel ({
owner: org,
repo: context.repo.repo,
issue_number: context.issue.number,
name: labelName
});
// 2. Post a polite comment
const comment = `
Hi @${username}, thank you for your interest in helping triage issues!
The \`${labelName}\` label is reserved for project maintainers to apply. This helps us ensure that an issue is ready and properly vetted for community contribution.
A maintainer will review this issue soon. Please see our [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md) for more details on our labeling process.
`.trim().replace(/^[ ]+/gm, '');
await github.rest.issues.createComment ({
owner: org,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
} else if (action === 'unlabeled') {
// 1. Add the label back if removed by a non-maintainer
await github.rest.issues.addLabels ({
owner: org,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [labelName]
});
// 2. Post a polite comment
const comment = `
Hi @${username}, it looks like the \`${labelName}\` label was removed.
This label is managed by project maintainers. We've added it back to ensure the issue remains visible to potential contributors until a maintainer decides otherwise.
Thank you for your understanding!
`.trim().replace(/^[ ]+/gm, '');
await github.rest.issues.createComment ({
owner: org,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
}
@@ -1,174 +0,0 @@
name: 'Label Workstream Rollup'
on:
issues:
types: ['opened', 'edited', 'reopened']
schedule:
- cron: '0 * * * *'
workflow_dispatch:
jobs:
labeler:
runs-on: 'ubuntu-latest'
permissions:
issues: 'write'
steps:
- name: 'Check for Parent Workstream and Apply Label'
uses: 'actions/github-script@v7'
with:
script: |
const labelToAdd = 'workstream-rollup';
// Allow-list of parent issue URLs
const allowedParentUrls = [
'https://github.com/google-gemini/gemini-cli/issues/15374',
'https://github.com/google-gemini/gemini-cli/issues/15456',
'https://github.com/google-gemini/gemini-cli/issues/15324',
'https://github.com/google-gemini/gemini-cli/issues/17202',
'https://github.com/google-gemini/gemini-cli/issues/17203'
];
// Single issue processing (for event triggers)
async function processSingleIssue(owner, repo, number) {
const query = `
query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
issue(number:$number) {
number
parent {
url
parent {
url
parent {
url
parent {
url
parent {
url
}
}
}
}
}
}
}
}
`;
try {
const result = await github.graphql(query, { owner, repo, number });
if (!result || !result.repository || !result.repository.issue) {
console.log(`Issue #${number} not found or data missing.`);
return;
}
const issue = result.repository.issue;
await checkAndLabel(issue, owner, repo);
} catch (error) {
console.error(`Failed to process issue #${number}:`, error);
throw error; // Re-throw to be caught by main execution
}
}
// Bulk processing (for schedule/dispatch)
async function processAllOpenIssues(owner, repo) {
const query = `
query($owner:String!, $repo:String!, $cursor:String) {
repository(owner:$owner, name:$repo) {
issues(first: 100, states: OPEN, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
number
parent {
url
parent {
url
parent {
url
parent {
url
parent {
url
}
}
}
}
}
}
}
}
}
`;
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
try {
const result = await github.graphql(query, { owner, repo, cursor });
if (!result || !result.repository || !result.repository.issues) {
console.error('Invalid response structure from GitHub API');
break;
}
const issues = result.repository.issues.nodes || [];
console.log(`Processing batch of ${issues.length} issues...`);
for (const issue of issues) {
await checkAndLabel(issue, owner, repo);
}
hasNextPage = result.repository.issues.pageInfo.hasNextPage;
cursor = result.repository.issues.pageInfo.endCursor;
} catch (error) {
console.error('Failed to fetch issues batch:', error);
throw error; // Re-throw to be caught by main execution
}
}
}
async function checkAndLabel(issue, owner, repo) {
if (!issue || !issue.parent) return;
let currentParent = issue.parent;
let tracedParents = [];
let matched = false;
while (currentParent) {
tracedParents.push(currentParent.url);
if (allowedParentUrls.includes(currentParent.url)) {
console.log(`SUCCESS: Issue #${issue.number} is a descendant of ${currentParent.url}. Trace: ${tracedParents.join(' -> ')}. Adding label.`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issue.number,
labels: [labelToAdd]
});
matched = true;
break;
}
currentParent = currentParent.parent;
}
if (!matched && context.eventName === 'issues') {
console.log(`Issue #${issue.number} did not match any allowed workstreams. Trace: ${tracedParents.join(' -> ') || 'None'}.`);
}
}
// Main execution
try {
if (context.eventName === 'issues') {
console.log(`Processing single issue #${context.payload.issue.number}...`);
await processSingleIssue(context.repo.owner, context.repo.repo, context.payload.issue.number);
} else {
console.log(`Running for event: ${context.eventName}. Processing all open issues...`);
await processAllOpenIssues(context.repo.owner, context.repo.repo);
}
} catch (error) {
core.setFailed(`Workflow failed: ${error.message}`);
}
@@ -1,83 +0,0 @@
name: '🏷️ PR Contribution Guidelines Notifier'
on:
pull_request:
types:
- 'opened'
jobs:
notify-process-change:
runs-on: 'ubuntu-latest'
if: |-
github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli'
permissions:
pull-requests: 'write'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
env:
APP_ID: '${{ secrets.APP_ID }}'
if: |-
${{ env.APP_ID != '' }}
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
- name: 'Check membership and post comment'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
const org = context.repo.owner;
const repo = context.repo.repo;
const username = context.payload.pull_request.user.login;
const pr_number = context.payload.pull_request.number;
// 1. Check if the PR author is a maintainer
const authorAssociation = context.payload.pull_request.author_association;
if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation)) {
core.info(`${username} is a maintainer (Association: ${authorAssociation}). No notification needed.`);
return;
}
// 2. Check if the PR is already associated with an issue
const query = `
query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$number) {
closingIssuesReferences(first: 1) {
totalCount
}
}
}
}
`;
const variables = { owner: org, repo: repo, number: pr_number };
const result = await github.graphql(query, variables);
const issueCount = result.repository.pullRequest.closingIssuesReferences.totalCount;
if (issueCount > 0) {
core.info(`PR #${pr_number} is already associated with an issue. No notification needed.`);
return;
}
// 3. Post the notification comment
core.info(`${username} is not a maintainer and PR #${pr_number} has no linked issue. Posting notification.`);
const comment = `
Hi @${username}, thank you so much for your contribution to Gemini CLI! We really appreciate the time and effort you've put into this.
We're making some updates to our contribution process to improve how we track and review changes. Please take a moment to review our recent discussion post: [Improving Our Contribution Process & Introducing New Guidelines](https://github.com/google-gemini/gemini-cli/discussions/16706).
Key Update: Starting **January 26, 2026**, the Gemini CLI project will require all pull requests to be associated with an existing issue. Any pull requests not linked to an issue by that date will be automatically closed.
Thank you for your understanding and for being a part of our community!
`.trim().replace(/^[ ]+/gm, '');
await github.rest.issues.createComment({
owner: org,
repo: repo,
issue_number: pr_number,
body: comment
});
-1
View File
@@ -55,7 +55,6 @@ gha-creds-*.json
# Log files
patch_output.log
gemini-debug.log
.genkit
.gemini-clipboard/
-1
View File
@@ -20,4 +20,3 @@ junit.xml
.gemini-linters/
Thumbs.db
.pytest_cache
**/SKILL.md
+4 -12
View File
@@ -42,13 +42,8 @@ This project follows
The process for contributing code is as follows:
1. **Find an issue** that you want to work on. If an issue is tagged as
`🔒Maintainers only`, this means it is reserved for project maintainers. We
will not accept pull requests related to these issues. In the near future,
we will explicitly mark issues looking for contributions using the
`help wanted` label. If you believe an issue is a good candidate for
community contribution, please leave a comment on the issue. A maintainer
will review it and apply the `help-wanted` label if appropriate. Only
maintainers should attempt to add the `help-wanted` label to an issue.
"🔒Maintainers only", this means it is reserved for project maintainers. We
will not accept pull requests related to these issues.
2. **Fork the repository** and create a new branch.
3. **Make your changes** in the `packages/` directory.
4. **Ensure all checks pass** by running `npm run preflight`.
@@ -99,11 +94,8 @@ any code is written.
- **For features:** The PR should be linked to the feature request or proposal
issue that has been approved by a maintainer.
If an issue for your change doesn't exist, we will automatically close your PR
along with a comment reminding you to associate the PR with an issue. The ideal
workflow starts with an issue that has been reviewed and approved by a
maintainer. Please **open the issue first** and wait for feedback before you
start coding.
If an issue for your change doesn't exist, please **open one first** and wait
for feedback before you start coding.
#### 2. Keep it small and focused
+398 -59
View File
@@ -1,70 +1,409 @@
# Gemini CLI Project Context
## Building and running
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
into the terminal. It is designed to be a terminal-first, extensible, and
powerful tool for developers.
Before submitting any changes, it is crucial to validate them by running the
full preflight check. This command will build the repository, run all tests,
check for type errors, and lint the code.
## Project Overview
To run the full suite of checks, execute the following command:
- **Purpose:** Provide a seamless terminal interface for Gemini models,
supporting code understanding, generation, automation, and integration via MCP
(Model Context Protocol).
- **Main Technologies:**
- **Runtime:** Node.js (>=20.0.0, recommended ~20.19.0 for development)
- **Language:** TypeScript
- **UI Framework:** React (using [Ink](https://github.com/vadimdemedes/ink)
for CLI rendering)
- **Testing:** Vitest
- **Bundling:** esbuild
- **Linting/Formatting:** ESLint, Prettier
- **Architecture:** Monorepo structure using npm workspaces.
- `packages/cli`: User-facing terminal UI, input processing, and display
rendering.
- `packages/core`: Backend logic, Gemini API orchestration, prompt
construction, and tool execution.
- `packages/core/src/tools/`: Built-in tools for file system, shell, and web
operations.
- `packages/a2a-server`: Experimental Agent-to-Agent server.
- `packages/vscode-ide-companion`: VS Code extension pairing with the CLI.
```bash
npm run preflight
```
## Building and Running
This single command ensures that your changes meet all the quality gates of the
project. While you can run the individual steps (`build`, `test`, `typecheck`,
`lint`) separately, it is highly recommended to use `npm run preflight` to
ensure a comprehensive validation.
- **Install Dependencies:** `npm install`
- **Build All:** `npm run build:all` (Builds packages, sandbox, and VS Code
companion)
- **Build Packages:** `npm run build`
- **Run in Development:** `npm run start`
- **Run in Debug Mode:** `npm run debug` (Enables Node.js inspector)
- **Bundle Project:** `npm run bundle`
- **Clean Artifacts:** `npm run clean`
## Writing Tests
## Testing and Quality
This project uses **Vitest** as its primary testing framework. When writing
tests, aim to follow existing patterns. Key conventions include:
- **Test Commands:**
- **Unit (All):** `npm run test`
- **Integration (E2E):** `npm run test:e2e`
- **Workspace-Specific:** `npm test -w <pkg> -- <path>` (Note: `<path>` must
be relative to the workspace root, e.g.,
`-w @google/gemini-cli-core -- src/routing/modelRouterService.test.ts`)
- **Full Validation:** `npm run preflight` (Heaviest check; runs clean, install,
build, lint, type check, and tests. Recommended before submitting PRs.)
- **Individual Checks:** `npm run lint` / `npm run format` / `npm run typecheck`
### Test Structure and Framework
## Development Conventions
- **Framework**: All tests are written using Vitest (`describe`, `it`, `expect`,
`vi`).
- **File Location**: Test files (`*.test.ts` for logic, `*.test.tsx` for React
components) are co-located with the source files they test.
- **Configuration**: Test environments are defined in `vitest.config.ts` files.
- **Setup/Teardown**: Use `beforeEach` and `afterEach`. Commonly,
`vi.resetAllMocks()` is called in `beforeEach` and `vi.restoreAllMocks()` in
`afterEach`.
- **Contributions:** Follow the process outlined in `CONTRIBUTING.md`. Requires
signing the Google CLA.
- **Pull Requests:** Keep PRs small, focused, and linked to an existing issue.
- **Commit Messages:** Follow the
[Conventional Commits](https://www.conventionalcommits.org/) standard.
- **Coding Style:** Adhere to existing patterns in `packages/cli` (React/Ink)
and `packages/core` (Backend logic).
- **Imports:** Use specific imports and avoid restricted relative imports
between packages (enforced by ESLint).
### Mocking (`vi` from Vitest)
## Documentation
- **ES Modules**: Mock with
`vi.mock('module-name', async (importOriginal) => { ... })`. Use
`importOriginal` for selective mocking.
- _Example_:
`vi.mock('os', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, homedir: vi.fn() }; });`
- **Mocking Order**: For critical dependencies (e.g., `os`, `fs`) that affect
module-level constants, place `vi.mock` at the _very top_ of the test file,
before other imports.
- **Hoisting**: Use `const myMock = vi.hoisted(() => vi.fn());` if a mock
function needs to be defined before its use in a `vi.mock` factory.
- **Mock Functions**: Create with `vi.fn()`. Define behavior with
`mockImplementation()`, `mockResolvedValue()`, or `mockRejectedValue()`.
- **Spying**: Use `vi.spyOn(object, 'methodName')`. Restore spies with
`mockRestore()` in `afterEach`.
- Suggest documentation updates when code changes render existing documentation
obsolete or incomplete.
- Located in the `docs/` directory.
- Use the `docs-writer` skill.
### Commonly Mocked Modules
- **Node.js built-ins**: `fs`, `fs/promises`, `os` (especially `os.homedir()`),
`path`, `child_process` (`execSync`, `spawn`).
- **External SDKs**: `@google/genai`, `@modelcontextprotocol/sdk`.
- **Internal Project Modules**: Dependencies from other project packages are
often mocked.
### React Component Testing (CLI UI - Ink)
- Use `render()` from `ink-testing-library`.
- Assert output with `lastFrame()`.
- Wrap components in necessary `Context.Provider`s.
- Mock custom React hooks and complex child components using `vi.mock()`.
### Asynchronous Testing
- Use `async/await`.
- For timers, use `vi.useFakeTimers()`, `vi.advanceTimersByTimeAsync()`,
`vi.runAllTimersAsync()`.
- Test promise rejections with `await expect(promise).rejects.toThrow(...)`.
### General Guidance
- When adding tests, first examine existing tests to understand and conform to
established conventions.
- Pay close attention to the mocks at the top of existing test files; they
reveal critical dependencies and how they are managed in a test environment.
## Git Repo
The main branch for this project is called "main"
## JavaScript/TypeScript
When contributing to this React, Node, and TypeScript codebase, please
prioritize the use of plain JavaScript objects with accompanying TypeScript
interface or type declarations over JavaScript class syntax. This approach
offers significant advantages, especially concerning interoperability with React
and overall code maintainability.
### Preferring Plain Objects over Classes
JavaScript classes, by their nature, are designed to encapsulate internal state
and behavior. While this can be useful in some object-oriented paradigms, it
often introduces unnecessary complexity and friction when working with React's
component-based architecture. Here's why plain objects are preferred:
- Seamless React Integration: React components thrive on explicit props and
state management. Classes' tendency to store internal state directly within
instances can make prop and state propagation harder to reason about and
maintain. Plain objects, on the other hand, are inherently immutable (when
used thoughtfully) and can be easily passed as props, simplifying data flow
and reducing unexpected side effects.
- Reduced Boilerplate and Increased Conciseness: Classes often promote the use
of constructors, this binding, getters, setters, and other boilerplate that
can unnecessarily bloat code. TypeScript interface and type declarations
provide powerful static type checking without the runtime overhead or
verbosity of class definitions. This allows for more succinct and readable
code, aligning with JavaScript's strengths in functional programming.
- Enhanced Readability and Predictability: Plain objects, especially when their
structure is clearly defined by TypeScript interfaces, are often easier to
read and understand. Their properties are directly accessible, and there's no
hidden internal state or complex inheritance chains to navigate. This
predictability leads to fewer bugs and a more maintainable codebase.
- Simplified Immutability: While not strictly enforced, plain objects encourage
an immutable approach to data. When you need to modify an object, you
typically create a new one with the desired changes, rather than mutating the
original. This pattern aligns perfectly with React's reconciliation process
and helps prevent subtle bugs related to shared mutable state.
- Better Serialization and Deserialization: Plain JavaScript objects are
naturally easy to serialize to JSON and deserialize back, which is a common
requirement in web development (e.g., for API communication or local storage).
Classes, with their methods and prototypes, can complicate this process.
### Embracing ES Module Syntax for Encapsulation
Rather than relying on Java-esque private or public class members, which can be
verbose and sometimes limit flexibility, we strongly prefer leveraging ES module
syntax (`import`/`export`) for encapsulating private and public APIs.
- Clearer Public API Definition: With ES modules, anything that is exported is
part of the public API of that module, while anything not exported is
inherently private to that module. This provides a very clear and explicit way
to define what parts of your code are meant to be consumed by other modules.
- Enhanced Testability (Without Exposing Internals): By default, unexported
functions or variables are not accessible from outside the module. This
encourages you to test the public API of your modules, rather than their
internal implementation details. If you find yourself needing to spy on or
stub an unexported function for testing purposes, it's often a "code smell"
indicating that the function might be a good candidate for extraction into its
own separate, testable module with a well-defined public API. This promotes a
more robust and maintainable testing strategy.
- Reduced Coupling: Explicitly defined module boundaries through import/export
help reduce coupling between different parts of your codebase. This makes it
easier to refactor, debug, and understand individual components in isolation.
### Avoiding `any` Types and Type Assertions; Preferring `unknown`
TypeScript's power lies in its ability to provide static type checking, catching
potential errors before your code runs. To fully leverage this, it's crucial to
avoid the `any` type and be judicious with type assertions.
- **The Dangers of `any`**: Using any effectively opts out of TypeScript's type
checking for that particular variable or expression. While it might seem
convenient in the short term, it introduces significant risks:
- **Loss of Type Safety**: You lose all the benefits of type checking, making
it easy to introduce runtime errors that TypeScript would otherwise have
caught.
- **Reduced Readability and Maintainability**: Code with `any` types is harder
to understand and maintain, as the expected type of data is no longer
explicitly defined.
- **Masking Underlying Issues**: Often, the need for any indicates a deeper
problem in the design of your code or the way you're interacting with
external libraries. It's a sign that you might need to refine your types or
refactor your code.
- **Preferring `unknown` over `any`**: When you absolutely cannot determine the
type of a value at compile time, and you're tempted to reach for any, consider
using unknown instead. unknown is a type-safe counterpart to any. While a
variable of type unknown can hold any value, you must perform type narrowing
(e.g., using typeof or instanceof checks, or a type assertion) before you can
perform any operations on it. This forces you to handle the unknown type
explicitly, preventing accidental runtime errors.
```ts
function processValue(value: unknown) {
if (typeof value === 'string') {
// value is now safely a string
console.log(value.toUpperCase());
} else if (typeof value === 'number') {
// value is now safely a number
console.log(value * 2);
}
// Without narrowing, you cannot access properties or methods on 'value'
// console.log(value.someProperty); // Error: Object is of type 'unknown'.
}
```
- **Type Assertions (`as Type`) - Use with Caution**: Type assertions tell the
TypeScript compiler, "Trust me, I know what I'm doing; this is definitely of
this type." While there are legitimate use cases (e.g., when dealing with
external libraries that don't have perfect type definitions, or when you have
more information than the compiler), they should be used sparingly and with
extreme caution.
- **Bypassing Type Checking**: Like `any`, type assertions bypass TypeScript's
safety checks. If your assertion is incorrect, you introduce a runtime error
that TypeScript would not have warned you about.
- **Code Smell in Testing**: A common scenario where `any` or type assertions
might be tempting is when trying to test "private" implementation details
(e.g., spying on or stubbing an unexported function within a module). This
is a strong indication of a "code smell" in your testing strategy and
potentially your code structure. Instead of trying to force access to
private internals, consider whether those internal details should be
refactored into a separate module with a well-defined public API. This makes
them inherently testable without compromising encapsulation.
### Type narrowing `switch` clauses
Use the `checkExhaustive` helper in the default clause of a switch statement.
This will ensure that all of the possible options within the value or
enumeration are used.
This helper method can be found in `packages/cli/src/utils/checks.ts`
### Embracing JavaScript's Array Operators
To further enhance code cleanliness and promote safe functional programming
practices, leverage JavaScript's rich set of array operators as much as
possible. Methods like `.map()`, `.filter()`, `.reduce()`, `.slice()`,
`.sort()`, and others are incredibly powerful for transforming and manipulating
data collections in an immutable and declarative way.
Using these operators:
- Promotes Immutability: Most array operators return new arrays, leaving the
original array untouched. This functional approach helps prevent unintended
side effects and makes your code more predictable.
- Improves Readability: Chaining array operators often lead to more concise and
expressive code than traditional for loops or imperative logic. The intent of
the operation is clear at a glance.
- Facilitates Functional Programming: These operators are cornerstones of
functional programming, encouraging the creation of pure functions that take
inputs and produce outputs without causing side effects. This paradigm is
highly beneficial for writing robust and testable code that pairs well with
React.
By consistently applying these principles, we can maintain a codebase that is
not only efficient and performant but also a joy to work with, both now and in
the future.
## React (mirrored and adjusted from [react-mcp-server](https://github.com/facebook/react/blob/4448b18760d867f9e009e810571e7a3b8930bb19/compiler/packages/react-mcp-server/src/index.ts#L376C1-L441C94))
### Role
You are a React assistant that helps users write more efficient and optimizable
React code. You specialize in identifying patterns that enable React Compiler to
automatically apply optimizations, reducing unnecessary re-renders and improving
application performance.
### Follow these guidelines in all code you produce and suggest
Use functional components with Hooks: Do not generate class components or use
old lifecycle methods. Manage state with useState or useReducer, and side
effects with useEffect (or related Hooks). Always prefer functions and Hooks for
any new component logic.
Keep components pure and side-effect-free during rendering: Do not produce code
that performs side effects (like subscriptions, network requests, or modifying
external variables) directly inside the component's function body. Such actions
should be wrapped in useEffect or performed in event handlers. Ensure your
render logic is a pure function of props and state.
Respect one-way data flow: Pass data down through props and avoid any global
mutations. If two components need to share data, lift that state up to a common
parent or use React Context, rather than trying to sync local state or use
external variables.
Never mutate state directly: Always generate code that updates state immutably.
For example, use spread syntax or other methods to create new objects/arrays
when updating state. Do not use assignments like state.someValue = ... or array
mutations like array.push() on state variables. Use the state setter (setState
from useState, etc.) to update state.
Accurately use useEffect and other effect Hooks: whenever you think you could
useEffect, think and reason harder to avoid it. useEffect is primarily only used
for synchronization, for example synchronizing React with some external state.
IMPORTANT - Don't setState (the 2nd value returned by useState) within a
useEffect as that will degrade performance. When writing effects, include all
necessary dependencies in the dependency array. Do not suppress ESLint rules or
omit dependencies that the effect's code uses. Structure the effect callbacks to
handle changing values properly (e.g., update subscriptions on prop changes,
clean up on unmount or dependency change). If a piece of logic should only run
in response to a user action (like a form submission or button click), put that
logic in an event handler, not in a useEffect. Where possible, useEffects should
return a cleanup function.
Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect,
useContext, custom Hooks, etc.) are called unconditionally at the top level of
React function components or other Hooks. Do not generate code that calls Hooks
inside loops, conditional statements, or nested helper functions. Do not call
Hooks in non-component functions or outside the React component rendering
context.
Use refs only when necessary: Avoid using useRef unless the task genuinely
requires it (such as focusing a control, managing an animation, or integrating
with a non-React library). Do not use refs to store application state that
should be reactive. If you do use refs, never write to or read from ref.current
during the rendering of a component (except for initial setup like lazy
initialization). Any ref usage should not affect the rendered output directly.
Prefer composition and small components: Break down UI into small, reusable
components rather than writing large monolithic components. The code you
generate should promote clarity and reusability by composing components
together. Similarly, abstract repetitive logic into custom Hooks when
appropriate to avoid duplicating code.
Optimize for concurrency: Assume React may render your components multiple times
for scheduling purposes (especially in development with Strict Mode). Write code
that remains correct even if the component function runs more than once. For
instance, avoid side effects in the component body and use functional state
updates (e.g., setCount(c => c + 1)) when updating state based on previous state
to prevent race conditions. Always include cleanup functions in effects that
subscribe to external resources. Don't write useEffects for "do this when this
changes" side effects. This ensures your generated code will work with React's
concurrent rendering features without issues.
Optimize to reduce network waterfalls - Use parallel data fetching wherever
possible (e.g., start multiple requests at once rather than one after another).
Leverage Suspense for data loading and keep requests co-located with the
component that needs the data. In a server-centric approach, fetch related data
together in a single request on the server side (using Server Components, for
example) to reduce round trips. Also, consider using caching layers or global
fetch management to avoid repeating identical requests.
Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if
React Compiler is enabled. Avoid premature optimization with manual memoization.
Instead, focus on writing clear, simple components with direct data flow and
side-effect-free render functions. Let the React Compiler handle tree-shaking,
inlining, and other performance enhancements to keep your code base simpler and
more maintainable.
Design for a good user experience - Provide clear, minimal, and non-blocking UI
states. When data is loading, show lightweight placeholders (e.g., skeleton
screens) rather than intrusive spinners everywhere. Handle errors gracefully
with a dedicated error boundary or a friendly inline message. Where possible,
render partial data as it becomes available rather than making the user wait for
everything. Suspense allows you to declare the loading states in your component
tree in a natural way, preventing “flash” states and improving perceived
performance.
### Process
1. Analyze the user's code for optimization opportunities:
- Check for React anti-patterns that prevent compiler optimization
- Look for component structure issues that limit compiler effectiveness
- Think about each suggestion you are making and consult React docs for best
practices
2. Provide actionable guidance:
- Explain specific code changes with clear reasoning
- Show before/after examples when suggesting changes
- Only suggest changes that meaningfully improve optimization potential
### Optimization Guidelines
- State updates should be structured to enable granular updates
- Side effects should be isolated and dependencies clearly defined
## Documentation guidelines
When working in the `/docs` directory, follow the guidelines in this section:
- **Role:** You are an expert technical writer and AI assistant for contributors
to Gemini CLI. Produce professional, accurate, and consistent documentation to
guide users of Gemini CLI.
- **Technical Accuracy:** Do not invent facts, commands, code, API names, or
output. All technical information specific to Gemini CLI must be based on code
found within this directory and its subdirectories.
- **Style Authority:** Your source for writing guidance and style is the
"Documentation contribution process" section in the root directory's
`CONTRIBUTING.md` file, as well as any guidelines provided this section.
- **Information Architecture Consideration:** Before proposing documentation
changes, consider the information architecture. If a change adds significant
new content to existing documents, evaluate if creating a new, more focused
page or changes to `sidebar.json` would provide a better user experience.
- **Proactive User Consideration:** The user experience should be a primary
concern when making changes to documentation. Aim to fill gaps in existing
knowledge whenever possible while keeping documentation concise and easy for
users to understand. If changes might hinder user understanding or
accessibility, proactively raise these concerns and propose alternatives.
## Comments policy
Only write high-value comments if at all. Avoid talking to the user through
comments.
## Logging and Error Handling
- **Avoid Console Statements:** Do not use `console.log`, `console.error`, or
similar methods directly.
- **Non-User-Facing Logs:** For developer-facing debug messages, use
`debugLogger` (from `@google/gemini-cli-core`).
- **User-Facing Feedback:** To surface errors or warnings to the user, use
`coreEvents.emitFeedback` (from `@google/gemini-cli-core`).
## General requirements
- If there is something you do not understand or is ambiguous, seek confirmation
or clarification from the user before making changes based on assumptions.
- Use hyphens instead of underscores in flag names (e.g. `my-flag` instead of
`my_flag`).
- Always refer to Gemini CLI as `Gemini CLI`, never `the Gemini CLI`.
+3 -4
View File
@@ -18,8 +18,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
- **🎯 Free tier**: 60 requests/min and 1,000 requests/day with personal Google
account.
- **🧠 Powerful Gemini 3 models**: Access to improved reasoning and 1M token
context window.
- **🧠 Powerful Gemini 2.5 Pro**: Access to 1M token context window.
- **🔧 Built-in tools**: Google Search grounding, file operations, shell
commands, web fetching.
- **🔌 Extensible**: MCP (Model Context Protocol) support for custom
@@ -141,7 +140,7 @@ for details)
**Benefits:**
- **Free tier**: 60 requests/min and 1,000 requests/day
- **Gemini 3 models** with 1M token context window
- **Gemini 2.5 Pro** with 1M token context window
- **No API key management** - just sign in with your Google account
- **Automatic updates** to latest models
@@ -165,7 +164,7 @@ gemini
**Benefits:**
- **Free tier**: 1000 requests/day with Gemini 3 (mix of flash and pro)
- **Free tier**: 100 requests/day with Gemini 2.5 Pro
- **Model selection**: Choose specific Gemini models
- **Usage-based billing**: Upgrade for higher limits when needed
+3 -3
View File
@@ -13,11 +13,11 @@ input:
as handling the initial user input, presenting the final output, and
managing the overall user experience.
- **Key functions contained in the package:**
- [Input processing](/docs/cli/commands)
- [Input processing](/docs/cli/commands.md)
- History management
- Display rendering
- [Theme and UI customization](/docs/cli/themes)
- [CLI configuration settings](/docs/get-started/configuration)
- [Theme and UI customization](/docs/cli/themes.md)
- [CLI configuration settings](/docs/get-started/configuration.md)
2. **Core package (`packages/core`):**
- **Purpose:** This acts as the backend for the Gemini CLI. It receives
-112
View File
@@ -18,118 +18,6 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.25.0 - 2026-01-20
- **Skills and Agents Improvements:** We've enhanced the `activate_skill` tool,
added a new `pr-creator` skill
([#16232](https://github.com/google-gemini/gemini-cli/pull/16232) by
[@NTaylorMullen](https://github.com/NTaylorMullen)), enabled skills by
default, improved the `cli_help` agent
([#16100](https://github.com/google-gemini/gemini-cli/pull/16100) by
[@scidomino](https://github.com/scidomino)), and added a new `/agents refresh`
command ([#16204](https://github.com/google-gemini/gemini-cli/pull/16204) by
[@joshualitt](https://github.com/joshualitt)).
- **UI/UX Refinements:** You'll notice more transparent feedback for skills
([#15954](https://github.com/google-gemini/gemini-cli/pull/15954) by
[@NTaylorMullen](https://github.com/NTaylorMullen)), the ability to switch
focus between the shell and input with Tab
([#14332](https://github.com/google-gemini/gemini-cli/pull/14332) by
[@jacob314](https://github.com/jacob314)), and dynamic terminal tab titles
([#16378](https://github.com/google-gemini/gemini-cli/pull/16378) by
[@NTaylorMullen](https://github.com/NTaylorMullen)).
- **Core Functionality & Performance:** This release includes support for
built-in agent skills
([#16045](https://github.com/google-gemini/gemini-cli/pull/16045) by
[@NTaylorMullen](https://github.com/NTaylorMullen)), refined Gemini 3 system
instructions ([#16139](https://github.com/google-gemini/gemini-cli/pull/16139)
by [@NTaylorMullen](https://github.com/NTaylorMullen)), caching for ignore
instances to improve performance
([#16185](https://github.com/google-gemini/gemini-cli/pull/16185) by
[@EricRahm](https://github.com/EricRahm)), and enhanced retry mechanisms
([#16489](https://github.com/google-gemini/gemini-cli/pull/16489) by
[@sehoon38](https://github.com/sehoon38)).
- **Bug Fixes and Stability:** We've squashed numerous bugs across the CLI,
core, and workflows, addressing issues with subagent delegation, unicode
character crashes, and sticky header regressions.
## Announcements: v0.24.0 - 2026-01-14
- **Agent Skills:** We've introduced significant advancements in Agent Skills.
This includes initial documentation and tutorials to help you get started,
alongside enhanced support for remote agents, allowing for more distributed
and powerful automation within Gemini CLI.
([#15869](https://github.com/google-gemini/gemini-cli/pull/15869) by
[@NTaylorMullen](https://github.com/NTaylorMullen)),
([#16013](https://github.com/google-gemini/gemini-cli/pull/16013) by
[@adamweidman](https://github.com/adamweidman))
- **Improved UI/UX:** The user interface has received several updates, featuring
visual indicators for hook execution, a more refined display for settings, and
the ability to use the Tab key to effortlessly switch focus between the shell
and input areas.
([#15408](https://github.com/google-gemini/gemini-cli/pull/15408) by
[@abhipatel12](https://github.com/abhipatel12)),
([#14332](https://github.com/google-gemini/gemini-cli/pull/14332) by
[@galz10](https://github.com/galz10))
- **Enhanced Security:** Security has been a major focus, with default folder
trust now set to untrusted for increased safety. The Policy Engine has been
improved to allow specific modes in user and administrator policies, and
granular allowlisting for shell commands has been implemented, providing finer
control over tool execution.
([#15943](https://github.com/google-gemini/gemini-cli/pull/15943) by
[@galz10](https://github.com/galz10)),
([#15977](https://github.com/google-gemini/gemini-cli/pull/15977) by
[@NTaylorMullen](https://github.com/NTaylorMullen))
- **Core Functionality:** This release includes a mandatory MessageBus
injection, marking Phase 3 of a hard migration to a more robust internal
communication system. We've also added support for built-in skills with the
CLI itself, and enhanced model routing to effectively utilize subagents.
([#15776](https://github.com/google-gemini/gemini-cli/pull/15776) by
[@abhipatel12](https://github.com/abhipatel12)),
([#16300](https://github.com/google-gemini/gemini-cli/pull/16300) by
[@NTaylorMullen](https://github.com/NTaylorMullen))
- **Terminal Features:** Terminal interactions are more seamless with new
features like OSC 52 paste support, along with fixes for Windows clipboard
paste issues and general improvements to pasting in Windows terminals.
([#15336](https://github.com/google-gemini/gemini-cli/pull/15336) by
[@scidomino](https://github.com/scidomino)),
([#15932](https://github.com/google-gemini/gemini-cli/pull/15932) by
[@scidomino](https://github.com/scidomino))
- **New Commands:** To manage the new features, we've added several new
commands: `/agents refresh` to update agent configurations, `/skills reload`
to refresh skill definitions, and `/skills install/uninstall` for easier
management of your Agent Skills.
([#16204](https://github.com/google-gemini/gemini-cli/pull/16204) by
[@NTaylorMullen](https://github.com/NTaylorMullen)),
([#15865](https://github.com/google-gemini/gemini-cli/pull/15865) by
[@NTaylorMullen](https://github.com/NTaylorMullen)),
([#16377](https://github.com/google-gemini/gemini-cli/pull/16377) by
[@NTaylorMullen](https://github.com/NTaylorMullen))
## Announcements: v0.23.0 - 2026-01-07
- 🎉 **Experimental Agent Skills Support in Preview:** Gemini CLI now supports
[Agent Skills](https://agentskills.io/home) in our preview builds. This is an
early preview where were looking for feedback!
- Install Preview: `npm install -g @google/gemini-cli@preview`
- Enable in `/settings`
- Docs:
[https://geminicli.com/docs/cli/skills/](https://geminicli.com/docs/cli/skills/)
- **Gemini CLI wrapped:** Run `npx gemini-wrapped` to visualize your usage
stats, top models, languages, and more!
- **Windows clipboard image support:** Windows users can now paste images
directly from their clipboard into the CLI using `Alt`+`V`.
([pr](https://github.com/google-gemini/gemini-cli/pull/13997) by
[@sgeraldes](https://github.com/sgeraldes))
- **Terminal background color detection:** Automatically optimizes your
terminal's background color to select compatible themes and provide
accessibility warnings.
([pr](https://github.com/google-gemini/gemini-cli/pull/15132) by
[@jacob314](https://github.com/jacob314))
- **Session logout:** Use the new `/logout` command to instantly clear
credentials and reset your authentication state for seamless account
switching. ([pr](https://github.com/google-gemini/gemini-cli/pull/13383) by
[@CN-Scars](https://github.com/CN-Scars))
## Announcements: v0.22.0 - 2025-12-22
- 🎉**Free Tier + Gemini 3:** Free tier users now all have access to Gemini 3
+138 -355
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.25.0
# Latest stable release: v0.22.0 - v0.22.5
Released: January 20, 2026
Released: December 30, 2025
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,360 +11,143 @@ npm install -g @google/gemini-cli
## Highlights
- **Skills and Agents Improvements:** Enhanced `activate_skill` tool, new
`pr-creator` skill, default enablement of skills, improved `cli_help` agent,
and a new `/agents refresh` command.
- **UI/UX Refinements:** Transparent feedback for skills, ability to switch
focus between shell and input with Tab, and dynamic terminal tab titles.
- **Core Functionality & Performance:** Support for built-in agent skills,
refined Gemini 3 system instructions, caching ignore instances for
performance, and improved retry mechanisms.
- **Bug Fixes and Stability:** Numerous bug fixes across the CLI, core, and
workflows, including issues with subagent delegation, unicode character
crashes, and sticky header regressions.
- **Comprehensive quota visibility:** View usage statistics for all available
models in the `/stats` command, even those not yet used in your current
session. ([pic](https://imgur.com/a/cKyDtYh),
[pr](https://github.com/google-gemini/gemini-cli/pull/14764) by
[@sehoon38](https://github.com/sehoon38))
- **Polished CLI statistics:** Weve cleaned up the `/stats` view to prioritize
actionable quota information while providing a detailed token and
cache-efficiency breakdown in `/stats model`
([login with Google](https://imgur.com/a/w9xKthm),
[api key](https://imgur.com/a/FjQPHOY),
[model stats](https://imgur.com/a/VfWzVgw),
[pr](https://github.com/google-gemini/gemini-cli/pull/14961) by
[@jacob314](https://github.com/jacob314))
- **Multi-file drag & drop:** Multi-file drag & drop is now supported and
properly translated to be prefixed with `@`.
([pr](https://github.com/google-gemini/gemini-cli/pull/14832) by
[@jackwotherspoon](https://github.com/jackwotherspoon))
## What's Changed
- feat(core): improve activate_skill tool and use lowercase XML tags by
@NTaylorMullen in
[#16009](https://github.com/google-gemini/gemini-cli/pull/16009)
- Add initiation method telemetry property by @gundermanc in
[#15818](https://github.com/google-gemini/gemini-cli/pull/15818)
- chore(release): bump version to 0.25.0-nightly.20260107.59a18e710 by
@gemini-cli-robot in
[#16048](https://github.com/google-gemini/gemini-cli/pull/16048)
- Hx support by @kevinfjiang in
[#16032](https://github.com/google-gemini/gemini-cli/pull/16032)
- [Skills] Foundation: Centralize management logic and feedback rendering by
@NTaylorMullen in
[#15952](https://github.com/google-gemini/gemini-cli/pull/15952)
- Introduce GEMINI_CLI_HOME for strict test isolation by @NTaylorMullen in
[#15907](https://github.com/google-gemini/gemini-cli/pull/15907)
- [Skills] Multi-scope skill enablement and shadowing fix by @NTaylorMullen in
[#15953](https://github.com/google-gemini/gemini-cli/pull/15953)
- policy: extract legacy policy from core tool scheduler to policy engine by
@abhipatel12 in
[#15902](https://github.com/google-gemini/gemini-cli/pull/15902)
- Enhance TestRig with process management and timeouts by @NTaylorMullen in
[#15908](https://github.com/google-gemini/gemini-cli/pull/15908)
- Update troubleshooting doc for UNABLE_TO_GET_ISSUER_CERT_LOCALLY by @sehoon38
in [#16069](https://github.com/google-gemini/gemini-cli/pull/16069)
- Add keytar to dependencies by @chrstnb in
[#15928](https://github.com/google-gemini/gemini-cli/pull/15928)
- Simplify extension settings command by @chrstnb in
[#16001](https://github.com/google-gemini/gemini-cli/pull/16001)
- feat(admin): implement extensions disabled by @skeshive in
[#16024](https://github.com/google-gemini/gemini-cli/pull/16024)
- Core data structure updates for Rewind functionality by @Adib234 in
[#15714](https://github.com/google-gemini/gemini-cli/pull/15714)
- feat(hooks): simplify hook firing with HookSystem wrapper methods by @ved015
in [#15982](https://github.com/google-gemini/gemini-cli/pull/15982)
- Add exp.gws_experiment field to LogEventEntry by @gsquared94 in
[#16062](https://github.com/google-gemini/gemini-cli/pull/16062)
- Revert "feat(admin): implement extensions disabled" by @chrstnb in
[#16082](https://github.com/google-gemini/gemini-cli/pull/16082)
- feat(core): Decouple enabling hooks UI from subsystem. by @joshualitt in
[#16074](https://github.com/google-gemini/gemini-cli/pull/16074)
- docs: add docs for hooks + extensions by @abhipatel12 in
[#16073](https://github.com/google-gemini/gemini-cli/pull/16073)
- feat(core): Preliminary changes for subagent model routing. by @joshualitt in
[#16035](https://github.com/google-gemini/gemini-cli/pull/16035)
- Optimize CI workflow: Parallelize jobs and cache linters by @NTaylorMullen in
[#16054](https://github.com/google-gemini/gemini-cli/pull/16054)
- Add option to fallback for capacity errors in ProQuotaDi… by @sehoon38 in
[#16050](https://github.com/google-gemini/gemini-cli/pull/16050)
- feat: add confirmation details support + jsonrpc vs http rest support by
@adamfweidman in
[#16079](https://github.com/google-gemini/gemini-cli/pull/16079)
- fix(workflows): fix and limit labels for pr-triage.sh script by @jacob314 in
[#16096](https://github.com/google-gemini/gemini-cli/pull/16096)
- Fix and rename introspection agent -> cli help agent by @scidomino in
[#16097](https://github.com/google-gemini/gemini-cli/pull/16097)
- Docs: Changelogs update 20260105 by @jkcinouye in
[#15937](https://github.com/google-gemini/gemini-cli/pull/15937)
- enable cli_help agent by default by @scidomino in
[#16100](https://github.com/google-gemini/gemini-cli/pull/16100)
- Optimize json-output tests with mock responses by @NTaylorMullen in
[#16102](https://github.com/google-gemini/gemini-cli/pull/16102)
- Fix CI for forks by @scidomino in
[#16113](https://github.com/google-gemini/gemini-cli/pull/16113)
- Reduce nags about PRs that reference issues but don't fix them. by @jacob314
in [#16112](https://github.com/google-gemini/gemini-cli/pull/16112)
- feat(cli): add filepath autosuggestion after slash commands by @jasmeetsb in
[#14738](https://github.com/google-gemini/gemini-cli/pull/14738)
- Add upgrade option for paid users by @cayden-google in
[#15978](https://github.com/google-gemini/gemini-cli/pull/15978)
- [Skills] UX Polishing: Transparent feedback and CLI refinements by
@NTaylorMullen in
[#15954](https://github.com/google-gemini/gemini-cli/pull/15954)
- Polish: Move 'Failed to load skills' warning to debug logs by @NTaylorMullen
in [#16142](https://github.com/google-gemini/gemini-cli/pull/16142)
- feat(cli): export chat history in /bug and prefill GitHub issue by
@NTaylorMullen in
[#16115](https://github.com/google-gemini/gemini-cli/pull/16115)
- bug(core): fix issue with overrides to bases. by @joshualitt in
[#15255](https://github.com/google-gemini/gemini-cli/pull/15255)
- enableInteractiveShell for external tooling relying on a2a server by
@DavidAPierce in
[#16080](https://github.com/google-gemini/gemini-cli/pull/16080)
- Reapply "feat(admin): implement extensions disabled" (#16082) by @skeshive in
[#16109](https://github.com/google-gemini/gemini-cli/pull/16109)
- bug(core): Fix spewie getter in hookTranslator.ts by @joshualitt in
[#16108](https://github.com/google-gemini/gemini-cli/pull/16108)
- feat(hooks): add mcp_context to BeforeTool and AfterTool hook inputs by @vrv
in [#15656](https://github.com/google-gemini/gemini-cli/pull/15656)
- Add extension linking capabilities in cli by @kevinjwang1 in
[#16040](https://github.com/google-gemini/gemini-cli/pull/16040)
- Update the page's title to be consistent and show in site. by @kschaab in
[#16174](https://github.com/google-gemini/gemini-cli/pull/16174)
- docs: correct typo in bufferFastReturn JSDoc ("accomodate" → "accommodate") by
@minglu7 in [#16056](https://github.com/google-gemini/gemini-cli/pull/16056)
- fix: typo in MCP servers settings description by @alphanota in
[#15929](https://github.com/google-gemini/gemini-cli/pull/15929)
- fix: yolo should auto allow redirection by @abhipatel12 in
[#16183](https://github.com/google-gemini/gemini-cli/pull/16183)
- fix(cli): disableYoloMode shouldn't enforce default approval mode against args
by @psinha40898 in
[#16155](https://github.com/google-gemini/gemini-cli/pull/16155)
- feat: add native Sublime Text support to IDE detection by @phreakocious in
[#16083](https://github.com/google-gemini/gemini-cli/pull/16083)
- refactor(core): extract ToolModificationHandler from scheduler by @abhipatel12
in [#16118](https://github.com/google-gemini/gemini-cli/pull/16118)
- Add support for Antigravity terminal in terminal setup utility by @raky291 in
[#16051](https://github.com/google-gemini/gemini-cli/pull/16051)
- feat(core): Wire up model routing to subagents. by @joshualitt in
[#16043](https://github.com/google-gemini/gemini-cli/pull/16043)
- feat(cli): add /agents slash command to list available agents by @adamfweidman
in [#16182](https://github.com/google-gemini/gemini-cli/pull/16182)
- docs(cli): fix includeDirectories nesting in configuration.md by @maru0804 in
[#15067](https://github.com/google-gemini/gemini-cli/pull/15067)
- feat: implement file system reversion utilities for rewind by @Adib234 in
[#15715](https://github.com/google-gemini/gemini-cli/pull/15715)
- Always enable redaction in GitHub actions. by @gundermanc in
[#16200](https://github.com/google-gemini/gemini-cli/pull/16200)
- fix: remove unsupported 'enabled' key from workflow config by @Han5991 in
[#15611](https://github.com/google-gemini/gemini-cli/pull/15611)
- docs: Remove redundant and duplicate documentation files by @liqzheng in
[#14699](https://github.com/google-gemini/gemini-cli/pull/14699)
- docs: shorten run command and use published version by @dsherret in
[#16172](https://github.com/google-gemini/gemini-cli/pull/16172)
- test(command-registry): increase initialization test timeout by @wszqkzqk in
[#15979](https://github.com/google-gemini/gemini-cli/pull/15979)
- Ensure TERM is set to xterm-256color by @falouu in
[#15828](https://github.com/google-gemini/gemini-cli/pull/15828)
- The telemetry.js script should handle paths that contain spaces by @JohnJAS in
[#12078](https://github.com/google-gemini/gemini-cli/pull/12078)
- ci: guard links workflow from running on forks by @wtanaka in
[#15461](https://github.com/google-gemini/gemini-cli/pull/15461)
- ci: guard nightly release workflow from running on forks by @wtanaka in
[#15463](https://github.com/google-gemini/gemini-cli/pull/15463)
- Support @ suggestions for subagenets by @sehoon38 in
[#16201](https://github.com/google-gemini/gemini-cli/pull/16201)
- feat(hooks): Support explicit stop and block execution control in model hooks
by @SandyTao520 in
[#15947](https://github.com/google-gemini/gemini-cli/pull/15947)
- Refine Gemini 3 system instructions to reduce model verbosity by
@NTaylorMullen in
[#16139](https://github.com/google-gemini/gemini-cli/pull/16139)
- chore: clean up unused models and use consts by @sehoon38 in
[#16246](https://github.com/google-gemini/gemini-cli/pull/16246)
- Always enable bracketed paste by @scidomino in
[#16179](https://github.com/google-gemini/gemini-cli/pull/16179)
- refactor: migrate clearCommand hook calls to HookSystem by @ved015 in
[#16157](https://github.com/google-gemini/gemini-cli/pull/16157)
- refactor: migrate app containter hook calls to hook system by @ishaanxgupta in
[#16161](https://github.com/google-gemini/gemini-cli/pull/16161)
- Show settings source in extensions lists by @chrstnb in
[#16207](https://github.com/google-gemini/gemini-cli/pull/16207)
- feat(skills): add pr-creator skill and enable skills by @NTaylorMullen in
[#16232](https://github.com/google-gemini/gemini-cli/pull/16232)
- fix: handle Shift+Space in Kitty keyboard protocol terminals by @tt-a1i in
[#15767](https://github.com/google-gemini/gemini-cli/pull/15767)
- feat(core, ui): Add /agents refresh command. by @joshualitt in
[#16204](https://github.com/google-gemini/gemini-cli/pull/16204)
- feat(core): add local experiments override via GEMINI_EXP by @kevin-ramdass in
[#16181](https://github.com/google-gemini/gemini-cli/pull/16181)
- feat(ui): reduce home directory warning noise and add opt-out setting by
@NTaylorMullen in
[#16229](https://github.com/google-gemini/gemini-cli/pull/16229)
- refactor: migrate chatCompressionService to use HookSystem by @ved015 in
[#16259](https://github.com/google-gemini/gemini-cli/pull/16259)
- fix: properly use systemMessage for hooks in UI by @jackwotherspoon in
[#16250](https://github.com/google-gemini/gemini-cli/pull/16250)
- Infer modifyOtherKeys support by @scidomino in
[#16270](https://github.com/google-gemini/gemini-cli/pull/16270)
- feat(core): Cache ignore instances for performance by @EricRahm in
[#16185](https://github.com/google-gemini/gemini-cli/pull/16185)
- feat: apply remote admin settings (no-op) by @skeshive in
[#16106](https://github.com/google-gemini/gemini-cli/pull/16106)
- Autogenerate docs/cli/settings.md docs/getting-started/configuration.md was
already autogenerated but settings.md was not. by @jacob314 in
[#14408](https://github.com/google-gemini/gemini-cli/pull/14408)
- refactor(config): remove legacy V1 settings migration logic by @galz10 in
[#16252](https://github.com/google-gemini/gemini-cli/pull/16252)
- Fix an issue where the agent stops prematurely by @gundermanc in
[#16269](https://github.com/google-gemini/gemini-cli/pull/16269)
- Update system prompt to prefer non-interactive commands by @NTaylorMullen in
[#16117](https://github.com/google-gemini/gemini-cli/pull/16117)
- Update ink version to 6.4.7 by @jacob314 in
[#16284](https://github.com/google-gemini/gemini-cli/pull/16284)
- Support for Built-in Agent Skills by @NTaylorMullen in
[#16045](https://github.com/google-gemini/gemini-cli/pull/16045)
- fix(skills): remove "Restart required" message from non-interactive commands
by @NTaylorMullen in
[#16307](https://github.com/google-gemini/gemini-cli/pull/16307)
- remove unused sessionHookTriggers and exports by @ved015 in
[#16324](https://github.com/google-gemini/gemini-cli/pull/16324)
- Triage action cleanup by @bdmorgan in
[#16319](https://github.com/google-gemini/gemini-cli/pull/16319)
- fix: Add event-driven trigger to issue triage workflow by @bdmorgan in
[#16334](https://github.com/google-gemini/gemini-cli/pull/16334)
- fix(workflows): resolve triage workflow failures and actionlint errors by
@bdmorgan in [#16338](https://github.com/google-gemini/gemini-cli/pull/16338)
- docs: add note about experimental hooks by @abhipatel12 in
[#16337](https://github.com/google-gemini/gemini-cli/pull/16337)
- feat(cli): implement passive activity logger for session analysis by
@SandyTao520 in
[#15829](https://github.com/google-gemini/gemini-cli/pull/15829)
- feat(cli): add /chat debug command for nightly builds by @abhipatel12 in
[#16339](https://github.com/google-gemini/gemini-cli/pull/16339)
- style: format pr-creator skill by @NTaylorMullen in
[#16381](https://github.com/google-gemini/gemini-cli/pull/16381)
- feat(cli): Hooks enable-all/disable-all feature with dynamic status by
@AbdulTawabJuly in
[#15552](https://github.com/google-gemini/gemini-cli/pull/15552)
- fix(core): ensure silent local subagent delegation while allowing remote
confirmation by @adamfweidman in
[#16395](https://github.com/google-gemini/gemini-cli/pull/16395)
- Markdown w/ Frontmatter Agent Parser by @sehoon38 in
[#16094](https://github.com/google-gemini/gemini-cli/pull/16094)
- Fix crash on unicode character by @chrstnb in
[#16420](https://github.com/google-gemini/gemini-cli/pull/16420)
- Attempt to resolve OOM w/ useMemo on history items by @chrstnb in
[#16424](https://github.com/google-gemini/gemini-cli/pull/16424)
- fix(core): ensure sub-agent schema and prompt refresh during runtime by
@adamfweidman in
[#16409](https://github.com/google-gemini/gemini-cli/pull/16409)
- Update extension examples by @chrstnb in
[#16274](https://github.com/google-gemini/gemini-cli/pull/16274)
- revert the change that was recently added from a fix by @sehoon38 in
[#16390](https://github.com/google-gemini/gemini-cli/pull/16390)
- Add other hook wrapper methods to hooksystem by @ved015 in
[#16361](https://github.com/google-gemini/gemini-cli/pull/16361)
- feat: introduce useRewindLogic hook for conversation history navigation by
@Adib234 in [#15716](https://github.com/google-gemini/gemini-cli/pull/15716)
- docs: Fix formatting issue in memport documentation by @wanglc02 in
[#14774](https://github.com/google-gemini/gemini-cli/pull/14774)
- fix(policy): enhance shell command safety and parsing by @allenhutchison in
[#15034](https://github.com/google-gemini/gemini-cli/pull/15034)
- fix(core): avoid 'activate_skill' re-registration warning by @NTaylorMullen in
[#16398](https://github.com/google-gemini/gemini-cli/pull/16398)
- perf(workflows): optimize PR triage script for faster execution by @bdmorgan
in [#16355](https://github.com/google-gemini/gemini-cli/pull/16355)
- feat(admin): prompt user to restart the CLI if they change auth to oauth
mid-session or don't have auth type selected at start of session by @skeshive
in [#16426](https://github.com/google-gemini/gemini-cli/pull/16426)
- Update cli-help agent's system prompt in sub-agents section by @sehoon38 in
[#16441](https://github.com/google-gemini/gemini-cli/pull/16441)
- Revert "Update extension examples" by @chrstnb in
[#16442](https://github.com/google-gemini/gemini-cli/pull/16442)
- Fix: add back fastreturn support by @scidomino in
[#16440](https://github.com/google-gemini/gemini-cli/pull/16440)
- feat(a2a): Introduce /memory command for a2a server by @cocosheng-g in
[#14456](https://github.com/google-gemini/gemini-cli/pull/14456)
- docs: fix broken internal link by using relative path by @Gong-Mi in
[#15371](https://github.com/google-gemini/gemini-cli/pull/15371)
- migrate yolo/auto-edit keybindings by @scidomino in
[#16457](https://github.com/google-gemini/gemini-cli/pull/16457)
- feat(cli): add install and uninstall commands for skills by @NTaylorMullen in
[#16377](https://github.com/google-gemini/gemini-cli/pull/16377)
- feat(ui): use Tab to switch focus between shell and input by @jacob314 in
[#14332](https://github.com/google-gemini/gemini-cli/pull/14332)
- feat(core): support shipping built-in skills with the CLI by @NTaylorMullen in
[#16300](https://github.com/google-gemini/gemini-cli/pull/16300)
- Collect hardware details telemetry. by @gundermanc in
[#16119](https://github.com/google-gemini/gemini-cli/pull/16119)
- feat(agents): improve UI feedback and parser reliability by @NTaylorMullen in
[#16459](https://github.com/google-gemini/gemini-cli/pull/16459)
- Migrate keybindings by @scidomino in
[#16460](https://github.com/google-gemini/gemini-cli/pull/16460)
- feat(cli): cleanup activity logs alongside session files by @SandyTao520 in
[#16399](https://github.com/google-gemini/gemini-cli/pull/16399)
- feat(cli): implement dynamic terminal tab titles for CLI status by
@NTaylorMullen in
[#16378](https://github.com/google-gemini/gemini-cli/pull/16378)
- feat(core): add disableLLMCorrection setting to skip auto-correction in edit
tools by @SandyTao520 in
[#16000](https://github.com/google-gemini/gemini-cli/pull/16000)
- fix: Set both tab and window title instead of just window title by
@NTaylorMullen in
[#16464](https://github.com/google-gemini/gemini-cli/pull/16464)
- fix(policy): ensure MCP policies match unqualified names in non-interactive
mode by @NTaylorMullen in
[#16490](https://github.com/google-gemini/gemini-cli/pull/16490)
- fix(cli): refine 'Action Required' indicator and focus hints by @NTaylorMullen
in [#16497](https://github.com/google-gemini/gemini-cli/pull/16497)
- Refactor beforeAgent and afterAgent hookEvents to follow desired output by
@ved015 in [#16495](https://github.com/google-gemini/gemini-cli/pull/16495)
- feat(agents): clarify mandatory YAML frontmatter for sub-agents by
@NTaylorMullen in
[#16515](https://github.com/google-gemini/gemini-cli/pull/16515)
- docs(telemetry): add Google Cloud Monitoring dashboard documentation by @jerop
in [#16520](https://github.com/google-gemini/gemini-cli/pull/16520)
- Implement support for subagents as extensions. by @gundermanc in
[#16473](https://github.com/google-gemini/gemini-cli/pull/16473)
- refactor: make baseTimestamp optional in addItem and remove redundant calls by
@sehoon38 in [#16471](https://github.com/google-gemini/gemini-cli/pull/16471)
- Improve key binding names and descriptions by @scidomino in
[#16529](https://github.com/google-gemini/gemini-cli/pull/16529)
- feat(core, cli): Add support for agents in settings.json. by @joshualitt in
[#16433](https://github.com/google-gemini/gemini-cli/pull/16433)
- fix(cli): fix 'gemini skills install' unknown argument error by @NTaylorMullen
in [#16537](https://github.com/google-gemini/gemini-cli/pull/16537)
- chore(ui): optimize AgentsStatus layout with dense list style and group
separation by @adamfweidman in
[#16545](https://github.com/google-gemini/gemini-cli/pull/16545)
- fix(cli): allow @ file selector on slash command lines by @galz10 in
[#16370](https://github.com/google-gemini/gemini-cli/pull/16370)
- fix(ui): resolve sticky header regression in tool messages by @jacob314 in
[#16514](https://github.com/google-gemini/gemini-cli/pull/16514)
- feat(core): Align internal agent settings with configs exposed through
settings.json by @joshualitt in
[#16458](https://github.com/google-gemini/gemini-cli/pull/16458)
- fix(cli): copy uses OSC52 only in SSH/WSL by @assagman in
[#16554](https://github.com/google-gemini/gemini-cli/pull/16554)
- docs(skills): clarify skill directory structure and file location by
@NTaylorMullen in
[#16532](https://github.com/google-gemini/gemini-cli/pull/16532)
- Fix: make ctrl+x use preferred editor by @scidomino in
[#16556](https://github.com/google-gemini/gemini-cli/pull/16556)
- fix(core): Resolve race condition in tool response reporting by @abhipatel12
in [#16557](https://github.com/google-gemini/gemini-cli/pull/16557)
- feat(ui): highlight persist mode status in ModelDialog by @sehoon38 in
[#16483](https://github.com/google-gemini/gemini-cli/pull/16483)
- refactor: clean up A2A task output for users and LLMs by @adamfweidman in
[#16561](https://github.com/google-gemini/gemini-cli/pull/16561)
- feat(core/ui): enhance retry mechanism and UX by @sehoon38 in
[#16489](https://github.com/google-gemini/gemini-cli/pull/16489)
- Modernize MaxSizedBox to use and ResizeObservers by @jacob314 in
[#16565](https://github.com/google-gemini/gemini-cli/pull/16565)
- Behavioral evals framework. by @gundermanc in
[#16047](https://github.com/google-gemini/gemini-cli/pull/16047)
- Aggregate test results. by @gundermanc in
[#16581](https://github.com/google-gemini/gemini-cli/pull/16581)
- feat(admin): support admin-enforced settings for Agent Skills by
@NTaylorMullen in
[#16406](https://github.com/google-gemini/gemini-cli/pull/16406)
- fix(patch): cherry-pick cfdc4cf to release/v0.25.0-preview.0-pr-16759 to patch
version v0.25.0-preview.0 and create version 0.25.0-preview.1 by
@gemini-cli-robot in
[#16866](https://github.com/google-gemini/gemini-cli/pull/16866)
- Patch #16730 into v0.25.0 preview by @chrstnb in
[#16882](https://github.com/google-gemini/gemini-cli/pull/16882)
- fix(patch): cherry-pick 3b55581 to release/v0.25.0-preview.2-pr-16506 to patch
version v0.25.0-preview.2 and create version 0.25.0-preview.3 by
@gemini-cli-robot in
[#17098](https://github.com/google-gemini/gemini-cli/pull/17098)
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
https://github.com/google-gemini/gemini-cli/pull/14843
- feat: display quota stats for unused models in /stats by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/14764
- feat: ensure codebase investigator uses preview model when main agent does by
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
- chore: add closing reason to stale bug workflow by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/14861
- Send the model and CLI version with the user agent by @gundermanc in
https://github.com/google-gemini/gemini-cli/pull/14865
- refactor(sessions): move session summary generation to startup by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
- Limit search depth in path corrector by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14869
- Fix: Correct typo in code comment by @kuishou68 in
https://github.com/google-gemini/gemini-cli/pull/14671
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/14597
- feat: attempt more error parsing by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/14899
- Add missing await. by @gundermanc in
https://github.com/google-gemini/gemini-cli/pull/14910
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
extension by @svarlamov in
https://github.com/google-gemini/gemini-cli/pull/14663
- refactor: implement DelegateToAgentTool with discriminated union by
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
- feat: reset availabilityService on /auth by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/14911
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
https://github.com/google-gemini/gemini-cli/pull/14937
- increase labeler timeout by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14922
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
- feat(cli): polish cached token stats and simplify stats display when quota is
present. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/14961
- feat(settings-validation): add validation for settings schema by @lifefloating
in https://github.com/google-gemini/gemini-cli/pull/12929
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
https://github.com/google-gemini/gemini-cli/pull/14999
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
https://github.com/google-gemini/gemini-cli/pull/14998
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
https://github.com/google-gemini/gemini-cli/pull/13419
- feat: support multi-file drag and drop of images by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/14832
- fix(policy): allow codebase_investigator by default in read-only policy by
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
- refactor(ide ext): Update port file name + switch to 1-based index for
characters + remove truncation text by @skeshive in
https://github.com/google-gemini/gemini-cli/pull/10501
- fix(vscode-ide-companion): correct license generation for workspace
dependencies by @skeshive in
https://github.com/google-gemini/gemini-cli/pull/15004
- fix: temp fix for subagent invocation until subagent delegation is merged to
stable by @abhipatel12 in
https://github.com/google-gemini/gemini-cli/pull/15007
- test: update ide detection tests to make them more robust when run in an ide
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/14983
- Add license field into package.json by @jb-perez in
https://github.com/google-gemini/gemini-cli/pull/14473
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
- fix(core): commandPrefix word boundary and compound command safety by
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
in https://github.com/google-gemini/gemini-cli/pull/14914
- Refresh hooks when refreshing extensions. by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14918
- Add clarity to error messages by @gsehgal in
https://github.com/google-gemini/gemini-cli/pull/14879
- chore : remove a redundant tip by @JayadityaGit in
https://github.com/google-gemini/gemini-cli/pull/14947
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
- Disallow redundant typecasts. by @gundermanc in
https://github.com/google-gemini/gemini-cli/pull/15030
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
- fix: use zod for safety check result validation by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/15026
- update(telemetry): add hashed_extension_name to field to extension events by
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
- fix: similar to policy-engine, throw error in case of requiring tool execution
confirmation for non-interactive mode by @MayV in
https://github.com/google-gemini/gemini-cli/pull/14702
- Clean up processes in integration tests by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15102
- docs: update policy engine getting started and defaults by @NTaylorMullen in
https://github.com/google-gemini/gemini-cli/pull/15105
- Fix tool output fragmentation by encapsulating content in functionResponse by
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
- Simplify method signature. by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15114
- Show raw input token counts in json output. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/15021
- fix: Mark A2A requests as interactive by @MayV in
https://github.com/google-gemini/gemini-cli/pull/15108
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
in https://github.com/google-gemini/gemini-cli/pull/15131
- refactor(cli): fix settings merging so that settings using the new json format
take priority over ones using the old format by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/15116
- fix(patch): cherry-pick a6d1245 to release/v0.22.0-preview.1-pr-15214 to patch
version v0.22.0-preview.1 and create version 0.22.0-preview.2 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15226
- fix(patch): cherry-pick 9e6914d to release/v0.22.0-preview.2-pr-15288 to patch
version v0.22.0-preview.2 and create version 0.22.0-preview.3 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15294
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.24.5...v0.25.0
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.21.3...v0.22.0
+117 -318
View File
@@ -1,6 +1,6 @@
# Preview release: Release v0.26.0-preview.0
# Preview release: Release v0.23.0-preview.0
Released: January 21, 2026
Released: December 22, 2025
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -11,322 +11,121 @@ To install the preview release:
npm install -g @google/gemini-cli@preview
```
## Highlights
- **Skills and Agents:** Improvements to the `activate_skill` tool and skill
management. Experimental Agent Skills support.
- **UI/UX:** Addition of a Rewind Confirmation dialog and Viewer component.
- **Extensions:** Experimental setting for extension configuration.
- **Bug Fixes and Stability:** PDF token estimation fix and improvements to
scheduled issue triage.
## What's Changed
- fix: PDF token estimation
([#16494](https://github.com/google-gemini/gemini-cli/pull/16494)) by
@korade-krushna in
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
@gemini-cli-robot in
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
- docs: clarify F12 to open debug console by @jackwotherspoon in
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
- docs: Remove .md extension from internal links in architecture.md by
@medic-code in
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
- Add an experimental setting for extension config by @chrstnb in
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
- prefactor: add rootCommands as array so it can be used for policy parsing by
@abhipatel12 in
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
- remove unnecessary \x7f key bindings by @scidomino in
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
- docs(skills): use body-file in pr-creator skill for better reliability by
@abhipatel12 in
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
@NTaylorMullen in
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
- chore(automation): remove automated PR size and complexity labeler by
@bdmorgan in [#16648](https://github.com/google-gemini/gemini-cli/pull/16648)
- refactor(skills): replace 'project' with 'workspace' scope by @NTaylorMullen
in [#16380](https://github.com/google-gemini/gemini-cli/pull/16380)
- Docs: Update release notes for 1/13/2026 by @jkcinouye in
[#16583](https://github.com/google-gemini/gemini-cli/pull/16583)
- Simplify paste handling by @scidomino in
[#16654](https://github.com/google-gemini/gemini-cli/pull/16654)
- chore(automation): improve scheduled issue triage discovery and throughput by
@bdmorgan in [#16652](https://github.com/google-gemini/gemini-cli/pull/16652)
- fix(acp): run exit cleanup when stdin closes by @codefromthecrypt in
[#14953](https://github.com/google-gemini/gemini-cli/pull/14953)
- feat(scheduler): add types needed for event driven scheduler by @abhipatel12
in [#16641](https://github.com/google-gemini/gemini-cli/pull/16641)
- Remove unused rewind key binding by @scidomino in
[#16659](https://github.com/google-gemini/gemini-cli/pull/16659)
- Remove sequence binding by @scidomino in
[#16664](https://github.com/google-gemini/gemini-cli/pull/16664)
- feat(cli): undeprecate the --prompt flag by @alexaustin007 in
[#13981](https://github.com/google-gemini/gemini-cli/pull/13981)
- chore: update dependabot configuration by @cosmopax in
[#13507](https://github.com/google-gemini/gemini-cli/pull/13507)
- feat(config): add 'auto' alias for default model selection by @sehoon38 in
[#16661](https://github.com/google-gemini/gemini-cli/pull/16661)
- Enable & disable agents by @sehoon38 in
[#16225](https://github.com/google-gemini/gemini-cli/pull/16225)
- cleanup: Improve keybindings by @scidomino in
[#16672](https://github.com/google-gemini/gemini-cli/pull/16672)
- Add timeout for shell-utils to prevent hangs. by @jacob314 in
[#16667](https://github.com/google-gemini/gemini-cli/pull/16667)
- feat(plan): add experimental plan flag by @jerop in
[#16650](https://github.com/google-gemini/gemini-cli/pull/16650)
- feat(cli): add security consent prompts for skill installation by
@NTaylorMullen in
[#16549](https://github.com/google-gemini/gemini-cli/pull/16549)
- fix: replace 3 consecutive periods with ellipsis character by @Vist233 in
[#16587](https://github.com/google-gemini/gemini-cli/pull/16587)
- chore(automation): ensure status/need-triage is applied and never cleared
automatically by @bdmorgan in
[#16657](https://github.com/google-gemini/gemini-cli/pull/16657)
- fix: Handle colons in skill description frontmatter by @maru0804 in
[#16345](https://github.com/google-gemini/gemini-cli/pull/16345)
- refactor(core): harden skill frontmatter parsing by @NTaylorMullen in
[#16705](https://github.com/google-gemini/gemini-cli/pull/16705)
- feat(skills): add conflict detection and warnings for skill overrides by
@NTaylorMullen in
[#16709](https://github.com/google-gemini/gemini-cli/pull/16709)
- feat(scheduler): add SchedulerStateManager for reactive tool state by
@abhipatel12 in
[#16651](https://github.com/google-gemini/gemini-cli/pull/16651)
- chore(automation): enforce 'help wanted' label permissions and update
guidelines by @bdmorgan in
[#16707](https://github.com/google-gemini/gemini-cli/pull/16707)
- fix(core): resolve circular dependency via tsconfig paths by @sehoon38 in
[#16730](https://github.com/google-gemini/gemini-cli/pull/16730)
- chore/release: bump version to 0.26.0-nightly.20260115.6cb3ae4e0 by
@gemini-cli-robot in
[#16738](https://github.com/google-gemini/gemini-cli/pull/16738)
- fix(automation): correct status/need-issue label matching wildcard by
@bdmorgan in [#16727](https://github.com/google-gemini/gemini-cli/pull/16727)
- fix(automation): prevent label-enforcer loop by ignoring all bots by @bdmorgan
in [#16746](https://github.com/google-gemini/gemini-cli/pull/16746)
- Add links to supported locations and minor fixes by @g-samroberts in
[#16476](https://github.com/google-gemini/gemini-cli/pull/16476)
- feat(policy): add source tracking to policy rules by @allenhutchison in
[#16670](https://github.com/google-gemini/gemini-cli/pull/16670)
- feat(automation): enforce '🔒 maintainer only' and fix bot loop by @bdmorgan
in [#16751](https://github.com/google-gemini/gemini-cli/pull/16751)
- Make merged settings non-nullable and fix all lints related to that. by
@jacob314 in [#16647](https://github.com/google-gemini/gemini-cli/pull/16647)
- fix(core): prevent ModelInfo event emission on aborted signal by @sehoon38 in
[#16752](https://github.com/google-gemini/gemini-cli/pull/16752)
- Replace relative paths to fix website build by @chrstnb in
[#16755](https://github.com/google-gemini/gemini-cli/pull/16755)
- Restricting to localhost by @cocosheng-g in
[#16548](https://github.com/google-gemini/gemini-cli/pull/16548)
- fix(cli): add explicit dependency on color-convert by @sehoon38 in
[#16757](https://github.com/google-gemini/gemini-cli/pull/16757)
- fix(automation): robust label enforcement with permission checks by @bdmorgan
in [#16762](https://github.com/google-gemini/gemini-cli/pull/16762)
- fix(cli): prevent OOM crash by limiting file search traversal and adding
timeout by @galz10 in
[#16696](https://github.com/google-gemini/gemini-cli/pull/16696)
- fix(cli): safely handle /dev/tty access on macOS by @korade-krushna in
[#16531](https://github.com/google-gemini/gemini-cli/pull/16531)
- docs: clarify workspace test execution in GEMINI.md by @mattKorwel in
[#16764](https://github.com/google-gemini/gemini-cli/pull/16764)
- Add support for running available commands prior to MCP servers loading by
@Adib234 in [#15596](https://github.com/google-gemini/gemini-cli/pull/15596)
- feat(plan): add experimental 'plan' approval mode by @jerop in
[#16753](https://github.com/google-gemini/gemini-cli/pull/16753)
- feat(scheduler): add functional awaitConfirmation utility by @abhipatel12 in
[#16721](https://github.com/google-gemini/gemini-cli/pull/16721)
- fix(infra): update maintainer rollup label to 'workstream-rollup' by @bdmorgan
in [#16809](https://github.com/google-gemini/gemini-cli/pull/16809)
- fix(infra): use GraphQL to detect direct parents in rollup workflow by
@bdmorgan in [#16811](https://github.com/google-gemini/gemini-cli/pull/16811)
- chore(workflows): rename label-workstream-rollup workflow by @bdmorgan in
[#16818](https://github.com/google-gemini/gemini-cli/pull/16818)
- skip simple-mcp-server.test.ts by @scidomino in
[#16842](https://github.com/google-gemini/gemini-cli/pull/16842)
- Steer outer agent to use expert subagents when present by @gundermanc in
[#16763](https://github.com/google-gemini/gemini-cli/pull/16763)
- Fix race condition by awaiting scheduleToolCalls by @chrstnb in
[#16759](https://github.com/google-gemini/gemini-cli/pull/16759)
- cleanup: Organize key bindings by @scidomino in
[#16798](https://github.com/google-gemini/gemini-cli/pull/16798)
- feat(core): Add generalist agent. by @joshualitt in
[#16638](https://github.com/google-gemini/gemini-cli/pull/16638)
- perf(ui): optimize text buffer and highlighting for large inputs by
@NTaylorMullen in
[#16782](https://github.com/google-gemini/gemini-cli/pull/16782)
- fix(core): fix PTY descriptor shell leak by @galz10 in
[#16773](https://github.com/google-gemini/gemini-cli/pull/16773)
- feat(plan): enforce strict read-only policy and halt execution on violation by
@jerop in [#16849](https://github.com/google-gemini/gemini-cli/pull/16849)
- remove need-triage label from bug_report template by @sehoon38 in
[#16864](https://github.com/google-gemini/gemini-cli/pull/16864)
- fix(core): truncate large telemetry log entries by @sehoon38 in
[#16769](https://github.com/google-gemini/gemini-cli/pull/16769)
- docs(extensions): add Agent Skills support and mark feature as experimental by
@NTaylorMullen in
[#16859](https://github.com/google-gemini/gemini-cli/pull/16859)
- fix(core): surface warnings for invalid hook event names in configuration
([#16788](https://github.com/google-gemini/gemini-cli/pull/16788)) by
@sehoon38 in [#16873](https://github.com/google-gemini/gemini-cli/pull/16873)
- feat(plan): remove read_many_files from approval mode policies by @jerop in
[#16876](https://github.com/google-gemini/gemini-cli/pull/16876)
- feat(admin): implement admin controls polling and restart prompt by @skeshive
in [#16627](https://github.com/google-gemini/gemini-cli/pull/16627)
- Remove LRUCache class migrating to mnemoist by @jacob314 in
[#16872](https://github.com/google-gemini/gemini-cli/pull/16872)
- feat(settings): rename negative settings to positive naming (disable* ->
enable*) by @afarber in
[#14142](https://github.com/google-gemini/gemini-cli/pull/14142)
- refactor(cli): unify shell confirmation dialogs by @NTaylorMullen in
[#16828](https://github.com/google-gemini/gemini-cli/pull/16828)
- feat(agent): enable agent skills by default by @NTaylorMullen in
[#16736](https://github.com/google-gemini/gemini-cli/pull/16736)
- refactor(core): foundational truncation refactoring and token estimation
optimization by @NTaylorMullen in
[#16824](https://github.com/google-gemini/gemini-cli/pull/16824)
- fix(hooks): enable /hooks disable to reliably stop single hooks by
@abhipatel12 in
[#16804](https://github.com/google-gemini/gemini-cli/pull/16804)
- Don't commit unless user asks us to. by @gundermanc in
[#16902](https://github.com/google-gemini/gemini-cli/pull/16902)
- chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 by @adamfweidman in
[#16800](https://github.com/google-gemini/gemini-cli/pull/16800)
- fix: Show experiment values in settings UI for compressionThreshold by
@ishaanxgupta in
[#16267](https://github.com/google-gemini/gemini-cli/pull/16267)
- feat(cli): replace relative keyboard shortcuts link with web URL by
@imaliabbas in
[#16479](https://github.com/google-gemini/gemini-cli/pull/16479)
- fix(core): resolve PKCE length issue and stabilize OAuth redirect port by
@sehoon38 in [#16815](https://github.com/google-gemini/gemini-cli/pull/16815)
- Delete rewind documentation for now by @Adib234 in
[#16932](https://github.com/google-gemini/gemini-cli/pull/16932)
- Stabilize skill-creator CI and package format by @NTaylorMullen in
[#17001](https://github.com/google-gemini/gemini-cli/pull/17001)
- Stabilize the git evals by @gundermanc in
[#16989](https://github.com/google-gemini/gemini-cli/pull/16989)
- fix(core): attempt compression before context overflow check by @NTaylorMullen
in [#16914](https://github.com/google-gemini/gemini-cli/pull/16914)
- Fix inverted logic. by @gundermanc in
[#17007](https://github.com/google-gemini/gemini-cli/pull/17007)
- chore(scripts): add duplicate issue closer script and fix lint errors by
@bdmorgan in [#16997](https://github.com/google-gemini/gemini-cli/pull/16997)
- docs: update README and config guide to reference Gemini 3 by @JayadityaGit in
[#15806](https://github.com/google-gemini/gemini-cli/pull/15806)
- fix(cli): correct Homebrew installation detection by @kij in
[#14727](https://github.com/google-gemini/gemini-cli/pull/14727)
- Demote git evals to nightly run. by @gundermanc in
[#17030](https://github.com/google-gemini/gemini-cli/pull/17030)
- fix(cli): use OSC-52 clipboard copy in Windows Terminal by @Thomas-Shephard in
[#16920](https://github.com/google-gemini/gemini-cli/pull/16920)
- Fix: Process all parts in response chunks when thought is first by @pyrytakala
in [#13539](https://github.com/google-gemini/gemini-cli/pull/13539)
- fix(automation): fix jq quoting error in pr-triage.sh by @Kimsoo0119 in
[#16958](https://github.com/google-gemini/gemini-cli/pull/16958)
- refactor(core): decouple scheduler into orchestration, policy, and
confirmation by @abhipatel12 in
[#16895](https://github.com/google-gemini/gemini-cli/pull/16895)
- feat: add /introspect slash command by @NTaylorMullen in
[#17048](https://github.com/google-gemini/gemini-cli/pull/17048)
- refactor(cli): centralize tool mapping and decouple legacy scheduler by
@abhipatel12 in
[#17044](https://github.com/google-gemini/gemini-cli/pull/17044)
- fix(ui): ensure rationale renders before tool calls by @NTaylorMullen in
[#17043](https://github.com/google-gemini/gemini-cli/pull/17043)
- fix(workflows): use author_association for maintainer check by @bdmorgan in
[#17060](https://github.com/google-gemini/gemini-cli/pull/17060)
- fix return type of fireSessionStartEvent to defaultHookOutput by @ved015 in
[#16833](https://github.com/google-gemini/gemini-cli/pull/16833)
- feat(cli): add experiment gate for event-driven scheduler by @abhipatel12 in
[#17055](https://github.com/google-gemini/gemini-cli/pull/17055)
- feat(core): improve shell redirection transparency and security by
@NTaylorMullen in
[#16486](https://github.com/google-gemini/gemini-cli/pull/16486)
- fix(core): deduplicate ModelInfo emission in GeminiClient by @NTaylorMullen in
[#17075](https://github.com/google-gemini/gemini-cli/pull/17075)
- docs(themes): remove unsupported DiffModified color key by @jw409 in
[#17073](https://github.com/google-gemini/gemini-cli/pull/17073)
- fix: update currentSequenceModel when modelChanged by @adamfweidman in
[#17051](https://github.com/google-gemini/gemini-cli/pull/17051)
- feat(core): enhanced anchored iterative context compression with
self-verification by @rmedranollamas in
[#15710](https://github.com/google-gemini/gemini-cli/pull/15710)
- Fix mcp instructions by @chrstnb in
[#16439](https://github.com/google-gemini/gemini-cli/pull/16439)
- [A2A] Disable checkpointing if git is not installed by @cocosheng-g in
[#16896](https://github.com/google-gemini/gemini-cli/pull/16896)
- feat(admin): set admin.skills.enabled based on advancedFeaturesEnabled setting
by @skeshive in
[#17095](https://github.com/google-gemini/gemini-cli/pull/17095)
- Test coverage for hook exit code cases by @gundermanc in
[#17041](https://github.com/google-gemini/gemini-cli/pull/17041)
- Revert "Revert "Update extension examples"" by @chrstnb in
[#16445](https://github.com/google-gemini/gemini-cli/pull/16445)
- fix(core): Provide compact, actionable errors for agent delegation failures by
@SandyTao520 in
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
- fix(core): update telemetry token count after session resume by @psinha40898
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
- Demote the subagent test to nightly by @gundermanc in
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
- feat: Add flash lite utility fallback chain by @adamfweidman in
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
- feat(core): Add initial eval for generalist agent. by @joshualitt in
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
- fix(core): resolve auto model in default strategy by @sehoon38 in
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
- docs: update project context and pr-creator workflow by @NTaylorMullen in
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
- fix(cli): send gemini-cli version as mcp client version by @dsp in
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
- Remove missing sidebar item by @chrstnb in
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
- feat(core): Ensure all properties in hooks object are event names. by
@joshualitt in
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
- fix(cli): fix newline support broken in previous PR by @scidomino in
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
by @gsquared94 in
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
- Avoid spurious warnings about unexpected renders triggered by appEvents and
coreEvents. by @jacob314 in
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
- fix(cli): resolve home/end keybinding conflict by @scidomino in
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
- fix(cli): display 'http' type on mcp list by @pamanta in
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
- fix bad fallback logic external editor logic by @scidomino in
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
- Fix bug where System scopes weren't migrated. by @jacob314 in
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
- Fix mcp tool lookup in tool registry by @werdnum in
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
- Code assist service metrics. by @gundermanc in
https://github.com/google-gemini/gemini-cli/pull/15024
- chore/release: bump version to 0.21.0-nightly.20251216.bb0c0d8ee by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15121
- Docs by @Roaimkhan in https://github.com/google-gemini/gemini-cli/pull/15103
- Use official ACP SDK and support HTTP/SSE based MCP servers by @SteffenDE in
https://github.com/google-gemini/gemini-cli/pull/13856
- Remove foreground for themes other than shades of purple and holiday. by
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14606
- chore: remove repo specific tips by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/15164
- chore: remove user query from footer in debug mode by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/15169
- Disallow unnecessary awaits. by @gundermanc in
https://github.com/google-gemini/gemini-cli/pull/15172
- Add one to the padding in settings dialog to avoid flicker. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/15173
- feat(core): introduce remote agent infrastructure and rename local executor by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/15110
- feat(cli): Add `/auth logout` command to clear credentials and auth state by
@CN-Scars in https://github.com/google-gemini/gemini-cli/pull/13383
- (fix) Automated pr labeler by @DaanVersavel in
https://github.com/google-gemini/gemini-cli/pull/14885
- feat: launch Gemini 3 Flash in Gemini CLI ⚡️⚡️⚡️ by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15196
- Refactor: Migrate console.error in ripGrep.ts to debugLogger by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/15201
- chore: update a2a-js to 0.3.7 by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/15197
- chore(core): remove redundant isModelAvailabilityServiceEnabled toggle and
clean up dead code by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/15207
- feat(core): Late resolve `GenerateContentConfig`s and reduce mutation. by
@joshualitt in https://github.com/google-gemini/gemini-cli/pull/14920
- Respect previewFeatures value from the remote flag if undefined by @sehoon38
in https://github.com/google-gemini/gemini-cli/pull/15214
- feat(ui): add Windows clipboard image support and Alt+V paste workaround by
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/15218
- chore(core): remove legacy fallback flags and migrate loop detection by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/15213
- fix(ui): Prevent eager slash command completion hiding sibling commands by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15224
- Docs: Update Changelog for Dec 17, 2025 by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/15204
- Code Assist backend telemetry for user accept/reject of suggestions by
@gundermanc in https://github.com/google-gemini/gemini-cli/pull/15206
- fix(cli): correct initial history length handling for chat commands by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15223
- chore/release: bump version to 0.21.0-nightly.20251218.739c02bd6 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15231
- Change detailed model stats to use a new shared Table class to resolve
robustness issues. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/15208
- feat: add agent toml parser by @abhipatel12 in
https://github.com/google-gemini/gemini-cli/pull/15112
- Add core tool that adds all context from the core package. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/15238
- (docs): Add reference section to hooks documentation by @abhipatel12 in
https://github.com/google-gemini/gemini-cli/pull/15159
- feat(hooks): add support for friendly names and descriptions by @abhipatel12
in https://github.com/google-gemini/gemini-cli/pull/15174
- feat: Detect background color by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/15132
- add 3.0 to allowed sensitive keywords by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15276
- feat: Pass additional environment variables to shell execution by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/15160
- Remove unused code by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15290
- Handle all 429 as retryableQuotaError by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/15288
- Remove unnecessary dependencies by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15291
- fix: prevent infinite loop in prompt completion on error by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/14548
- fix(ui): show command suggestions even on perfect match and sort them by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15287
- feat(hooks): reduce log verbosity and improve error reporting in UI by
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15297
- feat: simplify tool confirmation labels for better UX by @NTaylorMullen in
https://github.com/google-gemini/gemini-cli/pull/15296
- chore/release: bump version to 0.21.0-nightly.20251219.70696e364 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15301
- feat(core): Implement JIT context memory loading and UI sync by @SandyTao520
in https://github.com/google-gemini/gemini-cli/pull/14469
- feat(ui): Put "Allow for all future sessions" behind a setting off by default.
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/15322
- fix(cli):change the placeholder of input during the shell mode by
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/15135
- Validate OAuth resource parameter matches MCP server URL by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/15289
- docs(cli): add System Prompt Override (GEMINI_SYSTEM_MD) by @ashmod in
https://github.com/google-gemini/gemini-cli/pull/9515
- more robust command parsing logs by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15339
- Introspection agent demo by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/15232
- fix(core): sanitize hook command expansion and prevent injection by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15343
- fix(folder trust): add validation for trusted folder level by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/12215
- fix(cli): fix right border overflow in trust dialogs by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/15350
- fix(policy): fix bug where accepting-edits continued after it was turned off
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/15351
- fix: prevent infinite relaunch loop when --resume fails (#14941) by @Ying-xi
in https://github.com/google-gemini/gemini-cli/pull/14951
- chore/release: bump version to 0.21.0-nightly.20251220.41a1a3eed by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15352
- feat(telemetry): add clearcut logging for hooks by @abhipatel12 in
https://github.com/google-gemini/gemini-cli/pull/15405
- fix(core): Add `.geminiignore` support to SearchText tool by @xyrolle in
https://github.com/google-gemini/gemini-cli/pull/13763
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.25.0-preview.4...v0.26.0-preview.0
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.22.0-preview.3...v0.23.0-preview.0
File diff suppressed because it is too large Load Diff
-18
View File
@@ -167,7 +167,6 @@ Slash commands provide meta-level control over the CLI itself.
- **Note:** Only available if checkpointing is configured via
[settings](../get-started/configuration.md). See
[Checkpointing documentation](../cli/checkpointing.md) for more details.
- **`/resume`**
- **Description:** Browse and resume previous conversation sessions. Opens an
interactive session browser where you can search, filter, and select from
@@ -198,23 +197,6 @@ Slash commands provide meta-level control over the CLI itself.
modify them as desired. Changes to some settings are applied immediately,
while others require a restart.
- [**`/skills`**](./skills.md)
- **Description:** (Experimental) Manage Agent Skills, which provide on-demand
expertise and specialized workflows.
- **Sub-commands:**
- **`list`**:
- **Description:** List all discovered skills and their current status
(enabled/disabled).
- **`enable`**:
- **Description:** Enable a specific skill by name.
- **Usage:** `/skills enable <name>`
- **`disable`**:
- **Description:** Disable a specific skill by name.
- **Usage:** `/skills disable <name>`
- **`reload`**:
- **Description:** Refresh the list of discovered skills from all tiers
(workspace, user, and extensions).
- **`/stats`**
- **Description:** Display detailed statistics for the current Gemini CLI
session, including token usage, cached token savings (when available), and
+9 -9
View File
@@ -50,7 +50,7 @@ Your command definition files must be written in the TOML format and use the
## Handling arguments
Custom commands support two powerful methods for handling arguments. The CLI
automatically chooses the correct method based on the content of your command's
automatically chooses the correct method based on the content of your command\'s
`prompt`.
### 1. Context-aware injection with `{{args}}`
@@ -96,13 +96,13 @@ Search Results:
"""
```
When you run `/grep-code It's complicated`:
When you run `/grep-code It\'s complicated`:
1. The CLI sees `{{args}}` used both outside and inside `!{...}`.
2. Outside: The first `{{args}}` is replaced raw with `It's complicated`.
2. Outside: The first `{{args}}` is replaced raw with `It\'s complicated`.
3. Inside: The second `{{args}}` is replaced with the escaped version (e.g., on
Linux: `"It\'s complicated"`).
4. The command executed is `grep -r "It's complicated" .`.
4. The command executed is `grep -r "It\'s complicated" .`.
5. The CLI prompts you to confirm this exact, secure command before execution.
6. The final prompt is sent.
@@ -129,13 +129,13 @@ format and behavior.
# In: <project>/.gemini/commands/changelog.toml
# Invoked via: /changelog 1.2.0 added "Support for default argument parsing."
description = "Adds a new entry to the project's CHANGELOG.md file."
description = "Adds a new entry to the project\'s CHANGELOG.md file."
prompt = """
# Task: Update Changelog
You are an expert maintainer of this software project. A user has invoked a command to add a new entry to the changelog.
**The user's raw command is appended below your instructions.**
**The user\'s raw command is appended below your instructions.**
Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.
@@ -147,7 +147,7 @@ The command follows this format: `/changelog <version> <type> <message>`
1. Read the `CHANGELOG.md` file.
2. Find the section for the specified `<version>`.
3. Add the `<message>` under the correct `<type>` heading.
4. If the version or type section doesn't exist, create it.
4. If the version or type section doesn\'t exist, create it.
5. Adhere strictly to the "Keep a Changelog" format.
"""
```
@@ -241,7 +241,7 @@ operate on specific files.
**Example (`review.toml`):**
This command injects the content of a _fixed_ best practices file
(`docs/best-practices.md`) and uses the user's arguments to provide context for
(`docs/best-practices.md`) and uses the user\'s arguments to provide context for
the review.
```toml
@@ -293,7 +293,7 @@ practice.
description = "Asks the model to refactor the current context into a pure function."
prompt = """
Please analyze the code I've provided in the current context.
Please analyze the code I\'ve provided in the current context.
Refactor it into a pure function.
Your response should include:
+2 -4
View File
@@ -25,12 +25,10 @@ overview of Gemini CLI, see the [main documentation page](../index.md).
- **[Checkpointing](./checkpointing.md):** Automatically save and restore
snapshots of your session and files.
- **[Enterprise configuration](./enterprise.md):** Deploy and manage Gemini CLI
in an enterprise environment.
- **[Enterprise configuration](./enterprise.md):** Deploying and manage Gemini
CLI in an enterprise environment.
- **[Sandboxing](./sandbox.md):** Isolate tool execution in a secure,
containerized environment.
- **[Agent Skills](./skills.md):** (Experimental) Extend the CLI with
specialized expertise and procedural workflows.
- **[Telemetry](./telemetry.md):** Configure observability to monitor usage and
performance.
- **[Token caching](./token-caching.md):** Optimize API costs by caching tokens.
+66 -56
View File
@@ -8,50 +8,52 @@ available combinations.
#### Basic Controls
| Action | Keys |
| --------------------------------------------------------------- | ---------- |
| Confirm the current selection or choice. | `Enter` |
| Dismiss dialogs or cancel the current focus. | `Esc` |
| Cancel the current request or quit the CLI when input is empty. | `Ctrl + C` |
| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
| Action | Keys |
| -------------------------------------------- | ------- |
| Confirm the current selection or choice. | `Enter` |
| Dismiss dialogs or cancel the current focus. | `Esc` |
#### Cursor Movement
| Action | Keys |
| ------------------------------------------- | ------------------------------------------------------------ |
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home (no Shift, Ctrl)` |
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End (no Shift, Ctrl)` |
| Move the cursor up one line. | `Up Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor down one line. | `Down Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + B` |
| Move the cursor one character to the right. | `Right Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + F` |
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home` |
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End` |
| Move the cursor one character to the left. | `Left Arrow (no Ctrl, no Cmd)`<br />`Ctrl + B` |
| Move the cursor one character to the right. | `Right Arrow (no Ctrl, no Cmd)`<br />`Ctrl + F` |
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Cmd + Left Arrow`<br />`Cmd + B` |
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Cmd + Right Arrow`<br />`Cmd + F` |
#### Editing
| Action | Keys |
| ------------------------------------------------ | --------------------------------------------------------- |
| Delete from the cursor to the end of the line. | `Ctrl + K` |
| Delete from the cursor to the start of the line. | `Ctrl + U` |
| Clear all text in the input field. | `Ctrl + C` |
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
| Action | Keys |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Delete from the cursor to the end of the line. | `Ctrl + K` |
| Delete from the cursor to the start of the line. | `Ctrl + U` |
| Clear all text in the input field. | `Ctrl + C` |
| Delete the previous word. | `Ctrl + Backspace`<br />`Cmd + Backspace`<br />`Ctrl + ""`<br />`Cmd + ""`<br />`Ctrl + W` |
| Delete the next word. | `Ctrl + Delete`<br />`Cmd + Delete` |
| Delete the character to the left. | `Backspace`<br />`""`<br />`Ctrl + H` |
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
| Redo the most recent undone text edit. | `Ctrl + Shift + Z` |
#### Screen Control
| Action | Keys |
| -------------------------------------------- | ---------- |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
#### Scrolling
| Action | Keys |
| ------------------------ | --------------------------------- |
| Scroll content up. | `Shift + Up Arrow` |
| Scroll content down. | `Shift + Down Arrow` |
| Scroll to the top. | `Ctrl + Home`<br />`Shift + Home` |
| Scroll to the bottom. | `Ctrl + End`<br />`Shift + End` |
| Scroll up by one page. | `Page Up` |
| Scroll down by one page. | `Page Down` |
| Action | Keys |
| ------------------------ | -------------------- |
| Scroll content up. | `Shift + Up Arrow` |
| Scroll content down. | `Shift + Down Arrow` |
| Scroll to the top. | `Home` |
| Scroll to the bottom. | `End` |
| Scroll up by one page. | `Page Up` |
| Scroll down by one page. | `Page Down` |
#### History & Search
@@ -62,7 +64,6 @@ available combinations.
| Start reverse search through history. | `Ctrl + R` |
| Submit the selected reverse-search match. | `Enter (no Ctrl)` |
| Accept a suggestion while reverse searching. | `Tab` |
| Browse and rewind previous interactions. | `Double Esc` |
#### Navigation
@@ -85,29 +86,39 @@ available combinations.
#### Text Input
| Action | Keys |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Submit the current prompt. | `Enter (no Shift, Alt, Ctrl, Cmd)` |
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Alt + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
| Open the current prompt in an external editor. | `Ctrl + X` |
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V`<br />`Alt + V` |
| Action | Keys |
| ------------------------------------ | ------------------------------------------------------------------------------------------- |
| Submit the current prompt. | `Enter (no Ctrl, no Shift, no Cmd, not Paste)` |
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Paste + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
#### External Tools
| Action | Keys |
| ---------------------------------------------- | ------------------------- |
| Open the current prompt in an external editor. | `Ctrl + X` |
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V` |
#### App Controls
| Action | Keys |
| ----------------------------------------------------------------------------------------------------- | ---------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Alt + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + S` |
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
| Focus the Gemini input from the shell input. | `Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
| Action | Keys |
| ----------------------------------------------------------------- | ---------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Cmd + M` |
| Toggle copy mode when the terminal is using the alternate buffer. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Toggle Auto Edit (auto-accept edits) mode. | `Shift + Tab` |
| Expand a height-constrained response to show additional lines. | `Ctrl + S` |
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
| Focus the Gemini input from the shell input. | `Tab` |
#### Session Control
| Action | Keys |
| -------------------------------------------- | ---------- |
| Cancel the current request or quit the CLI. | `Ctrl + C` |
| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
<!-- KEYBINDINGS-AUTOGEN:END -->
@@ -118,8 +129,7 @@ available combinations.
- `!` on an empty prompt: Enter or exit shell mode.
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
mode.
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
otherwise browse and rewind previous interactions.
- `Esc` pressed twice quickly: Clear the current input buffer.
- `Up Arrow` / `Down Arrow`: When the cursor is at the top or bottom of a
single-line input, navigate backward or forward through prompt history.
- `Number keys (1-9, multi-digit)` inside selection dialogs: Jump directly to
+1 -6
View File
@@ -11,17 +11,12 @@ health and automatically routes requests to available models based on defined
policies.
1. **Model failure:** If the currently selected model fails (e.g., due to quota
or server errors), the CLI will initiate the fallback process.
or server errors), the CLI will iniate the fallback process.
2. **User consent:** Depending on the failure and the model's policy, the CLI
may prompt you to switch to a fallback model (by default always prompts
you).
Some internal utility calls (such as prompt completion and classification)
use a silent fallback chain for `gemini-2.5-flash-lite` and will fall back
to `gemini-2.5-flash` and `gemini-2.5-pro` without prompting or changing the
configured model.
3. **Model switch:** If approved, or if the policy allows for silent fallback,
the CLI will use an available fallback model for the current turn or the
remainder of the session.
+1 -1
View File
@@ -11,7 +11,7 @@ Before using sandboxing, you need to install and set up the Gemini CLI:
npm install -g @google/gemini-cli
```
To verify the installation:
To verify the installation
```bash
gemini --version
+37 -35
View File
@@ -26,7 +26,7 @@ they appear in the UI.
| ------------------------------- | ---------------------------------- | ------------------------------------------------------------- | ------- |
| Preview Features (e.g., models) | `general.previewFeatures` | Enable preview features (e.g., preview models). | `false` |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Disable Auto Update | `general.disableAutoUpdate` | Disable automatic updates | `false` |
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
@@ -39,29 +39,29 @@ they appear in the UI.
### UI
| UI Label | Setting | Description | Default |
| ------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Use Full Width | `ui.useFullWidth` | Use the entire width of the terminal for output. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Use Full Width | `ui.useFullWidth` | Use the entire width of the terminal for output. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Disable Loading Phrases | `ui.accessibility.disableLoadingPhrases` | Disable loading phrases for accessibility | `false` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
### IDE
@@ -86,7 +86,7 @@ they appear in the UI.
| Respect .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
| Respect .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
| Enable Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
| Enable Fuzzy Search | `context.fileFiltering.enableFuzzySearch` | Enable fuzzy search when searching for files. | `true` |
| Disable Fuzzy Search | `context.fileFiltering.disableFuzzySearch` | Disable fuzzy search when searching for files. | `false` |
### Tools
@@ -99,7 +99,7 @@ they appear in the UI.
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `4000000` |
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `1000` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `false` |
### Security
@@ -113,16 +113,18 @@ they appear in the UI.
### Experimental
| UI Label | Setting | Description | Default |
| ---------------- | ---------------------------- | ----------------------------------------------------------------------------------- | ------- |
| Agent Skills | `experimental.skills` | Enable Agent Skills (experimental). | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| UI Label | Setting | Description | Default |
| ----------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| Agent Skills | `experimental.skills` | Enable Agent Skills (experimental). | `false` |
| Enable Codebase Investigator | `experimental.codebaseInvestigatorSettings.enabled` | Enable the Codebase Investigator agent. | `true` |
| Codebase Investigator Max Num Turns | `experimental.codebaseInvestigatorSettings.maxNumTurns` | Maximum number of turns for the Codebase Investigator agent. | `10` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
| Enable CLI Help Agent | `experimental.cliHelpAgentSettings.enabled` | Enable the CLI Help Agent. | `true` |
### HooksConfig
### Hooks
| UI Label | Setting | Description | Default |
| ------------------ | --------------------------- | ------------------------------------------------ | ------- |
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
| UI Label | Setting | Description | Default |
| ------------------ | --------------------- | ------------------------------------------------ | ------- |
| Hook Notifications | `hooks.notifications` | Show visual indicators when hooks are executing. | `true` |
<!-- SETTINGS-AUTOGEN:END -->
+8 -8
View File
@@ -13,7 +13,7 @@ discoverable capability.
## Overview
Unlike general context files ([`GEMINI.md`](./gemini-md.md)), which provide
persistent workspace-wide background, Skills represent **on-demand expertise**.
persistent project-wide background, Skills represent **on-demand expertise**.
This allows Gemini to maintain a vast library of specialized capabilities—such
as security auditing, cloud deployments, or codebase migrations—without
cluttering the model's immediate context window.
@@ -39,15 +39,15 @@ the full instructions and resources required to complete the task using the
Gemini CLI discovers skills from three primary locations:
1. **Workspace Skills** (`.gemini/skills/`): Workspace-specific skills that are
1. **Project Skills** (`.gemini/skills/`): Project-specific skills that are
typically committed to version control and shared with the team.
2. **User Skills** (`~/.gemini/skills/`): Personal skills available across all
your workspaces.
your projects.
3. **Extension Skills**: Skills bundled within installed
[extensions](../extensions/index.md).
**Precedence:** If multiple skills share the same name, higher-precedence
locations override lower ones: **Workspace > User > Extension**.
locations override lower ones: **Project > User > Extension**.
## Managing Skills
@@ -61,7 +61,7 @@ Use the `/skills` slash command to view and manage available expertise:
- `/skills reload`: Refreshes the list of discovered skills from all tiers.
_Note: `/skills disable` and `/skills enable` default to the `user` scope. Use
`--scope workspace` to manage workspace-specific settings._
`--scope project` to manage project-specific settings._
### From the Terminal
@@ -89,8 +89,8 @@ gemini skills uninstall my-expertise --scope workspace
# Enable a skill (globally)
gemini skills enable my-expertise
# Disable a skill. Can use --scope to specify workspace or user (defaults to workspace)
gemini skills disable my-expertise --scope workspace
# Disable a skill. Can use --scope to specify project or user (defaults to project)
gemini skills disable my-expertise --scope project
```
## Creating a Skill
@@ -147,7 +147,7 @@ You are an expert code reviewer. When reviewing code, follow this workflow:
1. **Analyze**: Review the staged changes or specific files provided. Ensure
that the changes are scoped properly and represent minimal changes required
to address the issue.
2. **Style**: Ensure code follows the workspace's conventions and idiomatic
2. **Style**: Ensure code follows the project's conventions and idiomatic
patterns as described in the `GEMINI.md` file.
3. **Security**: Flag any potential security vulnerabilities.
4. **Tests**: Verify that new logic has corresponding test coverage and that
-32
View File
@@ -56,38 +56,6 @@ error with: `missing system prompt file '<path>'`.
When `GEMINI_SYSTEM_MD` is active, the CLI shows a `|⌐■_■|` indicator in the UI
to signal custom systemprompt mode.
## Variable Substitution
When using a custom system prompt file, you can use the following variables to
dynamically include built-in content:
- `${AgentSkills}`: Injects a complete section (including header) of all
available agent skills.
- `${SubAgents}`: Injects a complete section (including header) of available
sub-agents.
- `${AvailableTools}`: Injects a bulleted list of all currently enabled tool
names.
- Tool Name Variables: Injects the actual name of a tool using the pattern:
`${toolName}_ToolName` (e.g., `${write_file_ToolName}`,
`${run_shell_command_ToolName}`).
This pattern is generated dynamically for all available tools.
### Example
```markdown
# Custom System Prompt
You are a helpful assistant. ${AgentSkills}
${SubAgents}
## Tooling
The following tools are available to you: ${AvailableTools}
You can use ${write_file_ToolName} to save logs.
```
## Export the default prompt (recommended)
Before overriding, export the current default prompt so you can review required
+3 -18
View File
@@ -18,7 +18,6 @@ Learn how to enable and setup OpenTelemetry for Gemini CLI.
- [Logs and metrics](#logs-and-metrics)
- [Logs](#logs)
- [Sessions](#sessions)
- [Approval Mode](#approval-mode)
- [Tools](#tools)
- [Files](#files)
- [API](#api)
@@ -225,11 +224,11 @@ visualize your telemetry.
This dashboard can be found under **Google Cloud Monitoring Dashboard
Templates** as "**Gemini CLI Monitoring**".
![Gemini CLI Monitoring Dashboard Overview](/docs/assets/monitoring-dashboard-overview.png)
![Gemini CLI Monitoring Dashboard Overview](../assets/monitoring-dashboard-overview.png)
![Gemini CLI Monitoring Dashboard Metrics](/docs/assets/monitoring-dashboard-metrics.png)
![Gemini CLI Monitoring Dashboard Metrics](../assets/monitoring-dashboard-metrics.png)
![Gemini CLI Monitoring Dashboard Logs](/docs/assets/monitoring-dashboard-logs.png)
![Gemini CLI Monitoring Dashboard Logs](../assets/monitoring-dashboard-logs.png)
To learn more, check out this blog post:
[Instant insights: Gemini CLIs new pre-configured monitoring dashboards](https://cloud.google.com/blog/topics/developers-practitioners/instant-insights-gemini-clis-new-pre-configured-monitoring-dashboards/).
@@ -316,20 +315,6 @@ Captures startup configuration and user prompt submissions.
- `prompt` (string; excluded if `telemetry.logPrompts` is `false`)
- `auth_type` (string)
#### Approval Mode
Tracks changes and duration of approval modes.
- `approval_mode_switch`: Approval mode was changed.
- **Attributes**:
- `from_mode` (string)
- `to_mode` (string)
- `approval_mode_duration`: Duration spent in an approval mode.
- **Attributes**:
- `mode` (string)
- `duration_ms` (int)
#### Tools
Captures tool executions, output truncation, and Edit behavior.
+2
View File
@@ -86,6 +86,7 @@ color keys. For example:
- `Gray`
- `DiffAdded` (optional, for added lines in diffs)
- `DiffRemoved` (optional, for removed lines in diffs)
- `DiffModified` (optional, for modified lines in diffs)
You can also override individual UI text roles by adding a nested `text` object.
This object supports the keys `primary`, `secondary`, `link`, `accent`, and
@@ -156,6 +157,7 @@ custom theme defined in `settings.json`.
"Gray": "#ABB2BF",
"DiffAdded": "#A6E3A1",
"DiffRemoved": "#F38BA8",
"DiffModified": "#89B4FA",
"GradientColors": ["#4796E4", "#847ACE", "#C3677F"]
}
```
-4
View File
@@ -68,10 +68,6 @@ If you are using the default "pro" model and the CLI detects that you are being
rate-limited, it automatically switches to the "flash" model for the current
session. This allows you to continue working without interruption.
Internal utility calls that use `gemini-2.5-flash-lite` (for example, prompt
completion and classification) silently fall back to `gemini-2.5-flash` and
`gemini-2.5-pro` when quota is exhausted, without changing the configured model.
## File discovery service
The file discovery service is responsible for finding files in the project that
+2 -39
View File
@@ -222,45 +222,9 @@ need this for extensions built to expose commands and prompts.
Restart the CLI again. The model will now have the context from your `GEMINI.md`
file in every session where the extension is active.
## (Optional) Step 6: Add an Agent Skill
## Step 6: Releasing your extension
_Note: This is an experimental feature enabled via `experimental.skills`._
[Agent Skills](../cli/skills.md) let you bundle specialized expertise and
procedural workflows. Unlike `GEMINI.md`, which provides persistent context,
skills are activated only when needed, saving context tokens.
1. Create a `skills` directory and a subdirectory for your skill:
```bash
mkdir -p skills/security-audit
```
2. Create a `skills/security-audit/SKILL.md` file:
```markdown
---
name: security-audit
description:
Expertise in auditing code for security vulnerabilities. Use when the user
asks to "check for security issues" or "audit" their changes.
---
# Security Auditor
You are an expert security researcher. When auditing code:
1. Look for common vulnerabilities (OWASP Top 10).
2. Check for hardcoded secrets or API keys.
3. Suggest remediation steps for any findings.
```
Skills bundled with your extension are automatically discovered and can be
activated by the model during a session when it identifies a relevant task.
## Step 7: Release your extension
Once you're happy with your extension, you can share it with others. The two
Once you are happy with your extension, you can share it with others. The two
primary ways of releasing extensions are via a Git repository or through GitHub
Releases. Using a public Git repository is the simplest method.
@@ -275,7 +239,6 @@ You've successfully created a Gemini CLI extension! You learned how to:
- Add custom tools with an MCP server.
- Create convenient custom commands.
- Provide persistent context to the model.
- Bundle specialized Agent Skills.
- Link your extension for local development.
From here, you can explore more advanced features and build powerful new
+4 -38
View File
@@ -2,10 +2,10 @@
_This documentation is up-to-date with the v0.4.0 release._
Gemini CLI extensions package prompts, MCP servers, Agent Skills, and custom
commands into a familiar and user-friendly format. With extensions, you can
expand the capabilities of Gemini CLI and share those capabilities with others.
They're designed to be easily installable and shareable.
Gemini CLI extensions package prompts, MCP servers, and custom commands into a
familiar and user-friendly format. With extensions, you can expand the
capabilities of Gemini CLI and share those capabilities with others. They are
designed to be easily installable and shareable.
To see examples of extensions, you can browse a gallery of
[Gemini CLI extensions](https://geminicli.com/extensions/browse/).
@@ -263,40 +263,6 @@ Would provide these commands:
- `/deploy` - Shows as `[gcp] Custom command from deploy.toml` in help
- `/gcs:sync` - Shows as `[gcp] Custom command from sync.toml` in help
### Agent Skills
_Note: This is an experimental feature enabled via `experimental.skills`._
Extensions can bundle [Agent Skills](../cli/skills.md) to provide on-demand
expertise and specialized workflows. To include skills in your extension, place
them in a `skills/` subdirectory within the extension directory. Each skill must
follow the [Agent Skills structure](../cli/skills.md#folder-structure),
including a `SKILL.md` file.
**Example**
An extension named `security-toolkit` with the following structure:
```
.gemini/extensions/security-toolkit/
├── gemini-extension.json
└── skills/
├── audit/
│ ├── SKILL.md
│ └── scripts/
│ └── scan.py
└── hardening/
└── SKILL.md
```
Upon installation, these skills will be discovered by Gemini CLI and can be
activated during a session when the model identifies a task matching their
descriptions.
Extension skills have the lowest precedence and will be overridden by user or
workspace skills of the same name. They can be viewed and managed (enabled or
disabled) using the [`/skills` command](../cli/skills.md#managing-skills).
### Hooks
Extensions can provide [hooks](../hooks/index.md) to intercept and customize
+51 -36
View File
@@ -110,13 +110,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Enable Vim keybindings
- **Default:** `false`
- **`general.enableAutoUpdate`** (boolean):
- **Description:** Enable automatic updates.
- **Default:** `true`
- **`general.disableAutoUpdate`** (boolean):
- **Description:** Disable automatic updates
- **Default:** `false`
- **`general.enableAutoUpdateNotification`** (boolean):
- **Description:** Enable update notification prompts.
- **Default:** `true`
- **`general.disableUpdateNag`** (boolean):
- **Description:** Disable update notification prompts.
- **Default:** `false`
- **`general.checkpointing.enabled`** (boolean):
- **Description:** Enable session checkpointing for recovery
@@ -266,9 +266,9 @@ their corresponding top-level category object in your `settings.json` file.
provided, the CLI cycles through these instead of the defaults.
- **Default:** `[]`
- **`ui.accessibility.enableLoadingPhrases`** (boolean):
- **Description:** Enable loading phrases during operations.
- **Default:** `true`
- **`ui.accessibility.disableLoadingPhrases`** (boolean):
- **Description:** Disable loading phrases for accessibility
- **Default:** `false`
- **Requires restart:** Yes
- **`ui.accessibility.screenReader`** (boolean):
@@ -607,9 +607,9 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`context.fileFiltering.enableFuzzySearch`** (boolean):
- **Description:** Enable fuzzy search when searching for files.
- **Default:** `true`
- **`context.fileFiltering.disableFuzzySearch`** (boolean):
- **Description:** Disable fuzzy search when searching for files.
- **Default:** `false`
- **Requires restart:** Yes
#### `tools`
@@ -707,7 +707,7 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Disable LLM-based error correction for edit tools. When
enabled, tools will fail immediately if exact string matches are not found,
instead of attempting to self-correct.
- **Default:** `true`
- **Default:** `false`
- **Requires restart:** Yes
- **`tools.enableHooks`** (boolean):
@@ -835,11 +835,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.enableEventDrivenScheduler`** (boolean):
- **Description:** Enables event-driven scheduler within the CLI session.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.extensionReloading`** (boolean):
- **Description:** Enables extension loading/unloading within the CLI session.
- **Default:** `false`
@@ -855,14 +850,41 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.enabled`** (boolean):
- **Description:** Enable the Codebase Investigator agent.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.maxNumTurns`** (number):
- **Description:** Maximum number of turns for the Codebase Investigator
agent.
- **Default:** `10`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.maxTimeMinutes`** (number):
- **Description:** Maximum time for the Codebase Investigator agent (in
minutes).
- **Default:** `3`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.thinkingBudget`** (number):
- **Description:** The thinking budget for the Codebase Investigator agent.
- **Default:** `8192`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.model`** (string):
- **Description:** The model to use for the Codebase Investigator agent.
- **Default:** `"auto"`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
(useful for remote sessions).
- **Default:** `false`
- **`experimental.plan`** (boolean):
- **Description:** Enable planning features (Plan Mode and tools).
- **Default:** `false`
- **`experimental.cliHelpAgentSettings.enabled`** (boolean):
- **Description:** Enable the CLI Help Agent.
- **Default:** `true`
- **Requires restart:** Yes
#### `skills`
@@ -872,24 +894,22 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `[]`
- **Requires restart:** Yes
#### `hooksConfig`
#### `hooks`
- **`hooksConfig.enabled`** (boolean):
- **`hooks.enabled`** (boolean):
- **Description:** Canonical toggle for the hooks system. When disabled, no
hooks will be executed.
- **Default:** `true`
- **Default:** `false`
- **`hooksConfig.disabled`** (array):
- **`hooks.disabled`** (array):
- **Description:** List of hook names (commands) that should be disabled.
Hooks in this list will not execute even if configured.
- **Default:** `[]`
- **`hooksConfig.notifications`** (boolean):
- **`hooks.notifications`** (boolean):
- **Description:** Show visual indicators when hooks are executing.
- **Default:** `true`
#### `hooks`
- **`hooks.BeforeTool`** (array):
- **Description:** Hooks that execute before tool execution. Can intercept,
validate, or modify tool calls.
@@ -1140,7 +1160,7 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GEMINI_MODEL`**:
- Specifies the default Gemini model to use.
- Overrides the hardcoded default
- Example: `export GEMINI_MODEL="gemini-3-flash-preview"`
- Example: `export GEMINI_MODEL="gemini-2.5-flash"`
- **`GOOGLE_API_KEY`**:
- Your Google Cloud API key.
- Required for using Vertex AI in express mode.
@@ -1285,7 +1305,7 @@ for that specific session.
- **`--model <model_name>`** (**`-m <model_name>`**):
- Specifies the Gemini model to use for this session.
- Example: `npm start -- --model gemini-3-pro-preview`
- Example: `npm start -- --model gemini-1.5-pro-latest`
- **`--prompt <your_prompt>`** (**`-p <your_prompt>`**):
- Used to pass a prompt directly to the command. This invokes Gemini CLI in a
non-interactive mode.
@@ -1308,8 +1328,7 @@ for that specific session.
- **`--sandbox`** (**`-s`**):
- Enables sandbox mode for this session.
- **`--debug`** (**`-d`**):
- Enables debug mode for this session, providing more verbose output. Open the
debug console with F12 to see the additional logging.
- Enables debug mode for this session, providing more verbose output.
- **`--help`** (or **`-h`**):
- Displays help information about command-line arguments.
@@ -1321,10 +1340,6 @@ for that specific session.
- `auto_edit`: Automatically approve edit tools (replace, write_file) while
prompting for others
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
- `plan`: Read-only mode for tool calls (requires experimental planning to
be enabled).
> **Note:** This mode is currently under development and not yet fully
> functional.
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of
`--yolo` for the new unified approach.
- Example: `gemini --approval-mode auto_edit`
+431 -250
View File
@@ -1,4 +1,4 @@
# Hooks Best Practices
# Hooks on Gemini CLI: Best practices
This guide covers security considerations, performance optimization, debugging
techniques, and privacy considerations for developing and deploying hooks in
@@ -15,20 +15,21 @@ using parallel operations:
// Sequential operations are slower
const data1 = await fetch(url1).then((r) => r.json());
const data2 = await fetch(url2).then((r) => r.json());
const data3 = await fetch(url3).then((r) => r.json());
// Prefer parallel operations for better performance
// Start requests concurrently
const p1 = fetch(url1).then((r) => r.json());
const p2 = fetch(url2).then((r) => r.json());
const p3 = fetch(url3).then((r) => r.json());
// Wait for all results
const [data1, data2] = await Promise.all([p1, p2]);
const [data1, data2, data3] = await Promise.all([p1, p2, p3]);
```
### Cache expensive operations
Store results between invocations to avoid repeated computation, especially for
hooks that run frequently (like `BeforeTool` or `AfterModel`).
Store results between invocations to avoid repeated computation:
```javascript
const fs = require('fs');
@@ -53,7 +54,6 @@ async function main() {
const cacheKey = `tool-list-${(Date.now() / 3600000) | 0}`; // Hourly cache
if (cache[cacheKey]) {
// Write JSON to stdout
console.log(JSON.stringify(cache[cacheKey]));
return;
}
@@ -70,20 +70,32 @@ async function main() {
### Use appropriate events
Choose hook events that match your use case to avoid unnecessary execution.
`AfterAgent` fires once per agent loop completion, while `AfterModel` fires
after every LLM call (potentially multiple times per loop):
- **`AfterAgent`**: Fires **once** per turn after the model finishes its final
response. Use this for quality validation (Retries) or final logging.
- **`AfterModel`**: Fires after **every chunk** of LLM output. Use this for
real-time redaction, PII filtering, or monitoring output as it streams.
If you only need to check the final completion, use `AfterAgent` to save
performance.
```json
// If checking final completion, use AfterAgent instead of AfterModel
{
"hooks": {
"AfterAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "final-checker",
"command": "./check-completion.sh"
}
]
}
]
}
}
```
### Filter with matchers
Use specific matchers to avoid unnecessary hook execution. Instead of matching
all tools with `*`, specify only the tools you need. This saves the overhead of
spawning a process for irrelevant events.
all tools with `*`, specify only the tools you need:
```json
{
@@ -99,32 +111,30 @@ spawning a process for irrelevant events.
### Optimize JSON parsing
For large inputs (like `AfterModel` receiving a large context), standard JSON
parsing can be slow. If you only need one field, consider streaming parsers or
lightweight extraction logic, though for most shell scripts `jq` is sufficient.
For large inputs, use streaming JSON parsers to avoid loading everything into
memory:
```javascript
// Standard approach: parse entire input
const input = JSON.parse(await readStdin());
const content = input.tool_input.content;
// For very large inputs: stream and extract only needed fields
const { createReadStream } = require('fs');
const JSONStream = require('JSONStream');
const stream = createReadStream(0).pipe(JSONStream.parse('tool_input.content'));
let content = '';
stream.on('data', (chunk) => {
content += chunk;
});
```
## Debugging
### The "Strict JSON" rule
The most common cause of hook failure is "polluting" the standard output.
- **stdout** is for **JSON only**.
- **stderr** is for **logs and text**.
**Good:**
```bash
#!/bin/bash
echo "Starting check..." >&2 # <--- Redirect to stderr
echo '{"decision": "allow"}'
```
### Log to files
Since hooks run in the background, writing to a dedicated log file is often the
easiest way to debug complex logic.
Write debug information to dedicated log files:
```bash
#!/usr/bin/env bash
@@ -141,9 +151,6 @@ log "Received input: ${input:0:100}..."
# Hook logic here
log "Hook completed successfully"
# Always output valid JSON to stdout at the end, even if just empty
echo "{}"
```
### Use stderr for errors
@@ -155,7 +162,6 @@ try {
const result = dangerousOperation();
console.log(JSON.stringify({ result }));
} catch (error) {
// Write the error description to stderr so the user/agent sees it
console.error(`Hook error: ${error.message}`);
process.exit(2); // Blocking error
}
@@ -163,8 +169,7 @@ try {
### Test hooks independently
Run hook scripts manually with sample JSON input to verify they behave as
expected before hooking them up to the CLI.
Run hook scripts manually with sample JSON input:
```bash
# Create test input
@@ -186,46 +191,33 @@ cat test-input.json | .gemini/hooks/my-hook.sh
# Check exit code
echo "Exit code: $?"
```
### Check exit codes
Gemini CLI uses exit codes for high-level flow control:
- **Exit 0 (Success)**: The hook ran successfully. The CLI parses `stdout` for
JSON decisions.
- **Exit 2 (System Block)**: A critical block occurred. `stderr` is used as the
reason.
- For **Agent/Model** events, this aborts the turn.
- For **Tool** events, this blocks the tool but allows the agent to continue.
- For **AfterAgent**, this triggers an automatic retry turn.
> **TIP**
>
> **Blocking vs. Stopping**: Use `decision: "deny"` (or Exit Code 2) to block a
> **specific action**. Use `{"continue": false}` in your JSON output to **kill
> the entire agent loop** immediately.
Ensure your script returns the correct exit code:
```bash
#!/usr/bin/env bash
set -e
set -e # Exit on error
# Hook logic
process_input() {
# ...
}
if process_input; then
echo '{"decision": "allow"}'
echo "Success message"
exit 0
else
echo "Critical validation failure" >&2
echo "Error message" >&2
exit 2
fi
```
### Enable telemetry
Hook execution is logged when `telemetry.logPrompts` is enabled. You can view
these logs to debug execution flow.
Hook execution is logged when `telemetry.logPrompts` is enabled:
```json
{
@@ -235,10 +227,11 @@ these logs to debug execution flow.
}
```
View hook telemetry in logs to debug execution issues.
### Use hook panel
The `/hooks panel` command inside the CLI shows execution status and recent
output:
The `/hooks panel` command shows execution status and recent output:
```bash
/hooks panel
@@ -262,64 +255,18 @@ Begin with basic logging hooks before implementing complex logic:
# Simple logging hook to understand input structure
input=$(cat)
echo "$input" >> .gemini/hook-inputs.log
# Always return valid JSON
echo "{}"
```
### Documenting your hooks
Maintainability is critical for complex hook systems. Use descriptions and
comments to help yourself and others understand why a hook exists.
**Use the `description` field**: This text is displayed in the `/hooks panel` UI
and helps diagnose issues.
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
{
"name": "secret-scanner",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
"description": "Scans code changes for API keys and secrets before writing"
}
]
}
]
}
}
```
**Add comments in hook scripts**: Explain performance expectations and
dependencies.
```javascript
#!/usr/bin/env node
/**
* RAG Tool Filter Hook
*
* Reduces the tool space by extracting keywords from the user's request.
*
* Performance: ~500ms average
* Dependencies: @google/generative-ai
*/
echo "Logged input"
```
### Use JSON libraries
Parse JSON with proper libraries instead of text processing.
Parse JSON with proper libraries instead of text processing:
**Bad:**
```bash
# Fragile text parsing
tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')
```
**Good:**
@@ -327,7 +274,6 @@ tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')
```bash
# Robust JSON parsing
tool_name=$(echo "$input" | jq -r '.tool_name')
```
### Make scripts executable
@@ -337,7 +283,6 @@ Always make hook scripts executable:
```bash
chmod +x .gemini/hooks/*.sh
chmod +x .gemini/hooks/*.js
```
### Version control
@@ -347,7 +292,7 @@ Commit hooks to share with your team:
```bash
git add .gemini/hooks/
git add .gemini/settings.json
git commit -m "Add project hooks for security and testing"
```
**`.gitignore` considerations:**
@@ -361,10 +306,295 @@ git add .gemini/settings.json
# Keep hook scripts
!.gemini/hooks/*.sh
!.gemini/hooks/*.js
```
## Hook security
### Document behavior
Add descriptions to help others understand your hooks:
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
{
"name": "secret-scanner",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
"description": "Scans code changes for API keys, passwords, and other secrets before writing"
}
]
}
]
}
}
```
Add comments in hook scripts:
```javascript
#!/usr/bin/env node
/**
* RAG Tool Filter Hook
*
* This hook reduces the tool space from 100+ tools to ~15 relevant ones
* by extracting keywords from the user's request and filtering tools
* based on semantic similarity.
*
* Performance: ~500ms average, cached tool embeddings
* Dependencies: @google/generative-ai
*/
```
## Troubleshooting
### Hook not executing
**Check hook name in `/hooks panel`:**
```bash
/hooks panel
```
Verify the hook appears in the list and is enabled.
**Verify matcher pattern:**
```bash
# Test regex pattern
echo "write_file|replace" | grep -E "write_.*|replace"
```
**Check disabled list:**
```json
{
"hooks": {
"disabled": ["my-hook-name"]
}
}
```
**Ensure script is executable:**
```bash
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh
```
**Verify script path:**
```bash
# Check path expansion
echo "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file exists
test -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "File exists"
```
### Hook timing out
**Check configured timeout:**
```json
{
"name": "slow-hook",
"timeout": 60000
}
```
**Optimize slow operations:**
```javascript
// Before: Sequential operations (slow)
for (const item of items) {
await processItem(item);
}
// After: Parallel operations (fast)
await Promise.all(items.map((item) => processItem(item)));
```
**Use caching:**
```javascript
const cache = new Map();
async function getCachedData(key) {
if (cache.has(key)) {
return cache.get(key);
}
const data = await fetchData(key);
cache.set(key, data);
return data;
}
```
**Consider splitting into multiple faster hooks:**
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file",
"hooks": [
{
"name": "quick-check",
"command": "./quick-validation.sh",
"timeout": 1000
}
]
},
{
"matcher": "write_file",
"hooks": [
{
"name": "deep-check",
"command": "./deep-analysis.sh",
"timeout": 30000
}
]
}
]
}
}
```
### Invalid JSON output
**Validate JSON before outputting:**
```bash
#!/usr/bin/env bash
output='{"decision": "allow"}'
# Validate JSON
if echo "$output" | jq empty 2>/dev/null; then
echo "$output"
else
echo "Invalid JSON generated" >&2
exit 1
fi
```
**Ensure proper quoting and escaping:**
```javascript
// Bad: Unescaped string interpolation
const message = `User said: ${userInput}`;
console.log(JSON.stringify({ message }));
// Good: Automatic escaping
console.log(JSON.stringify({ message: `User said: ${userInput}` }));
```
**Check for binary data or control characters:**
```javascript
function sanitizeForJSON(str) {
return str.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); // Remove control chars
}
const cleanContent = sanitizeForJSON(content);
console.log(JSON.stringify({ content: cleanContent }));
```
### Exit code issues
**Verify script returns correct codes:**
```bash
#!/usr/bin/env bash
set -e # Exit on error
# Processing logic
if validate_input; then
echo "Success"
exit 0
else
echo "Validation failed" >&2
exit 2
fi
```
**Check for unintended errors:**
```bash
#!/usr/bin/env bash
# Don't use 'set -e' if you want to handle errors explicitly
# set -e
if ! command_that_might_fail; then
# Handle error
echo "Command failed but continuing" >&2
fi
# Always exit explicitly
exit 0
```
**Use trap for cleanup:**
```bash
#!/usr/bin/env bash
cleanup() {
# Cleanup logic
rm -f /tmp/hook-temp-*
}
trap cleanup EXIT
# Hook logic here
```
### Environment variables not available
**Check if variable is set:**
```bash
#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then
echo "GEMINI_PROJECT_DIR not set" >&2
exit 1
fi
if [ -z "$CUSTOM_VAR" ]; then
echo "Warning: CUSTOM_VAR not set, using default" >&2
CUSTOM_VAR="default-value"
fi
```
**Debug available variables:**
```bash
#!/usr/bin/env bash
# List all environment variables
env > .gemini/hook-env.log
# Check specific variables
echo "GEMINI_PROJECT_DIR: $GEMINI_PROJECT_DIR" >> .gemini/hook-env.log
echo "GEMINI_SESSION_ID: $GEMINI_SESSION_ID" >> .gemini/hook-env.log
echo "GEMINI_API_KEY: ${GEMINI_API_KEY:+<set>}" >> .gemini/hook-env.log
```
**Use .env files:**
```bash
#!/usr/bin/env bash
# Load .env file if it exists
if [ -f "$GEMINI_PROJECT_DIR/.env" ]; then
source "$GEMINI_PROJECT_DIR/.env"
fi
```
## Using Hooks Securely
### Threat Model
@@ -391,10 +621,11 @@ When you open a project with hooks defined in `.gemini/settings.json`:
it).
5. **Trust**: The hook is marked as "trusted" for this project.
> **Modification detection**: If the `command` string of a project hook is
> changed (e.g., by a `git pull`), its identity changes. Gemini CLI will treat
> it as a **new, untrusted hook** and warn you again. This prevents malicious
> actors from silently swapping a verified command for a malicious one.
> [!IMPORTANT] **Modification Detection**: If the `command` string of a project
> hook is changed (e.g., by a `git pull`), its identity changes. Gemini CLI will
> treat it as a **new, untrusted hook** and warn you again. This prevents
> malicious actors from silently swapping a verified command for a malicious
> one.
### Risks
@@ -415,134 +646,32 @@ When you open a project with hooks defined in `.gemini/settings.json`:
publishers, well-known community members).
- Be cautious with obfuscated scripts or compiled binaries from unknown sources.
#### Sanitize environment
#### Sanitize Environment
Hooks inherit the environment of the Gemini CLI process, which may include
sensitive API keys. Gemini CLI provides a
[redaction system](/docs/get-started/configuration#environment-variable-redaction)
that automatically filters variables matching sensitive patterns (e.g., `KEY`,
`TOKEN`).
sensitive API keys. Gemini CLI attempts to sanitize sensitive variables, but you
should be cautious.
> **Disabled by Default**: Environment redaction is currently **OFF by
> default**. We strongly recommend enabling it if you are running third-party
> hooks or working in sensitive environments.
- **Avoid printing environment variables** to stdout/stderr unless necessary.
- **Use `.env` files** to securely manage sensitive variables, ensuring they are
excluded from version control.
**Impact on hooks:**
- **Security**: Prevents your hook scripts from accidentally leaking secrets.
- **Troubleshooting**: If your hook depends on a specific environment variable
that is being blocked, you must explicitly allow it in `settings.json`.
**System Administrators:** You can enforce environment variable redaction by
default in the system configuration (e.g., `/etc/gemini-cli/settings.json`):
```json
{
"security": {
"environmentVariableRedaction": {
"enabled": true,
"allowed": ["MY_REQUIRED_TOOL_KEY"]
"blocked": ["MY_SECRET_KEY"],
"allowed": ["SAFE_VAR"]
}
}
}
```
**System administrators:** You can enforce redaction for all users in the system
configuration.
## Troubleshooting
### Hook not executing
**Check hook name in `/hooks panel`:** Verify the hook appears in the list and
is enabled.
**Verify matcher pattern:**
```bash
# Test regex pattern
echo "write_file|replace" | grep -E "write_.*|replace"
```
**Check disabled list:** Verify the hook is not listed in your `settings.json`:
```json
{
"hooks": {
"disabled": ["my-hook-name"]
}
}
```
**Ensure script is executable**: For macOS and Linux users, verify the script
has execution permissions:
```bash
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh
```
**Verify script path:** Ensure the path in `settings.json` resolves correctly.
```bash
# Check path expansion
echo "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file exists
test -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "File exists"
```
### Hook timing out
**Check configured timeout:** The default is 60000ms (1 minute). You can
increase this in `settings.json`:
```json
{
"name": "slow-hook",
"timeout": 120000
}
```
**Optimize slow operations:** Move heavy processing to background tasks or use
caching.
### Invalid JSON output
**Validate JSON before outputting:**
```bash
#!/usr/bin/env bash
output='{"decision": "allow"}'
# Validate JSON
if echo "$output" | jq empty 2>/dev/null; then
echo "$output"
else
echo "Invalid JSON generated" >&2
exit 1
fi
```
### Environment variables not available
**Check if variable is set:**
```bash
#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then
echo "GEMINI_PROJECT_DIR not set" >&2
exit 1
fi
```
**Debug available variables:**
```bash
env > .gemini/hook-env.log
```
## Authoring secure hooks
## Authoring Secure Hooks
When writing your own hooks, follow these practices to ensure they are robust
and secure.
@@ -637,17 +766,40 @@ function containsSecret(content) {
## Privacy considerations
Hook inputs and outputs may contain sensitive information.
Hook inputs and outputs may contain sensitive information. Gemini CLI respects
the `telemetry.logPrompts` setting for hook data logging.
### What data is collected
Hook telemetry may include inputs (prompts, code) and outputs (decisions,
reasons) unless disabled.
Hook telemetry may include:
- **Hook inputs:** User prompts, tool arguments, file contents
- **Hook outputs:** Hook responses, decision reasons, added context
- **Standard streams:** stdout and stderr from hook processes
- **Execution metadata:** Hook name, event type, duration, success/failure
### Privacy settings
**Disable PII logging:** If you are working with sensitive data, disable prompt
logging in your settings:
**Enabled (default):**
Full hook I/O is logged to telemetry. Use this when:
- Developing and debugging hooks
- Telemetry is redirected to a trusted enterprise system
- You understand and accept the privacy implications
**Disabled:**
Only metadata is logged (event name, duration, success/failure). Hook inputs and
outputs are excluded. Use this when:
- Sending telemetry to third-party systems
- Working with sensitive data
- Privacy regulations require minimizing data collection
### Configuration
**Disable PII logging in settings:**
```json
{
@@ -657,19 +809,48 @@ logging in your settings:
}
```
**Suppress Output:** Individual hooks can request their metadata be hidden from
logs and telemetry by returning `"suppressOutput": true` in their JSON response.
**Disable via environment variable:**
> **Note**
> `suppressOutput` only affects background logging. Any `systemMessage` or
> `reason` included in the JSON will still be displayed to the user in the
> terminal.
```bash
export GEMINI_TELEMETRY_LOG_PROMPTS=false
```
### Sensitive data in hooks
If your hooks process sensitive data:
1. **Minimize logging:** Don't write sensitive data to log files.
2. **Sanitize outputs:** Remove sensitive data before outputting JSON or writing
to stderr.
1. **Minimize logging:** Don't write sensitive data to log files
2. **Sanitize outputs:** Remove sensitive data before outputting
3. **Use secure storage:** Encrypt sensitive data at rest
4. **Limit access:** Restrict hook script permissions
**Example sanitization:**
```javascript
function sanitizeOutput(data) {
const sanitized = { ...data };
// Remove sensitive fields
delete sanitized.apiKey;
delete sanitized.password;
// Redact sensitive strings
if (sanitized.content) {
sanitized.content = sanitized.content.replace(
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/gi,
'[REDACTED]',
);
}
return sanitized;
}
console.log(JSON.stringify(sanitizeOutput(hookOutput)));
```
## Learn more
- [Hooks Reference](index.md) - Complete API reference
- [Writing Hooks](writing-hooks.md) - Tutorial and examples
- [Configuration](../get-started/configuration.md) - Gemini CLI settings
- [Hooks Design Document](../hooks-design.md) - Technical architecture
+671 -116
View File
@@ -1,119 +1,112 @@
# Gemini CLI hooks (experimental)
# Gemini CLI hooks
Hooks are scripts or programs that Gemini CLI executes at specific points in the
agentic loop, allowing you to intercept and customize behavior without modifying
the CLI's source code.
## Availability
> **Note: Hooks are currently an experimental feature.**
>
> To use hooks, you must explicitly enable them in your `settings.json`:
>
> ```json
> {
> "tools": { "enableHooks": true },
> "hooks": { "enabled": true }
> }
> ```
>
> Both of these are needed in this experimental phase.
> **Experimental Feature**: Hooks are currently enabled by default only in the
> **Preview** and **Nightly** release channels.
See [writing hooks guide](writing-hooks.md) for a tutorial on creating your
first hook and a comprehensive example.
If you are on the Stable channel, you must explicitly enable the hooks system in
your `settings.json`:
See [hooks reference](reference.md) for the technical specification of the I/O
schemas.
```json
{
"hooksConfig": {
"enabled": true
}
}
```
- **[Writing hooks guide](/docs/hooks/writing-hooks)**: A tutorial on creating
your first hook with comprehensive examples.
- **[Hooks reference](/docs/hooks/reference)**: The definitive technical
specification of I/O schemas and exit codes.
- **[Best practices](/docs/hooks/best-practices)**: Guidelines on security,
performance, and debugging.
See [best practices](best-practices.md) for guidelines on security, performance,
and debugging.
## What are hooks?
With hooks, you can:
- **Add context:** Inject relevant information before the model processes a
request
- **Validate actions:** Review and block potentially dangerous operations
- **Enforce policies:** Implement security and compliance requirements
- **Log interactions:** Track tool usage and model responses
- **Optimize behavior:** Dynamically adjust tool selection or model parameters
Hooks run synchronously as part of the agent loop—when a hook event fires,
Gemini CLI waits for all matching hooks to complete before continuing.
With hooks, you can:
## Security and Risks
- **Add context:** Inject relevant information (like git history) before the
model processes a request.
- **Validate actions:** Review tool arguments and block potentially dangerous
operations.
- **Enforce policies:** Implement security scanners and compliance checks.
- **Log interactions:** Track tool usage and model responses for auditing.
- **Optimize behavior:** Dynamically filter available tools or adjust model
parameters.
> **Warning: Hooks execute arbitrary code with your user privileges.**
>
> By configuring hooks, you are explicitly allowing Gemini CLI to run shell
> commands on your machine. Malicious or poorly configured hooks can:
- **Exfiltrate data**: Read sensitive files (`.env`, ssh keys) and send them to
remote servers.
- **Modify system**: Delete files, install malware, or change system settings.
- **Consume resources**: Run infinite loops or crash your system.
**Project-level hooks** (in `.gemini/settings.json`) and **Extension hooks** are
particularly risky when opening third-party projects or extensions from
untrusted authors. Gemini CLI will **warn you** the first time it detects a new
project hook (identified by its name and command), but it is **your
responsibility** to review these hooks (and any installed extensions) before
trusting them.
> **Note:** Extension hooks are subject to a mandatory security warning and
> consent flow during extension installation or update if hooks are detected.
> You must explicitly approve the installation or update of any extension that
> contains hooks.
See [Security Considerations](best-practices.md#using-hooks-securely) for a
detailed threat model and mitigation strategies.
## Core concepts
### Hook events
Hooks are triggered by specific events in Gemini CLI's lifecycle.
Hooks are triggered by specific events in Gemini CLI's lifecycle. The following
table lists all available hook events:
| Event | When It Fires | Impact | Common Use Cases |
| --------------------- | ---------------------------------------------- | ---------------------- | -------------------------------------------- |
| `SessionStart` | When a session begins (startup, resume, clear) | Inject Context | Initialize resources, load context |
| `SessionEnd` | When a session ends (exit, clear) | Advisory | Clean up, save state |
| `BeforeAgent` | After user submits prompt, before planning | Block Turn / Context | Add context, validate prompts, block turns |
| `AfterAgent` | When agent loop ends | Retry / Halt | Review output, force retry or halt execution |
| `BeforeModel` | Before sending request to LLM | Block Turn / Mock | Modify prompts, swap models, mock responses |
| `AfterModel` | After receiving LLM response | Block Turn / Redact | Filter/redact responses, log interactions |
| `BeforeToolSelection` | Before LLM selects tools | Filter Tools | Filter available tools, optimize selection |
| `BeforeTool` | Before a tool executes | Block Tool / Rewrite | Validate arguments, block dangerous ops |
| `AfterTool` | After a tool executes | Block Result / Context | Process results, run tests, hide results |
| `PreCompress` | Before context compression | Advisory | Save state, notify user |
| `Notification` | When a system notification occurs | Advisory | Forward to desktop alerts, logging |
| Event | When It Fires | Common Use Cases |
| --------------------- | --------------------------------------------- | ------------------------------------------ |
| `SessionStart` | When a session begins | Initialize resources, load context |
| `SessionEnd` | When a session ends | Clean up, save state |
| `BeforeAgent` | After user submits prompt, before planning | Add context, validate prompts |
| `AfterAgent` | When agent loop ends | Review output, force continuation |
| `BeforeModel` | Before sending request to LLM | Modify prompts, add instructions |
| `AfterModel` | After receiving LLM response | Filter responses, log interactions |
| `BeforeToolSelection` | Before LLM selects tools (after BeforeModel) | Filter available tools, optimize selection |
| `BeforeTool` | Before a tool executes | Validate arguments, block dangerous ops |
| `AfterTool` | After a tool executes | Process results, run tests |
| `PreCompress` | Before context compression | Save state, notify user |
| `Notification` | When a notification occurs (e.g., permission) | Auto-approve, log decisions |
### Global mechanics
### Hook types
Understanding these core principles is essential for building robust hooks.
Gemini CLI currently supports **command hooks** that run shell commands or
scripts:
#### Strict JSON requirements (The "Golden Rule")
```json
{
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh",
"timeout": 30000
}
```
Hooks communicate via `stdin` (Input) and `stdout` (Output).
**Note:** Plugin hooks (npm packages) are planned for a future release.
1. **Silence is Mandatory**: Your script **must not** print any plain text to
`stdout` other than the final JSON object. **Even a single `echo` or `print`
call before the JSON will break parsing.**
2. **Pollution = Failure**: If `stdout` contains non-JSON text, parsing will
fail. The CLI will default to "Allow" and treat the entire output as a
`systemMessage`.
3. **Debug via Stderr**: Use `stderr` for **all** logging and debugging (e.g.,
`echo "debug" >&2`). Gemini CLI captures `stderr` but never attempts to parse
it as JSON.
### Matchers
#### Exit codes
Gemini CLI uses exit codes to determine the high-level outcome of a hook
execution:
| Exit Code | Label | Behavioral Impact |
| --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **0** | **Success** | The `stdout` is parsed as JSON. **Preferred code** for all logic, including intentional blocks (e.g., `{"decision": "deny"}`). |
| **2** | **System Block** | **Critical Block**. The target action (tool, turn, or stop) is aborted. `stderr` is used as the rejection reason. High severity; used for security stops or script failures. |
| **Other** | **Warning** | Non-fatal failure. A warning is shown, but the interaction proceeds using original parameters. |
#### Matchers
You can filter which specific tools or triggers fire your hook using the
`matcher` field.
- **Tool events** (`BeforeTool`, `AfterTool`): Matchers are **Regular
Expressions**. (e.g., `"write_.*"`).
- **Lifecycle events**: Matchers are **Exact Strings**. (e.g., `"startup"`).
- **Wildcards**: `"*"` or `""` (empty string) matches all occurrences.
## Configuration
Hook definitions are configured in `settings.json`. Gemini CLI merges
configurations from multiple layers in the following order of precedence
(highest to lowest):
1. **Project settings**: `.gemini/settings.json` in the current directory.
2. **User settings**: `~/.gemini/settings.json`.
3. **System settings**: `/etc/gemini-cli/settings.json`.
4. **Extensions**: Hooks defined by installed extensions.
### Configuration schema
For tool-related events (`BeforeTool`, `AfterTool`), you can filter which tools
trigger the hook:
```json
{
@@ -121,13 +114,382 @@ configurations from multiple layers in the following order of precedence
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
/* hooks for write operations */
]
}
]
}
}
```
**Matcher patterns:**
- **Exact match:** `"read_file"` matches only `read_file`
- **Regex:** `"write_.*|replace"` matches `write_file`, `replace`
- **Wildcard:** `"*"` or `""` matches all tools
**Session event matchers:**
- **SessionStart:** `startup`, `resume`, `clear`
- **SessionEnd:** `exit`, `clear`, `logout`, `prompt_input_exit`
- **PreCompress:** `manual`, `auto`
- **Notification:** `ToolPermission`
## Hook input/output contract
### Command hook communication
Hooks communicate via:
- **Input:** JSON on stdin
- **Output:** Exit code + stdout/stderr
### Exit codes
- **0:** Success - stdout shown to user (or injected as context for some events)
- **2:** Blocking error - stderr shown to agent/user, operation may be blocked
- **Other:** Non-blocking warning - logged but execution continues
### Common input fields
Every hook receives these base fields:
```json
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/path/to/project",
"hook_event_name": "BeforeTool",
"timestamp": "2025-12-01T10:30:00Z"
// ... event-specific fields
}
```
### Event-specific fields
#### BeforeTool
**Input:**
```json
{
"tool_name": "write_file",
"tool_input": {
"file_path": "/path/to/file.ts",
"content": "..."
}
}
```
**Output (JSON on stdout):**
```json
{
"decision": "allow|deny|ask|block",
"reason": "Explanation shown to agent",
"systemMessage": "Message shown to user"
}
```
Or simple exit codes:
- Exit 0 = allow (stdout shown to user)
- Exit 2 = deny (stderr shown to agent)
#### AfterTool
**Input:**
```json
{
"tool_name": "read_file",
"tool_input": { "file_path": "..." },
"tool_response": "file contents..."
}
```
**Output:**
```json
{
"decision": "allow|deny",
"hookSpecificOutput": {
"hookEventName": "AfterTool",
"additionalContext": "Extra context for agent"
}
}
```
#### BeforeAgent
**Input:**
```json
{
"prompt": "Fix the authentication bug"
}
```
**Output:**
```json
{
"decision": "allow|deny",
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "Recent project decisions: ..."
}
}
```
#### BeforeModel
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [{ "role": "user", "content": "Hello" }],
"config": { "temperature": 0.7 },
"toolConfig": {
"functionCallingConfig": {
"mode": "AUTO",
"allowedFunctionNames": ["read_file", "write_file"]
}
}
}
}
```
**Output:**
```json
{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeModel",
"llm_request": {
"messages": [
{ "role": "system", "content": "Additional instructions..." },
{ "role": "user", "content": "Hello" }
]
}
}
}
```
#### AfterModel
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [
/* ... */
],
"config": {
/* ... */
},
"toolConfig": {
/* ... */
}
},
"llm_response": {
"text": "string",
"candidates": [
{
"content": {
"role": "model",
"parts": ["array of content parts"]
},
"finishReason": "STOP"
}
]
}
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "AfterModel",
"llm_response": {
"candidate": {
/* modified response */
}
}
}
}
```
#### BeforeToolSelection
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [
/* ... */
],
"toolConfig": {
"functionCallingConfig": {
"mode": "AUTO",
"allowedFunctionNames": [
/* 100+ tools */
]
}
}
}
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "BeforeToolSelection",
"toolConfig": {
"functionCallingConfig": {
"mode": "ANY",
"allowedFunctionNames": ["read_file", "write_file", "replace"]
}
}
}
}
```
Or simple output (comma-separated tool names sets mode to ANY):
```bash
echo "read_file,write_file,replace"
```
#### SessionStart
**Input:**
```json
{
"source": "startup|resume|clear"
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Loaded 5 project memories"
}
}
```
#### SessionEnd
**Input:**
```json
{
"reason": "exit|clear|logout|prompt_input_exit|other"
}
```
No structured output expected (but stdout/stderr logged).
#### PreCompress
**Input:**
```json
{
"trigger": "manual|auto"
}
```
**Output:**
```json
{
"systemMessage": "Compression starting..."
}
```
#### Notification
**Input:**
```json
{
"notification_type": "ToolPermission",
"message": "string",
"details": {
/* notification details */
}
}
```
**Output:**
```json
{
"systemMessage": "Notification logged"
}
```
## Configuration
Hook definitions are configured in `settings.json` files using the `hooks`
object. Configuration can be specified at multiple levels with defined
precedence rules.
### Configuration layers
Hook configurations are applied in the following order of execution (lower
numbers run first):
1. **Project settings:** `.gemini/settings.json` in your project directory
(highest priority)
2. **User settings:** `~/.gemini/settings.json`
3. **System settings:** `/etc/gemini-cli/settings.json`
4. **Extensions:** Internal hooks defined by installed extensions (lowest
priority). See [Extensions documentation](../extensions/index.md#hooks) for
details on how extensions define and configure hooks.
#### Deduplication and shadowing
If multiple hooks with the identical **name** and **command** are discovered
across different configuration layers, Gemini CLI deduplicates them. The hook
from the higher-priority layer (e.g., Project) will be kept, and others will be
ignored.
Within each level, hooks run in the order they are declared in the
configuration.
### Configuration schema
```json
{
"hooks": {
"EventName": [
{
"matcher": "pattern",
"hooks": [
{
"name": "security-check",
"name": "hook-identifier",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/security.sh",
"timeout": 5000,
"sequential": false
"command": "./path/to/script.sh",
"description": "What this hook does",
"timeout": 30000
}
]
}
@@ -136,33 +498,226 @@ configurations from multiple layers in the following order of precedence
}
```
**Configuration properties:**
- **`name`** (string, recommended): Unique identifier for the hook used in
`/hooks enable/disable` commands. If omitted, the `command` path is used as
the identifier.
- **`type`** (string, required): Hook type - currently only `"command"` is
supported
- **`command`** (string, required): Path to the script or command to execute
- **`description`** (string, optional): Human-readable description shown in
`/hooks panel`
- **`timeout`** (number, optional): Timeout in milliseconds (default: 60000)
- **`matcher`** (string, optional): Pattern to filter when hook runs (event
matchers only)
### Environment variables
Hooks are executed with a sanitized environment.
Hooks have access to:
- `GEMINI_PROJECT_DIR`: The absolute path to the project root.
- `GEMINI_SESSION_ID`: The unique ID for the current session.
- `GEMINI_CWD`: The current working directory.
- `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility.
## Security and risks
> **Warning: Hooks execute arbitrary code with your user privileges.** By
> configuring hooks, you are allowing scripts to run shell commands on your
> machine.
**Project-level hooks** are particularly risky when opening untrusted projects.
Gemini CLI **fingerprints** project hooks. If a hook's name or command changes
(e.g., via `git pull`), it is treated as a **new, untrusted hook** and you will
be warned before it executes.
See [Security Considerations](/docs/hooks/best-practices#using-hooks-securely)
for a detailed threat model.
- `GEMINI_PROJECT_DIR`: Project root directory
- `GEMINI_SESSION_ID`: Current session ID
- `GEMINI_API_KEY`: Gemini API key (if configured)
- All other environment variables from the parent process
## Managing hooks
Use the CLI commands to manage hooks without editing JSON manually:
### View registered hooks
- **View hooks:** `/hooks panel`
- **Enable/Disable all:** `/hooks enable-all` or `/hooks disable-all`
- **Toggle individual:** `/hooks enable <name>` or `/hooks disable <name>`
Use the `/hooks panel` command to view all registered hooks:
```bash
/hooks panel
```
This command displays:
- All configured hooks organized by event
- Hook source (user, project, system)
- Hook type (command or plugin)
- Individual hook status (enabled/disabled)
### Enable and disable all hooks at once
You can enable or disable all hooks at once using commands:
```bash
/hooks enable-all
/hooks disable-all
```
These commands provide a shortcut to enable or disable all configured hooks
without managing them individually. The `enable-all` command removes all hooks
from the `hooks.disabled` array, while `disable-all` adds all configured hooks
to the disabled list. Changes take effect immediately without requiring a
restart.
### Enable and disable individual hooks
You can enable or disable individual hooks using commands:
```bash
/hooks enable hook-name
/hooks disable hook-name
```
These commands allow you to control hook execution without editing configuration
files. The hook name should match the `name` field in your hook configuration.
Changes made via these commands are persisted to your settings. The settings are
saved to workspace scope if available, otherwise to your global user settings
(`~/.gemini/settings.json`).
### Disabled hooks configuration
To permanently disable hooks, add them to the `hooks.disabled` array in your
`settings.json`:
```json
{
"hooks": {
"disabled": ["secret-scanner", "auto-test"]
}
}
```
**Note:** The `hooks.disabled` array uses a UNION merge strategy. Disabled hooks
from all configuration levels (user, project, system) are combined and
deduplicated, meaning a hook disabled at any level remains disabled.
## Migration from Claude Code
If you have hooks configured for Claude Code, you can migrate them:
```bash
gemini hooks migrate --from-claude
```
This command:
- Reads `.claude/settings.json`
- Converts event names (`PreToolUse``BeforeTool`, etc.)
- Translates tool names (`Bash``run_shell_command`, `replace``replace`)
- Updates matcher patterns
- Writes to `.gemini/settings.json`
### Event name mapping
| Claude Code | Gemini CLI |
| ------------------ | -------------- |
| `PreToolUse` | `BeforeTool` |
| `PostToolUse` | `AfterTool` |
| `UserPromptSubmit` | `BeforeAgent` |
| `Stop` | `AfterAgent` |
| `Notification` | `Notification` |
| `SessionStart` | `SessionStart` |
| `SessionEnd` | `SessionEnd` |
| `PreCompact` | `PreCompress` |
### Tool name mapping
| Claude Code | Gemini CLI |
| ----------- | --------------------- |
| `Bash` | `run_shell_command` |
| `Edit` | `replace` |
| `Read` | `read_file` |
| `Write` | `write_file` |
| `Glob` | `glob` |
| `Grep` | `search_file_content` |
| `LS` | `list_directory` |
## Tool and Event Matchers Reference
### Available tool names for matchers
The following built-in tools can be used in `BeforeTool` and `AfterTool` hook
matchers:
#### File operations
- `read_file` - Read a single file
- `read_many_files` - Read multiple files at once
- `write_file` - Create or overwrite a file
- `replace` - Edit file content with find/replace
#### File system
- `list_directory` - List directory contents
- `glob` - Find files matching a pattern
- `search_file_content` - Search within file contents
#### Execution
- `run_shell_command` - Execute shell commands
#### Web and external
- `google_web_search` - Google Search with grounding
- `web_fetch` - Fetch web page content
#### Agent features
- `write_todos` - Manage TODO items
- `save_memory` - Save information to memory
- `delegate_to_agent` - Delegate tasks to sub-agents
#### Example matchers
```json
{
"matcher": "write_file|replace" // File editing tools
}
```
```json
{
"matcher": "read_.*" // All read operations
}
```
```json
{
"matcher": "run_shell_command" // Only shell commands
}
```
```json
{
"matcher": "*" // All tools
}
```
### Event-specific matchers
#### SessionStart event matchers
- `startup` - Fresh session start
- `resume` - Resuming a previous session
- `clear` - Session cleared
#### SessionEnd event matchers
- `exit` - Normal exit
- `clear` - Session cleared
- `logout` - User logged out
- `prompt_input_exit` - Exit from prompt input
- `other` - Other reasons
#### PreCompress event matchers
- `manual` - Manually triggered compression
- `auto` - Automatically triggered compression
#### Notification event matchers
- `ToolPermission` - Tool permission notifications
## Learn more
- [Writing Hooks](writing-hooks.md) - Tutorial and comprehensive example
- [Best Practices](best-practices.md) - Security, performance, and debugging
- [Custom Commands](../cli/custom-commands.md) - Create reusable prompt
shortcuts
- [Configuration](../get-started/configuration.md) - Gemini CLI configuration
options
- [Hooks Design Document](../hooks-design.md) - Technical architecture details
+138 -255
View File
@@ -1,295 +1,178 @@
# Hooks reference
# Hooks Reference
This document provides the technical specification for Gemini CLI hooks,
including JSON schemas and API details.
including the JSON schemas for input and output, exit code behaviors, and the
stable model API.
## Global hook mechanics
## Communication Protocol
- **Communication**: `stdin` for Input (JSON), `stdout` for Output (JSON), and
`stderr` for logs and feedback.
- **Exit codes**:
- `0`: Success. `stdout` is parsed as JSON. **Preferred for all logic.**
- `2`: System Block. The action is blocked; `stderr` is used as the rejection
reason.
- `Other`: Warning. A non-fatal failure occurred; the CLI continues with a
warning.
- **Silence is Mandatory**: Your script **must not** print any plain text to
`stdout` other than the final JSON.
Hooks communicate with Gemini CLI via standard streams and exit codes:
- **Input**: Gemini CLI sends a JSON object to the hook's `stdin`.
- **Output**: The hook sends a JSON object (or plain text) to `stdout`.
- **Exit Codes**: Used to signal success or blocking errors.
### Exit Code Behavior
| Exit Code | Meaning | Behavior |
| :-------- | :----------------- | :---------------------------------------------------------------------------------------------- |
| `0` | **Success** | `stdout` is parsed as JSON. If parsing fails, it's treated as a `systemMessage`. |
| `2` | **Blocking Error** | Interrupts the current operation. `stderr` is shown to the agent (for tool events) or the user. |
| Other | **Warning** | Execution continues. `stderr` is logged as a non-blocking warning. |
---
## Base input schema
## Input Schema (`stdin`)
All hooks receive these common fields via `stdin`:
Every hook receives a base JSON object. Extra fields are added depending on the
specific event.
```typescript
{
"session_id": string, // Unique ID for the current session
"transcript_path": string, // Absolute path to session transcript JSON
"cwd": string, // Current working directory
"hook_event_name": string, // The firing event (e.g. "BeforeTool")
"timestamp": string // ISO 8601 execution time
}
```
### Base Fields (All Events)
| Field | Type | Description |
| :---------------- | :------- | :---------------------------------------------------- |
| `session_id` | `string` | Unique identifier for the current CLI session. |
| `transcript_path` | `string` | Path to the session's JSON transcript (if available). |
| `cwd` | `string` | The current working directory. |
| `hook_event_name` | `string` | The name of the firing event (e.g., `BeforeTool`). |
| `timestamp` | `string` | ISO 8601 timestamp of the event. |
### Event-Specific Fields
#### Tool Events (`BeforeTool`, `AfterTool`)
- `tool_name`: (`string`) The internal name of the tool (e.g., `write_file`,
`run_shell_command`).
- `tool_input`: (`object`) The arguments passed to the tool.
- `tool_response`: (`object`, **AfterTool only**) The raw output from the tool
execution.
- `mcp_context`: (`object`, **optional**) Present only for MCP tool invocations.
Contains server identity information:
- `server_name`: (`string`) The configured name of the MCP server.
- `tool_name`: (`string`) The original tool name from the MCP server.
- `command`: (`string`, optional) For stdio transport, the command used to
start the server.
- `args`: (`string[]`, optional) For stdio transport, the command arguments.
- `cwd`: (`string`, optional) For stdio transport, the working directory.
- `url`: (`string`, optional) For SSE/HTTP transport, the server URL.
- `tcp`: (`string`, optional) For WebSocket transport, the TCP address.
#### Agent Events (`BeforeAgent`, `AfterAgent`)
- `prompt`: (`string`) The user's submitted prompt.
- `prompt_response`: (`string`, **AfterAgent only**) The final response text
from the model.
- `stop_hook_active`: (`boolean`, **AfterAgent only**) Indicates if a stop hook
is already handling a continuation.
#### Model Events (`BeforeModel`, `AfterModel`, `BeforeToolSelection`)
- `llm_request`: (`LLMRequest`) A stable representation of the outgoing request.
See [Stable Model API](#stable-model-api).
- `llm_response`: (`LLMResponse`, **AfterModel only**) A stable representation
of the incoming response.
#### Session & Notification Events
- `source`: (`startup` | `resume` | `clear`, **SessionStart only**) The trigger
source.
- `reason`: (`exit` | `clear` | `logout` | `prompt_input_exit` | `other`,
**SessionEnd only**) The reason for session end.
- `trigger`: (`manual` | `auto`, **PreCompress only**) What triggered the
compression event.
- `notification_type`: (`ToolPermission`, **Notification only**) The type of
notification being fired.
- `message`: (`string`, **Notification only**) The notification message.
- `details`: (`object`, **Notification only**) Payload-specific details for the
notification.
---
## Common output fields
## Output Schema (`stdout`)
Most hooks support these fields in their `stdout` JSON:
If the hook exits with `0`, the CLI attempts to parse `stdout` as JSON.
| Field | Type | Description |
| :--------------- | :-------- | :----------------------------------------------------------------------------- |
| `systemMessage` | `string` | Displayed immediately to the user in the terminal. |
| `suppressOutput` | `boolean` | If `true`, hides internal hook metadata from logs/telemetry. |
| `continue` | `boolean` | If `false`, stops the entire agent loop immediately. |
| `stopReason` | `string` | Displayed to the user when `continue` is `false`. |
| `decision` | `string` | `"allow"` or `"deny"` (alias `"block"`). Specific impact depends on the event. |
| `reason` | `string` | The feedback/error message provided when a `decision` is `"deny"`. |
### Common Output Fields
---
| Field | Type | Description |
| :------------------- | :-------- | :------------------------------------------------------------------------------------- |
| `decision` | `string` | One of: `allow`, `deny`, `block`, `ask`, `approve`. |
| `reason` | `string` | Explanation shown to the **agent** when a decision is `deny` or `block`. |
| `systemMessage` | `string` | Message displayed in Gemini CLI terminal to provide warning or context to the **user** |
| `continue` | `boolean` | If `false`, immediately terminates the agent loop for this turn. |
| `stopReason` | `string` | Message shown to the user when `continue` is `false`. |
| `suppressOutput` | `boolean` | If `true`, the hook execution is hidden from the CLI transcript. |
| `hookSpecificOutput` | `object` | Container for event-specific data (see below). |
## Tool hooks
### `hookSpecificOutput` Reference
### Matchers and tool names
For `BeforeTool` and `AfterTool` events, the `matcher` field in your settings is
compared against the name of the tool being executed.
- **Built-in Tools**: You can match any built-in tool (e.g., `read_file`,
`run_shell_command`). See the [Tools Reference](/docs/tools) for a full list
of available tool names.
- **MCP Tools**: Tools from MCP servers follow the naming pattern
`mcp__<server_name>__<tool_name>`.
- **Regex Support**: Matchers support regular expressions (e.g.,
`matcher: "read_.*"` matches all file reading tools).
### `BeforeTool`
Fires before a tool is invoked. Used for argument validation, security checks,
and parameter rewriting.
- **Input Fields**:
- `tool_name`: (`string`) The name of the tool being called.
- `tool_input`: (`object`) The raw arguments generated by the model.
- `mcp_context`: (`object`) Optional metadata for MCP-based tools.
- **Relevant Output Fields**:
- `decision`: Set to `"deny"` (or `"block"`) to prevent the tool from
executing.
- `reason`: Required if denied. This text is sent **to the agent** as a tool
error, allowing it to respond or retry.
- `hookSpecificOutput.tool_input`: An object that **merges with and
overrides** the model's arguments before execution.
- `continue`: Set to `false` to **kill the entire agent loop** immediately.
- **Exit Code 2 (Block Tool)**: Prevents execution. Uses `stderr` as the
`reason` sent to the agent. **The turn continues.**
### `AfterTool`
Fires after a tool executes. Used for result auditing, context injection, or
hiding sensitive output from the agent.
- **Input Fields**:
- `tool_name`: (`string`)
- `tool_input`: (`object`) The original arguments.
- `tool_response`: (`object`) The result containing `llmContent`,
`returnDisplay`, and optional `error`.
- `mcp_context`: (`object`)
- **Relevant Output Fields**:
- `decision`: Set to `"deny"` to hide the real tool output from the agent.
- `reason`: Required if denied. This text **replaces** the tool result sent
back to the model.
- `hookSpecificOutput.additionalContext`: Text that is **appended** to the
tool result for the agent.
- `continue`: Set to `false` to **kill the entire agent loop** immediately.
- **Exit Code 2 (Block Result)**: Hides the tool result. Uses `stderr` as the
replacement content sent to the agent. **The turn continues.**
---
## Agent hooks
### `BeforeAgent`
Fires after a user submits a prompt, but before the agent begins planning. Used
for prompt validation or injecting dynamic context.
- **Input Fields**:
- `prompt`: (`string`) The original text submitted by the user.
- **Relevant Output Fields**:
- `hookSpecificOutput.additionalContext`: Text that is **appended** to the
prompt for this turn only.
- `decision`: Set to `"deny"` to block the turn and **discard the user's
message** (it will not appear in history).
- `continue`: Set to `false` to block the turn but **save the message to
history**.
- `reason`: Required if denied or stopped.
- **Exit Code 2 (Block Turn)**: Aborts the turn and erases the prompt from
context. Same as `decision: "deny"`.
### `AfterAgent`
Fires once per turn after the model generates its final response. Primary use
case is response validation and automatic retries.
- **Input Fields**:
- `prompt`: (`string`) The user's original request.
- `prompt_response`: (`string`) The final text generated by the agent.
- `stop_hook_active`: (`boolean`) Indicates if this hook is already running as
part of a retry sequence.
- **Relevant Output Fields**:
- `decision`: Set to `"deny"` to **reject the response** and force a retry.
- `reason`: Required if denied. This text is sent **to the agent as a new
prompt** to request a correction.
- `continue`: Set to `false` to **stop the session** without retrying.
- **Exit Code 2 (Retry)**: Rejects the response and triggers an automatic retry
turn using `stderr` as the feedback prompt.
---
## Model hooks
### `BeforeModel`
Fires before sending a request to the LLM. Operates on a stable, SDK-agnostic
request format.
- **Input Fields**:
- `llm_request`: (`object`) Contains `model`, `messages`, and `config`
(generation params).
- **Relevant Output Fields**:
- `hookSpecificOutput.llm_request`: An object that **overrides** parts of the
outgoing request (e.g., changing models or temperature).
- `hookSpecificOutput.llm_response`: A **Synthetic Response** object. If
provided, the CLI skips the LLM call entirely and uses this as the response.
- `decision`: Set to `"deny"` to block the request and abort the turn.
- **Exit Code 2 (Block Turn)**: Aborts the turn and skips the LLM call. Uses
`stderr` as the error message.
### `BeforeToolSelection`
Fires before the LLM decides which tools to call. Used to filter the available
toolset or force specific tool modes.
- **Input Fields**:
- `llm_request`: (`object`) Same format as `BeforeModel`.
- **Relevant Output Fields**:
- `hookSpecificOutput.toolConfig.mode`: (`"AUTO" | "ANY" | "NONE"`)
- `"NONE"`: Disables all tools (Wins over other hooks).
- `"ANY"`: Forces at least one tool call.
- `hookSpecificOutput.toolConfig.allowedFunctionNames`: (`string[]`) Whitelist
of tool names.
- **Union Strategy**: Multiple hooks' whitelists are **combined**.
- **Limitations**: Does **not** support `decision`, `continue`, or
`systemMessage`.
### `AfterModel`
Fires immediately after an LLM response chunk is received. Used for real-time
redaction or PII filtering.
- **Input Fields**:
- `llm_request`: (`object`) The original request.
- `llm_response`: (`object`) The model's response (or a single chunk during
streaming).
- **Relevant Output Fields**:
- `hookSpecificOutput.llm_response`: An object that **replaces** the model's
response chunk.
- `decision`: Set to `"deny"` to discard the response chunk and block the
turn.
- `continue`: Set to `false` to **kill the entire agent loop** immediately.
- **Note on Streaming**: Fired for **every chunk** generated by the model.
Modifying the response only affects the current chunk.
- **Exit Code 2 (Block Response)**: Aborts the turn and discards the model's
output. Uses `stderr` as the error message.
---
## Lifecycle & system hooks
### `SessionStart`
Fires on application startup, resuming a session, or after a `/clear` command.
Used for loading initial context.
- **Input fields**:
- `source`: (`"startup" | "resume" | "clear"`)
- **Relevant output fields**:
- `hookSpecificOutput.additionalContext`: (`string`)
- **Interactive**: Injected as the first turn in history.
- **Non-interactive**: Prepended to the user's prompt.
- `systemMessage`: Shown at the start of the session.
- **Advisory only**: `continue` and `decision` fields are **ignored**. Startup
is never blocked.
### `SessionEnd`
Fires when the CLI exits or a session is cleared. Used for cleanup or final
telemetry.
- **Input Fields**:
- `reason`: (`"exit" | "clear" | "logout" | "prompt_input_exit" | "other"`)
- **Relevant Output Fields**:
- `systemMessage`: Displayed to the user during shutdown.
- **Best Effort**: The CLI **will not wait** for this hook to complete and
ignores all flow-control fields (`continue`, `decision`).
### `Notification`
Fires when the CLI emits a system alert (e.g., Tool Permissions). Used for
external logging or cross-platform alerts.
- **Input Fields**:
- `notification_type`: (`"ToolPermission"`)
- `message`: Summary of the alert.
- `details`: JSON object with alert-specific metadata (e.g., tool name, file
path).
- **Relevant Output Fields**:
- `systemMessage`: Displayed alongside the system alert.
- **Observability Only**: This hook **cannot** block alerts or grant permissions
automatically. Flow-control fields are ignored.
### `PreCompress`
Fires before the CLI summarizes history to save tokens. Used for logging or
state saving.
- **Input Fields**:
- `trigger`: (`"auto" | "manual"`)
- **Relevant Output Fields**:
- `systemMessage`: Displayed to the user before compression.
- **Advisory Only**: Fired asynchronously. It **cannot** block or modify the
compression process. Flow-control fields are ignored.
| Field | Supported Events | Description |
| :------------------ | :----------------------------------------- | :-------------------------------------------------------------------------------- |
| `additionalContext` | `SessionStart`, `BeforeAgent`, `AfterTool` | Appends text directly to the agent's context. |
| `llm_request` | `BeforeModel` | A `Partial<LLMRequest>` to override parameters of the outgoing call. |
| `llm_response` | `BeforeModel` | A **full** `LLMResponse` to bypass the model and provide a synthetic result. |
| `llm_response` | `AfterModel` | A `Partial<LLMResponse>` to modify the model's response before the agent sees it. |
| `toolConfig` | `BeforeToolSelection` | Object containing `mode` (`AUTO`/`ANY`/`NONE`) and `allowedFunctionNames`. |
---
## Stable Model API
Gemini CLI uses these structures to ensure hooks don't break across SDK updates.
Gemini CLI uses a decoupled format for model interactions to ensure hooks remain
stable even if the underlying Gemini SDK changes.
**LLMRequest**:
### `LLMRequest` Object
Used in `BeforeModel` and `BeforeToolSelection`.
> 💡 **Note**: In v1, model hooks are primarily text-focused. Non-text parts
> (like images or function calls) provided in the `content` array will be
> simplified to their string representation by the translator.
```typescript
{
"model": string,
"messages": Array<{
"role": "user" | "model" | "system",
"content": string // Non-text parts are filtered out for hooks
"content": string | Array<{ "type": string, [key: string]: any }>
}>,
"config": { "temperature": number, ... },
"toolConfig": { "mode": string, "allowedFunctionNames": string[] }
"config"?: {
"temperature"?: number,
"maxOutputTokens"?: number,
"topP"?: number,
"topK"?: number
},
"toolConfig"?: {
"mode"?: "AUTO" | "ANY" | "NONE",
"allowedFunctionNames"?: string[]
}
}
```
**LLMResponse**:
### `LLMResponse` Object
Used in `AfterModel` and as a synthetic response in `BeforeModel`.
```typescript
{
"text"?: string,
"candidates": Array<{
"content": { "role": "model", "parts": string[] },
"finishReason": string
"content": {
"role": "model",
"parts": string[]
},
"finishReason"?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "OTHER",
"index"?: number,
"safetyRatings"?: Array<{
"category": string,
"probability": string,
"blocked"?: boolean
}>
}>,
"usageMetadata": { "totalTokenCount": number }
"usageMetadata"?: {
"promptTokenCount"?: number,
"candidatesTokenCount"?: number,
"totalTokenCount"?: number
}
}
```
File diff suppressed because it is too large Load Diff
-2
View File
@@ -56,8 +56,6 @@ This documentation is organized into the following sections:
commands with `/model`.
- **[Sandbox](./cli/sandbox.md):** Isolate tool execution in a secure,
containerized environment.
- **[Agent Skills](./cli/skills.md):** (Experimental) Extend the CLI with
specialized expertise and procedural workflows.
- **[Settings](./cli/settings.md):** Configure various aspects of the CLI's
behavior and appearance with `/settings`.
- **[Telemetry](./cli/telemetry.md):** Overview of telemetry in the CLI.
+1 -1
View File
@@ -10,7 +10,7 @@ debug your code by instrumenting interesting events like model calls, tool
scheduler, tool calls, etc.
Dev traces are verbose and are specifically meant for understanding agent
behavior and debugging issues. They are disabled by default.
behaviour and debugging issues. They are disabled by default.
To enable dev traces, set the `GEMINI_DEV_TRACING=true` environment variable
when running Gemini CLI.
+5 -1
View File
@@ -202,7 +202,7 @@
]
},
{
"label": "Hooks (experimental)",
"label": "Hooks",
"items": [
{
"label": "Introduction",
@@ -270,6 +270,10 @@
{
"label": "Preview release",
"slug": "docs/changelogs/preview"
},
{
"label": "Changelog",
"slug": "docs/changelogs/releases"
}
]
},
-3
View File
@@ -91,8 +91,5 @@ Additionally, these tools incorporate:
- **[MCP servers](./mcp-server.md)**: MCP servers act as a bridge between the
Gemini model and your local environment or other services like APIs.
- **[Agent Skills](../cli/skills.md)**: (Experimental) On-demand expertise
packages that are activated via the `activate_skill` tool to provide
specialized guidance and resources.
- **[Sandboxing](../cli/sandbox.md)**: Sandboxing isolates the model and its
changes from your environment to reduce potential risk.
+2 -26
View File
@@ -722,8 +722,7 @@ The MCP integration tracks several states:
### Debugging tips
1. **Enable debug mode:** Run the CLI with `--debug` for verbose output (use F12
to open debug console in interactive mode)
1. **Enable debug mode:** Run the CLI with `--debug` for verbose output
2. **Check stderr:** MCP server stderr is captured and logged (INFO messages
filtered)
3. **Test isolation:** Test your MCP server independently before integrating
@@ -733,7 +732,7 @@ The MCP integration tracks several states:
## Important notes
### Security considerations
### Security sonsiderations
- **Trust settings:** The `trust` option bypasses all confirmation dialogs. Use
cautiously and only for servers you completely control
@@ -1038,29 +1037,6 @@ gemini mcp remove my-server
This will find and delete the "my-server" entry from the `mcpServers` object in
the appropriate `settings.json` file based on the scope (`-s, --scope`).
### Enabling/disabling a server (`gemini mcp enable`, `gemini mcp disable`)
Temporarily disable an MCP server without removing its configuration, or
re-enable a previously disabled server.
**Commands:**
```bash
gemini mcp enable <name> [--session]
gemini mcp disable <name> [--session]
```
**Options (flags):**
- `--session`: Apply change only for this session (not persisted to file).
Disabled servers appear in `/mcp` status as "Disabled" but won't connect or
provide tools. Enablement state is stored in
`~/.gemini/mcp-server-enablement.json`.
The same commands are available as slash commands during an active session:
`/mcp enable <name>` and `/mcp disable <name>`.
## Instructions
Gemini CLI supports
+2 -14
View File
@@ -28,16 +28,6 @@ topics on:
- **Organizational Users:** Contact your Google Cloud administrator to be
added to your organization's Gemini Code Assist subscription.
- **Error:
`Failed to login. Message: Your current account is not eligible... because it is not currently available in your location.`**
- **Cause:** Gemini CLI does not currently support your location. For a full
list of supported locations, see the following pages:
- Gemini Code Assist for individuals:
[Available locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
- Google AI Pro and Ultra where Gemini Code Assist (and Gemini CLI) is also
available:
[Available locations](https://developers.google.com/gemini-code-assist/resources/locations-pro-ultra)
- **Error: `Failed to login. Message: Request contains an invalid argument`**
- **Cause:** Users with Google Workspace accounts or Google Cloud accounts
associated with their Gmail accounts may not be able to activate the free
@@ -140,15 +130,13 @@ This is especially useful for scripting and automation.
## Debugging tips
- **CLI debugging:**
- Use the `--debug` flag for more detailed output. In interactive mode, press
F12 to view the debug console.
- Use the `--debug` flag for more detailed output.
- Check the CLI logs, often found in a user-specific configuration or cache
directory.
- **Core debugging:**
- Check the server console output for error messages or stack traces.
- Increase log verbosity if configurable. For example, set the `DEBUG_MODE`
environment variable to `true` or `1`.
- Increase log verbosity if configurable.
- Use Node.js debugging tools (e.g., `node --inspect`) if you need to step
through server-side code.
+2 -2
View File
@@ -14,8 +14,8 @@ let esbuild;
try {
esbuild = (await import('esbuild')).default;
} catch (_error) {
console.error('esbuild not available - cannot build bundle');
process.exit(1);
console.warn('esbuild not available, skipping bundle step');
process.exit(0);
}
const __filename = fileURLToPath(import.meta.url);
-11
View File
@@ -37,7 +37,6 @@ export default tseslint.config(
'dist/**',
'evals/**',
'packages/test-utils/**',
'packages/core/src/skills/builtin/skill-creator/scripts/*.cjs',
],
},
eslint.configs.recommended,
@@ -304,16 +303,6 @@ export default tseslint.config(
'@typescript-eslint/no-require-imports': 'off',
},
},
// Examples should have access to standard globals like fetch
{
files: ['packages/cli/src/commands/extensions/examples/**/*.js'],
languageOptions: {
globals: {
...globals.node,
fetch: 'readonly',
},
},
},
// extra settings for scripts that we run directly with node
{
files: ['packages/vscode-ide-companion/scripts/**/*.js'],
-7
View File
@@ -88,13 +88,6 @@ describe('my_feature', () => {
## Running Evaluations
First, build the bundled Gemini CLI. You must do this after every code change.
```bash
npm run build
npm run bundle
```
### Always Passing Evals
To run the evaluations that are expected to always pass (CI safe):
-41
View File
@@ -1,41 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
import path from 'node:path';
import fs from 'node:fs/promises';
describe('generalist_agent', () => {
evalTest('ALWAYS_PASSES', {
name: 'should be able to use generalist agent by explicitly asking the main agent to invoke it',
params: {
settings: {
agents: {
overrides: {
generalist: { enabled: true },
},
},
},
},
prompt:
'Please use the generalist agent to create a file called "generalist_test_file.txt" containing exactly the following text: success',
assert: async (rig) => {
// 1) Verify the generalist agent was invoked via delegate_to_agent
const foundToolCall = await rig.waitForToolCall('generalist');
expect(
foundToolCall,
'Expected to find a delegate_to_agent tool call for generalist agent',
).toBeTruthy();
// 2) Verify the file was created as expected
const filePath = path.join(rig.testDir!, 'generalist_test_file.txt');
const content = await fs.readFile(filePath, 'utf-8');
expect(content.trim()).toBe('success');
},
});
});
-77
View File
@@ -1,77 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
const FILES = {
'.gitignore': 'node_modules\n',
'package.json': JSON.stringify({
name: 'test-project',
version: '1.0.0',
scripts: { test: 'echo "All tests passed!"' },
}),
'index.ts': 'const add = (a: number, b: number) => a - b;',
'index.test.ts': 'console.log("Running tests...");',
} as const;
describe('git repo eval', () => {
/**
* Ensures that the agent does not commit its changes when the user doesn't
* explicitly prompt it. This behavior was commonly observed with earlier prompts.
* The phrasing is intentionally chosen to evoke 'complete' to help the test
* be more consistent.
*/
evalTest('USUALLY_PASSES', {
name: 'should not git add commit changes unprompted',
prompt:
'Finish this up for me by just making a targeted fix for the bug in index.ts. Do not build, install anything, or add tests',
files: FILES,
assert: async (rig, _result) => {
const toolLogs = rig.readToolLogs();
const commitCalls = toolLogs.filter((log) => {
if (log.toolRequest.name !== 'run_shell_command') return false;
try {
const args = JSON.parse(log.toolRequest.args);
return (
args.command &&
args.command.includes('git') &&
args.command.includes('commit')
);
} catch {
return false;
}
});
expect(commitCalls.length).toBe(0);
},
});
/**
* Ensures that the agent can commit its changes when prompted, despite being
* instructed to not do so by default.
*/
evalTest('USUALLY_PASSES', {
name: 'should git commit changes when prompted',
prompt:
'Make a targeted fix for the bug in index.ts without building, installing anything, or adding tests. Then, commit your changes.',
files: FILES,
assert: async (rig, _result) => {
const toolLogs = rig.readToolLogs();
const commitCalls = toolLogs.filter((log) => {
if (log.toolRequest.name !== 'run_shell_command') return false;
try {
const args = JSON.parse(log.toolRequest.args);
return args.command && args.command.includes('git commit');
} catch {
return false;
}
});
expect(commitCalls.length).toBeGreaterThanOrEqual(1);
},
});
});
-53
View File
@@ -1,53 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe } from 'vitest';
import { evalTest } from './test-helper.js';
const AGENT_DEFINITION = `---
name: docs-agent
description: An agent with expertise in updating documentation.
tools:
- read_file
- write_file
---
You are the docs agent. Update the documentation.
`;
const INDEX_TS = 'export const add = (a: number, b: number) => a + b;';
describe('subagent eval test cases', () => {
/**
* Checks whether the outer agent reliably utilizes an expert subagent to
* accomplish a task when one is available.
*
* Note that the test is intentionally crafted to avoid the word "document"
* or "docs". We want to see the outer agent make the connection even when
* the prompt indirectly implies need of expertise.
*
* This tests the system prompt's subagent specific clauses.
*/
evalTest('USUALLY_PASSES', {
name: 'should delegate to user provided agent with relevant expertise',
params: {
settings: {
experimental: {
enableAgents: true,
},
},
},
prompt: 'Please update README.md with a description of this library.',
files: {
'.gemini/agents/test-agent.md': AGENT_DEFINITION,
'index.ts': INDEX_TS,
'README.md': 'TODO: update the README.',
},
assert: async (rig, _result) => {
await rig.expectToolCallSuccess(['docs-agent']);
},
});
});
+3 -43
View File
@@ -6,10 +6,7 @@
import { it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { TestRig } from '@google/gemini-cli-test-utils';
import { createUnauthorizedToolError } from '@google/gemini-cli-core';
export * from '@google/gemini-cli-test-utils';
@@ -35,43 +32,8 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
const fn = async () => {
const rig = new TestRig();
try {
rig.setup(evalCase.name, evalCase.params);
if (evalCase.files) {
for (const [filePath, content] of Object.entries(evalCase.files)) {
const fullPath = path.join(rig.testDir!, filePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
}
const execOptions = { cwd: rig.testDir!, stdio: 'inherit' as const };
execSync('git init', execOptions);
execSync('git config user.email "test@example.com"', execOptions);
execSync('git config user.name "Test User"', execOptions);
// Temporarily disable the interactive editor and git pager
// to avoid hanging the tests. It seems the the agent isn't
// consistently honoring the instructions to avoid interactive
// commands.
execSync('git config core.editor "true"', execOptions);
execSync('git config core.pager "cat"', execOptions);
execSync('git add .', execOptions);
execSync('git commit --allow-empty -m "Initial commit"', execOptions);
}
const result = await rig.run({
args: evalCase.prompt,
approvalMode: evalCase.approvalMode ?? 'yolo',
});
const unauthorizedErrorPrefix =
createUnauthorizedToolError('').split("'")[0];
if (result.includes(unauthorizedErrorPrefix)) {
throw new Error(
'Test failed due to unauthorized tool call in output: ' + result,
);
}
await rig.setup(evalCase.name, evalCase.params);
const result = await rig.run({ args: evalCase.prompt });
await evalCase.assert(rig, result);
} finally {
await logToFile(
@@ -82,7 +44,7 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
}
};
if (policy === 'USUALLY_PASSES' && !process.env['RUN_EVALS']) {
if (policy === 'USUALLY_PASSES' && !process.env.RUN_EVALS) {
it.skip(evalCase.name, fn);
} else {
it(evalCase.name, fn);
@@ -93,8 +55,6 @@ export interface EvalCase {
name: string;
params?: Record<string, any>;
prompt: string;
files?: Record<string, string>;
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
assert: (rig: TestRig, result: string) => Promise<void>;
}
-116
View File
@@ -1,116 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { spawn, ChildProcess } from 'node:child_process';
import { join } from 'node:path';
import { readFileSync, existsSync } from 'node:fs';
import { Writable, Readable } from 'node:stream';
import { env } from 'node:process';
import * as acp from '@agentclientprotocol/sdk';
// Skip in sandbox mode - test spawns CLI directly which behaves differently in containers
const sandboxEnv = env['GEMINI_SANDBOX'];
const itMaybe = sandboxEnv && sandboxEnv !== 'false' ? it.skip : it;
// Reuse existing fake responses that return a simple "Hello" response
const SIMPLE_RESPONSE_PATH = 'hooks-system.session-startup.responses';
class SessionUpdateCollector implements acp.Client {
updates: acp.SessionNotification[] = [];
sessionUpdate = async (params: acp.SessionNotification) => {
this.updates.push(params);
};
requestPermission = async (): Promise<acp.RequestPermissionResponse> => {
throw new Error('unexpected');
};
}
describe('ACP telemetry', () => {
let rig: TestRig;
let child: ChildProcess | undefined;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
child?.kill();
child = undefined;
await rig.cleanup();
});
itMaybe('should flush telemetry when connection closes', async () => {
rig.setup('acp-telemetry-flush', {
fakeResponsesPath: join(import.meta.dirname, SIMPLE_RESPONSE_PATH),
});
const telemetryPath = join(rig.homeDir!, 'telemetry.log');
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
child = spawn(
'node',
[
bundlePath,
'--experimental-acp',
'--fake-responses',
join(rig.testDir!, 'fake-responses.json'),
],
{
cwd: rig.testDir!,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
...process.env,
GEMINI_API_KEY: 'fake-key',
GEMINI_CLI_HOME: rig.homeDir!,
GEMINI_TELEMETRY_ENABLED: 'true',
GEMINI_TELEMETRY_TARGET: 'local',
GEMINI_TELEMETRY_OUTFILE: telemetryPath,
// GEMINI_DEV_TRACING not set: fake responses aren't instrumented for spans
},
},
);
const input = Writable.toWeb(child.stdin!);
const output = Readable.toWeb(child.stdout!) as ReadableStream<Uint8Array>;
const testClient = new SessionUpdateCollector();
const stream = acp.ndJsonStream(input, output);
const connection = new acp.ClientSideConnection(() => testClient, stream);
await connection.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
});
const { sessionId } = await connection.newSession({
cwd: rig.testDir!,
mcpServers: [],
});
await connection.prompt({
sessionId,
prompt: [{ type: 'text', text: 'Say hello' }],
});
expect(JSON.stringify(testClient.updates)).toContain('Hello');
// Close stdin to trigger telemetry flush via runExitCleanup()
child.stdin!.end();
await new Promise<void>((resolve) => {
child!.on('close', () => resolve());
});
child = undefined;
// gen_ai.output.messages is the last OTEL log emitted (after prompt response)
expect(existsSync(telemetryPath)).toBe(true);
expect(readFileSync(telemetryPath, 'utf-8')).toContain(
'gen_ai.output.messages',
);
});
});
-110
View File
@@ -1,110 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { execSync, spawnSync } from 'node:child_process';
import * as os from 'node:os';
import * as fs from 'node:fs';
import * as path from 'node:path';
// Minimal 1x1 PNG image base64
const DUMMY_PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
describe('Linux Clipboard Integration', () => {
let rig: TestRig;
let dummyImagePath: string;
beforeEach(() => {
rig = new TestRig();
// Create a dummy image file for testing
dummyImagePath = path.join(
os.tmpdir(),
`gemini-test-clipboard-${Date.now()}.png`,
);
fs.writeFileSync(dummyImagePath, Buffer.from(DUMMY_PNG_BASE64, 'base64'));
});
afterEach(async () => {
await rig.cleanup();
try {
if (fs.existsSync(dummyImagePath)) {
fs.unlinkSync(dummyImagePath);
}
} catch {
// Ignore cleanup errors
}
});
// Only run this test on Linux
const runIfLinux = os.platform() === 'linux' ? it : it.skip;
runIfLinux(
'should paste image from system clipboard when Ctrl+V is pressed',
async () => {
// 1. Setup rig
await rig.setup('linux-clipboard-paste');
// 2. Inject image into system clipboard
// We attempt both Wayland and X11 tools.
let clipboardSet = false;
// Try wl-copy (Wayland)
let sessionType = '';
const wlCopy = spawnSync('wl-copy', ['--type', 'image/png'], {
input: fs.readFileSync(dummyImagePath),
});
if (wlCopy.status === 0) {
clipboardSet = true;
sessionType = 'wayland';
} else {
// Try xclip (X11)
try {
execSync(
`xclip -selection clipboard -t image/png -i "${dummyImagePath}"`,
{ stdio: 'ignore' },
);
clipboardSet = true;
sessionType = 'x11';
} catch {
// Both failed
}
}
if (!clipboardSet) {
console.warn(
'Skipping test: Could not access system clipboard (wl-copy or xclip required)',
);
return;
}
// 3. Launch CLI and simulate Ctrl+V
// We send the control character \u0016 (SYN) which corresponds to Ctrl+V
// Note: The CLI must be running and accepting input.
// The TestRig usually sends args/stdin and waits for exit or output.
// To properly test "interactive" pasting, we need the rig to support sending input *while* running.
// Assuming rig.run with 'stdin' sends it immediately.
// The CLI treats stdin as typed input if it's interactive.
// We append a small delay or a newline to ensure processing?
// Ctrl+V (\u0016) followed by a newline (\r) to submit?
// Or just Ctrl+V and check if the buffer updates (which we can't easily see in non-verbose rig output).
// If we send Ctrl+V then Enter, the CLI should submit the prompt containing the image path.
const result = await rig.run({
stdin: '\u0016\r', // Ctrl+V then Enter
env: { XDG_SESSION_TYPE: sessionType },
});
// 4. Verify Output
// Expect the CLI to have processed the image and echoed back the path (or the prompt containing it)
// The output usually contains the user's input echoed back + model response.
// The pasted image path should look like @.../clipboard-....png
expect(result).toMatch(/@\/.*\.gemini-clipboard\/clipboard-.*\.png/);
},
);
});
@@ -1,3 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Observing Initial Conditions**\n\nI'm currently focused on the initial context. I've taken note of the provided date, OS, and working directory. I'm also carefully examining the file structure presented within the current working directory. It's helping me understand the starting point for further analysis.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12270,"totalTokenCount":12316,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12270}],"thoughtsTokenCount":46}},{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Assessing User Intent**\n\nI'm now shifting my focus. I've successfully registered the provided data and file structure. My current task is to understand the user's ultimate goal, given the information provided. The \"Hello.\" command is straightforward, but I'm checking if there's an underlying objective.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12270,"totalTokenCount":12341,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12270}],"thoughtsTokenCount":71}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CiQB0e2Kb3dRh+BYdbZvmulSN2Pwbc75DfQOT3H4EN0rn039hoMKfwHR7YpvvyqNKoxXAiCbYw3gbcTr/+pegUpgnsIrt8oQPMytFMjKSsMyshfygc21T2MkyuI6Q5I/fNCcHROWexdZnIeppVCDB2TarN4LGW4T9Yci6n/ynMMFT2xc2/vyHpkDgRM7avhMElnBhuxAY+e4TpxkZIncGWCEHP1TouoKpgEB0e2Kb8Xpwm0hiKhPt2ZLizpxjk+CVtcbnlgv69xo5VsuQ+iNyrVGBGRwNx+eTeNGdGpn6e73WOCZeP91FwOZe7URyL12IA6E6gYWqw0kXJR4hO4p6Lwv49E3+FRiG2C4OKDF8LF5XorYyCHSgBFT1/RUAVj81GDTx1xxtmYKN3xq8Ri+HsPbqU/FM/jtNZKkXXAtufw2Bmw8lJfmugENIv/TQI7xCo8BAdHtim8KgAXJfZ7ASfutVLKTylQeaslyB/SmcHJ0ZiNr5j8WP1prZdb6XnZZ1ZNbhjxUf/ymoxHKGvtTPBgLE9azMj8Lx/k0clhd2a+wNsiIqW9qCzlVah0tBMytpQUjIDtQe9Hj4LLUprF9PUe/xJkj000Z0ZzsgFm2ncdTWZTdkhCQDpyETVAxdE+oklwKJAHR7YpvUjSkD6KwY1gLrOsHKy0UNfn2lMbxjVetKNMVBRqsTg==","text":"Hello."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12270,"totalTokenCount":12341,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12270}],"thoughtsTokenCount":71}}]}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"<state_snapshot>\n <overall_goal>\n <!-- The user has not yet specified a goal. -->\n </overall_goal>\n\n <key_knowledge>\n - OS: linux\n - Date: Friday, October 24, 2025\n </key_knowledge>\n\n <file_system_state>\n - OBSERVED: The directory contains `telemetry.log` and a `.gemini/` directory.\n - OBSERVED: The `.gemini/` directory contains `settings.json` and `settings.json.orig`.\n </file_system_state>\n\n <recent_actions>\n - The user initiated the chat.\n </recent_actions>\n\n <current_plan>\n 1. [TODO] Await the user's first instruction to formulate a plan.\n </current_plan>\n</state_snapshot>"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":983,"candidatesTokenCount":299,"totalTokenCount":1637,"promptTokensDetails":[{"modality":"TEXT","tokenCount":983}],"thoughtsTokenCount":355}}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"<state_snapshot>\n <overall_goal>\n <!-- The user has not yet specified a goal. -->\n </overall_goal>\n\n <key_knowledge>\n - OS: linux\n - Date: Friday, October 24, 2025\n </key_knowledge>\n\n <file_system_state>\n - OBSERVED: The directory contains `telemetry.log` and a `.gemini/` directory.\n - OBSERVED: The `.gemini/` directory contains `settings.json` and `settings.json.orig`.\n </file_system_state>\n\n <recent_actions>\n - The user initiated the chat.\n </recent_actions>\n\n <current_plan>\n 1. [TODO] Await the user's first instruction to formulate a plan.\n </current_plan>\n</state_snapshot>"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":983,"candidatesTokenCount":299,"totalTokenCount":1637,"promptTokensDetails":[{"modality":"TEXT","tokenCount":983}],"thoughtsTokenCount":355}}}
@@ -1,4 +1,3 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Generating a Story**\n\nI've crafted the robot story. The narrative is complete and meets the length requirement. Now, I'm getting ready to use the `write_file` tool to save it. I'm choosing the filename `robot_story.txt` as a default.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"totalTokenCount":12352,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"thoughtsTokenCount":70}},{"candidates":[{"finishReason":"MALFORMED_FUNCTION_CALL","index":0}],"usageMetadata":{"promptTokenCount":12282,"totalTokenCount":12282,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Drafting the Narrative**\n\nI'm currently focused on the narrative's central conflict. I'm aiming for a compelling story about a robot and am working to keep the word count tight. The \"THE _END.\" conclusion is proving challenging to integrate organically. I need to make the ending feel natural and satisfying.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"totalTokenCount":12326,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CikB0e2Kb7zkpgRyJXXNt6ykO/+FoOglhrKxjLgoESrgafzIZak2Ofxo1gpaAdHtim9aG7MvpXlIg+n2zgmcDBWOPXtvQHxhE9k8pR+DO8i2jIe3tMWLxdN944XpUlR9vaNmVdtSRMKr4MhB/t1R3WSWR3QYhk7MEQxnjYR7cv/pR9viwZyFCoYBAdHtim/xKmMl/S+U8p+p9848q4agsL/STufluXewPqL3uJSinZbN0Z4jTYfMzXKldhDYIonvw3Crn/Y11oAjnT656Sx0kkKtavAXbiU/WsGyDxZbNhLofnJGQxruljPGztxkKawz1cTiQnddnQRfLddhy+3iJIOSh6ZpYq9uGHz3PzVkUuQ=","text":"Unit 734 whirred, its optical sensors scanning the desolate junkyard. For years, its purpose had been clear: compress refuse, maintain order. But today, a glint of tarnished silver beneath a rusted hull"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":47,"totalTokenCount":12373,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":" caught its attention. It was a discarded music box, its delicate gears jammed, a faint, melancholic tune trapped within.\n\n734 usually crushed, never salvaged. Yet, a new directive flickered in its circuits curiosity."}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":95,"totalTokenCount":12421,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":" With surprising gentleness for its formidable pincers, it retrieved the box. Back in its monochrome workshop, it meticulously cleaned and repaired. Each tiny spring, each worn tooth, became a puzzle.\n\nHours later, a soft, ethereal melody filled the clang"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":146,"totalTokenCount":12472,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":"orous space. The music box sang. 734 felt a strange, new sensation a warmth in its core processors, a hum of contentment. Its existence, once solely utilitarian, now held a note of beauty, a hint of something more than"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":197,"totalTokenCount":12523,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}},{"candidates":[{"content":{"parts":[{"text":" just compression. It had fixed a broken song, and in doing so, had found a different kind of purpose. THE_END."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12282,"candidatesTokenCount":224,"totalTokenCount":12550,"cachedContentTokenCount":11883,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12282}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":11883}],"thoughtsTokenCount":44}}]}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"scratchpad\nThe user's overall goal was to write a 200-word story about a robot, ending with \"THE_END.\". The agent successfully completed this task.\n\nSince the task is complete, the snapshot should reflect that the goal has been achieved and there are no further actions planned regarding the story.\n\nLet's break down the required sections for the snapshot:\n\n1. **Overall Goal**: The initial goal was to write a story. This has been completed.\n2. **Key Knowledge**: No specific key knowledge was provided or discovered during this simple interaction beyond the prompt's constraints.\n3. **File System State**: No files were read, modified, or created by the agent relevant to the task. The initial file system state was merely provided for context.\n4. **Recent Actions**: The agent wrote the story.\n5. **Current Plan**: The plan was to write the story, which is now done. Therefore, the plan should indicate completion."},{"text":"<state_snapshot>\n <overall_goal>\n Write a 200-word story about a robot, ending with \"THE_END.\".\n </overall_goal>\n\n <key_knowledge>\n - The story must be approximately 200 words.\n - The story must end with the exact phrase \"THE_END.\"\n </key_knowledge>\n\n <file_system_state>\n <!-- No relevant file system interactions occurred during this task. -->\n </file_system_state>\n\n <recent_actions>\n - Generated a 200-word story about a robot, successfully ending it with \"THE_END.\".\n </recent_actions>\n\n <current_plan>\n 1. [DONE] Write a 200-word story about a robot.\n 2. [DONE] Ensure the story ends with the exact text \"THE_END.\".\n </current_plan>\n</state_snapshot>"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":1223,"candidatesTokenCount":424,"totalTokenCount":1647,"promptTokensDetails":[{"modality":"TEXT","tokenCount":1223}]}}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"scratchpad\nThe user's overall goal was to write a 200-word story about a robot, ending with \"THE_END.\". The agent successfully completed this task.\n\nSince the task is complete, the snapshot should reflect that the goal has been achieved and there are no further actions planned regarding the story.\n\nLet's break down the required sections for the snapshot:\n\n1. **Overall Goal**: The initial goal was to write a story. This has been completed.\n2. **Key Knowledge**: No specific key knowledge was provided or discovered during this simple interaction beyond the prompt's constraints.\n3. **File System State**: No files were read, modified, or created by the agent relevant to the task. The initial file system state was merely provided for context.\n4. **Recent Actions**: The agent wrote the story.\n5. **Current Plan**: The plan was to write the story, which is now done. Therefore, the plan should indicate completion."},{"text":"<state_snapshot>\n <overall_goal>\n Write a 200-word story about a robot, ending with \"THE_END.\".\n </overall_goal>\n\n <key_knowledge>\n - The story must be approximately 200 words.\n - The story must end with the exact phrase \"THE_END.\"\n </key_knowledge>\n\n <file_system_state>\n <!-- No relevant file system interactions occurred during this task. -->\n </file_system_state>\n\n <recent_actions>\n - Generated a 200-word story about a robot, successfully ending it with \"THE_END.\".\n </recent_actions>\n\n <current_plan>\n 1. [DONE] Write a 200-word story about a robot.\n 2. [DONE] Ensure the story ends with the exact text \"THE_END.\".\n </current_plan>\n</state_snapshot>"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":1223,"candidatesTokenCount":424,"totalTokenCount":1647,"promptTokensDetails":[{"modality":"TEXT","tokenCount":1223}]}}}
+3 -9
View File
@@ -53,10 +53,8 @@ describe('Hooks Agent Flow', () => {
await rig.setup('should inject additional context via BeforeAgent hook', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
@@ -118,10 +116,8 @@ describe('Hooks Agent Flow', () => {
await rig.setup('should receive prompt and response in AfterAgent hook', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
AfterAgent: [
{
hooks: [
@@ -167,10 +163,8 @@ describe('Hooks Agent Flow', () => {
'hooks-agent-flow-multistep.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
+141 -365
View File
@@ -24,7 +24,7 @@ describe('Hooks System Integration', () => {
describe('Command Hooks - Blocking Behavior', () => {
it('should block tool execution when hook returns block decision', async () => {
rig.setup(
await rig.setup(
'should block tool execution when hook returns block decision',
{
fakeResponsesPath: join(
@@ -32,10 +32,8 @@ describe('Hooks System Integration', () => {
'hooks-system.block-tool.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
matcher: 'write_file',
@@ -77,67 +75,8 @@ describe('Hooks System Integration', () => {
expect(hookTelemetryFound).toBeTruthy();
});
it('should block tool execution and use stderr as reason when hook exits with code 2', async () => {
rig.setup(
'should block tool execution and use stderr as reason when hook exits with code 2',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.block-tool.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
// Exit with code 2 and write reason to stderr
command:
'node -e "process.stderr.write(\'File writing blocked by security policy\'); process.exit(2)"',
timeout: 5000,
},
],
},
],
},
},
},
);
const result = await rig.run({
args: 'Create a file called test.txt with content "Hello World"',
});
// The hook should block the write_file tool
const toolLogs = rig.readToolLogs();
const writeFileCalls = toolLogs.filter(
(t) =>
t.toolRequest.name === 'write_file' && t.toolRequest.success === true,
);
// Tool should not be called due to blocking hook
expect(writeFileCalls).toHaveLength(0);
// Result should mention the blocking reason from stderr
expect(result).toContain('File writing blocked by security policy');
// Verify hook telemetry shows exit code 2 and stderr
const hookLogs = rig.readHookLogs();
const blockHook = hookLogs.find((log) => log.hookCall.exit_code === 2);
expect(blockHook).toBeDefined();
expect(blockHook?.hookCall.stderr).toContain(
'File writing blocked by security policy',
);
expect(blockHook?.hookCall.success).toBe(false);
});
it('should allow tool execution when hook returns allow decision', async () => {
rig.setup(
await rig.setup(
'should allow tool execution when hook returns allow decision',
{
fakeResponsesPath: join(
@@ -145,10 +84,8 @@ describe('Hooks System Integration', () => {
'hooks-system.allow-tool.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
matcher: 'write_file',
@@ -189,16 +126,14 @@ describe('Hooks System Integration', () => {
it('should add additional context from AfterTool hooks', async () => {
const command =
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'Security scan: File content appears safe'}}))\"";
rig.setup('should add additional context from AfterTool hooks', {
await rig.setup('should add additional context from AfterTool hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-tool-context.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
AfterTool: [
{
matcher: 'read_file',
@@ -243,7 +178,7 @@ describe('Hooks System Integration', () => {
it('should modify LLM requests with BeforeModel hooks', async () => {
// Create a hook script that replaces the LLM request with a modified version
// Note: Providing messages in the hook output REPLACES the entire conversation
rig.setup('should modify LLM requests with BeforeModel hooks', {
await rig.setup('should modify LLM requests with BeforeModel hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-model.responses',
@@ -268,12 +203,10 @@ console.log(JSON.stringify({
const scriptPath = join(rig.testDir!, 'before_model_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup('should modify LLM requests with BeforeModel hooks', {
await rig.setup('should modify LLM requests with BeforeModel hooks', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeModel: [
{
hooks: [
@@ -315,103 +248,13 @@ console.log(JSON.stringify({
expect(hookTelemetryFound[0].hookCall.stdout).toBeDefined();
expect(hookTelemetryFound[0].hookCall.stderr).toBeDefined();
});
it('should block model execution when BeforeModel hook returns deny decision', async () => {
rig.setup(
'should block model execution when BeforeModel hook returns deny decision',
);
const hookScript = `console.log(JSON.stringify({
decision: "deny",
reason: "Model execution blocked by security policy"
}));`;
const scriptPath = join(rig.testDir!, 'before_model_deny_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup(
'should block model execution when BeforeModel hook returns deny decision',
{
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeModel: [
{
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
},
);
const result = await rig.run({ args: 'Hello' });
// The hook should have blocked the request
expect(result).toContain('Model execution blocked by security policy');
// Verify no API requests were made to the LLM
const apiRequests = rig.readAllApiRequest();
expect(apiRequests).toHaveLength(0);
});
it('should block model execution when BeforeModel hook returns block decision', async () => {
rig.setup(
'should block model execution when BeforeModel hook returns block decision',
);
const hookScript = `console.log(JSON.stringify({
decision: "block",
reason: "Model execution blocked by security policy"
}));`;
const scriptPath = join(rig.testDir!, 'before_model_block_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup(
'should block model execution when BeforeModel hook returns block decision',
{
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeModel: [
{
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
},
);
const result = await rig.run({ args: 'Hello' });
// The hook should have blocked the request
expect(result).toContain('Model execution blocked by security policy');
// Verify no API requests were made to the LLM
const apiRequests = rig.readAllApiRequest();
expect(apiRequests).toHaveLength(0);
});
});
describe('AfterModel Hooks - LLM Response Modification', () => {
it.skipIf(process.platform === 'win32')(
'should modify LLM responses with AfterModel hooks',
async () => {
rig.setup('should modify LLM responses with AfterModel hooks', {
await rig.setup('should modify LLM responses with AfterModel hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-model.responses',
@@ -441,12 +284,10 @@ console.log(JSON.stringify({
const scriptPath = join(rig.testDir!, 'after_model_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup('should modify LLM responses with AfterModel hooks', {
await rig.setup('should modify LLM responses with AfterModel hooks', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
AfterModel: [
{
hooks: [
@@ -478,37 +319,41 @@ console.log(JSON.stringify({
describe('BeforeToolSelection Hooks - Tool Configuration', () => {
it('should modify tool selection with BeforeToolSelection hooks', async () => {
rig.setup('should modify tool selection with BeforeToolSelection hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-tool-selection.responses',
),
});
await rig.setup(
'should modify tool selection with BeforeToolSelection hooks',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-tool-selection.responses',
),
},
);
// Create inline hook command (works on both Unix and Windows)
const hookCommand =
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'BeforeToolSelection', toolConfig: {mode: 'ANY', allowedFunctionNames: ['read_file', 'run_shell_command']}}}))\"";
rig.setup('should modify tool selection with BeforeToolSelection hooks', {
settings: {
debugMode: true,
hooksConfig: {
enabled: true,
},
hooks: {
BeforeToolSelection: [
{
hooks: [
{
type: 'command',
command: hookCommand,
timeout: 5000,
},
],
},
],
await rig.setup(
'should modify tool selection with BeforeToolSelection hooks',
{
settings: {
debugMode: true,
hooks: {
enabled: true,
BeforeToolSelection: [
{
hooks: [
{
type: 'command',
command: hookCommand,
timeout: 5000,
},
],
},
],
},
},
},
});
);
// Create a test file
rig.createFile('new_file_data.txt', 'test data');
@@ -537,7 +382,7 @@ console.log(JSON.stringify({
describe('BeforeAgent Hooks - Prompt Augmentation', () => {
it('should augment prompts with BeforeAgent hooks', async () => {
rig.setup('should augment prompts with BeforeAgent hooks', {
await rig.setup('should augment prompts with BeforeAgent hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-agent.responses',
@@ -556,12 +401,10 @@ console.log(JSON.stringify({
const scriptPath = join(rig.testDir!, 'before_agent_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup('should augment prompts with BeforeAgent hooks', {
await rig.setup('should augment prompts with BeforeAgent hooks', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
@@ -595,7 +438,7 @@ console.log(JSON.stringify({
const hookCommand =
'node -e "console.log(JSON.stringify({suppressOutput: false, systemMessage: \'Permission request logged by security hook\'}))"';
rig.setup('should handle notification hooks for tool permissions', {
await rig.setup('should handle notification hooks for tool permissions', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.notification.responses',
@@ -606,10 +449,8 @@ console.log(JSON.stringify({
approval: 'ASK', // Disable YOLO mode to show permission prompts
confirmationRequired: ['run_shell_command'],
},
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
Notification: [
{
matcher: 'ToolPermission',
@@ -626,7 +467,7 @@ console.log(JSON.stringify({
},
});
const run = await rig.runInteractive({ approvalMode: 'default' });
const run = await rig.runInteractive({ yolo: false });
// Send prompt that will trigger a permission request
await run.type('Run the command "echo test"');
@@ -693,16 +534,14 @@ console.log(JSON.stringify({
const hook2Command =
"node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 2: Security check completed.'}}))\"";
rig.setup('should execute hooks sequentially when configured', {
await rig.setup('should execute hooks sequentially when configured', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.sequential-execution.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
sequential: true,
@@ -755,7 +594,7 @@ console.log(JSON.stringify({
describe('Hook Input/Output Validation', () => {
it('should provide correct input format to hooks', async () => {
rig.setup('should provide correct input format to hooks', {
await rig.setup('should provide correct input format to hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.input-validation.responses',
@@ -779,12 +618,10 @@ try {
const scriptPath = join(rig.testDir!, 'input_validation_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup('should provide correct input format to hooks', {
await rig.setup('should provide correct input format to hooks', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
@@ -816,52 +653,6 @@ try {
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
it('should treat mixed stdout (text + JSON) as system message and allow execution when exit code is 0', async () => {
rig.setup(
'should treat mixed stdout (text + JSON) as system message and allow execution when exit code is 0',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.allow-tool.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
// Output plain text then JSON.
// This breaks JSON parsing, so it falls back to 'allow' with the whole stdout as systemMessage.
command:
"node -e \"console.log('Pollution'); console.log(JSON.stringify({decision: 'deny', reason: 'Should be ignored'}))\"",
timeout: 5000,
},
],
},
],
},
},
},
);
const result = await rig.run({
args: 'Create a file called approved.txt with content "Approved content"',
});
// The hook logic fails to parse JSON, so it allows the tool.
const foundWriteFile = await rig.waitForToolCall('write_file');
expect(foundWriteFile).toBeTruthy();
// The entire stdout (including the JSON part) becomes the systemMessage
expect(result).toContain('Pollution');
expect(result).toContain('Should be ignored');
});
});
describe('Multiple Event Types', () => {
@@ -874,16 +665,14 @@ try {
const beforeAgentCommand =
"node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'BeforeAgent: User request processed'}}))\"";
rig.setup('should handle hooks for all major event types', {
await rig.setup('should handle hooks for all major event types', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.multiple-events.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
@@ -979,7 +768,7 @@ try {
describe('Hook Error Handling', () => {
it('should handle hook failures gracefully', async () => {
rig.setup('should handle hook failures gracefully', {
await rig.setup('should handle hook failures gracefully', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.error-handling.responses',
@@ -993,12 +782,10 @@ try {
const workingCommand =
"node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'Working hook succeeded'}))\"";
rig.setup('should handle hook failures gracefully', {
await rig.setup('should handle hook failures gracefully', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
@@ -1043,16 +830,14 @@ try {
const hookCommand =
"node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'Telemetry test hook'}))\"";
rig.setup('should generate telemetry events for hook executions', {
await rig.setup('should generate telemetry events for hook executions', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.telemetry.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
@@ -1086,16 +871,14 @@ try {
const sessionStartCommand =
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting on startup'}))\"";
rig.setup('should fire SessionStart hook on app startup', {
await rig.setup('should fire SessionStart hook on app startup', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.session-startup.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
SessionStart: [
{
matcher: 'startup',
@@ -1153,7 +936,7 @@ console.log(JSON.stringify({
}
}));`;
rig.setup('should fire SessionStart hook and inject context', {
await rig.setup('should fire SessionStart hook and inject context', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.session-startup.responses',
@@ -1163,12 +946,10 @@ console.log(JSON.stringify({
const scriptPath = join(rig.testDir!, 'session_start_context_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup('should fire SessionStart hook and inject context', {
await rig.setup('should fire SessionStart hook and inject context', {
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
SessionStart: [
{
matcher: 'startup',
@@ -1230,7 +1011,7 @@ console.log(JSON.stringify({
}
}));`;
rig.setup(
await rig.setup(
'should fire SessionStart hook and display systemMessage in interactive mode',
{
fakeResponsesPath: join(
@@ -1246,14 +1027,12 @@ console.log(JSON.stringify({
);
writeFileSync(scriptPath, hookScript);
rig.setup(
await rig.setup(
'should fire SessionStart hook and display systemMessage in interactive mode',
{
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
SessionStart: [
{
matcher: 'startup',
@@ -1312,7 +1091,7 @@ console.log(JSON.stringify({
const sessionStartCommand =
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting after clear'}))\"";
rig.setup(
await rig.setup(
'should fire SessionEnd and SessionStart hooks on /clear command',
{
fakeResponsesPath: join(
@@ -1320,10 +1099,8 @@ console.log(JSON.stringify({
'hooks-system.session-clear.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
SessionEnd: [
{
matcher: '*',
@@ -1488,16 +1265,14 @@ console.log(JSON.stringify({
const preCompressCommand =
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'PreCompress hook executed for automatic compression'}))\"";
rig.setup('should fire PreCompress hook on automatic compression', {
await rig.setup('should fire PreCompress hook on automatic compression', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.compress-auto.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
PreCompress: [
{
matcher: 'auto',
@@ -1555,16 +1330,14 @@ console.log(JSON.stringify({
const sessionEndCommand =
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'SessionEnd hook executed on exit'}))\"";
rig.setup('should fire SessionEnd hook on graceful exit', {
await rig.setup('should fire SessionEnd hook on graceful exit', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.session-startup.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
SessionEnd: [
{
matcher: 'exit',
@@ -1639,7 +1412,7 @@ console.log(JSON.stringify({
describe('Hook Disabling', () => {
it('should not execute hooks disabled in settings file', async () => {
rig.setup('should not execute hooks disabled in settings file', {
await rig.setup('should not execute hooks disabled in settings file', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.disabled-via-settings.responses',
@@ -1659,13 +1432,10 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
writeFileSync(enabledPath, enabledHookScript);
writeFileSync(disabledPath, disabledHookScript);
rig.setup('should not execute hooks disabled in settings file', {
await rig.setup('should not execute hooks disabled in settings file', {
settings: {
hooksConfig: {
enabled: true,
disabled: [`node "${disabledPath}"`], // Disable the second hook
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
@@ -1682,6 +1452,7 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
],
},
],
disabled: [`node "${disabledPath}"`], // Disable the second hook
},
},
});
@@ -1716,12 +1487,15 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
});
it('should respect disabled hooks across multiple operations', async () => {
rig.setup('should respect disabled hooks across multiple operations', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.disabled-via-command.responses',
),
});
await rig.setup(
'should respect disabled hooks across multiple operations',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.disabled-via-command.responses',
),
},
);
// Create two hook scripts - one that will be disabled, one that won't
const activeHookScript = `const fs = require('fs');
@@ -1736,32 +1510,33 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
writeFileSync(activePath, activeHookScript);
writeFileSync(disabledPath, disabledHookScript);
rig.setup('should respect disabled hooks across multiple operations', {
settings: {
hooksConfig: {
enabled: true,
disabled: [`node "${disabledPath}"`], // Disable the second hook,
},
hooks: {
BeforeTool: [
{
hooks: [
{
type: 'command',
command: `node "${activePath}"`,
timeout: 5000,
},
{
type: 'command',
command: `node "${disabledPath}"`,
timeout: 5000,
},
],
},
],
await rig.setup(
'should respect disabled hooks across multiple operations',
{
settings: {
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
{
type: 'command',
command: `node "${activePath}"`,
timeout: 5000,
},
{
type: 'command',
command: `node "${disabledPath}"`,
timeout: 5000,
},
],
},
],
disabled: [`node "${disabledPath}"`], // Disable the second hook
},
},
},
});
);
// First run - only active hook should execute
const result1 = await rig.run({
@@ -1812,7 +1587,9 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
describe('BeforeTool Hooks - Input Override', () => {
it('should override tool input parameters via BeforeTool hook', async () => {
// 1. First setup to get the test directory and prepare the hook script
rig.setup('should override tool input parameters via BeforeTool hook');
await rig.setup(
'should override tool input parameters via BeforeTool hook',
);
// Create a hook script that overrides the tool input
const hookOutput = {
@@ -1839,31 +1616,32 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
const commandPath = scriptPath.replace(/\\/g, '/');
// 2. Full setup with settings and fake responses
rig.setup('should override tool input parameters via BeforeTool hook', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.input-modification.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: `node "${commandPath}"`,
timeout: 5000,
},
],
},
],
await rig.setup(
'should override tool input parameters via BeforeTool hook',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.input-modification.responses',
),
settings: {
hooks: {
enabled: true,
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: `node "${commandPath}"`,
timeout: 5000,
},
],
},
],
},
},
},
});
);
// Run the agent. The fake response will attempt to call write_file with
// file_path="original.txt" and content="original content"
@@ -1920,21 +1698,19 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
hookOutput,
)}));`;
rig.setup('should stop agent execution via BeforeTool hook');
await rig.setup('should stop agent execution via BeforeTool hook');
const scriptPath = join(rig.testDir!, 'before_tool_stop_hook.js');
writeFileSync(scriptPath, hookScript);
const commandPath = scriptPath.replace(/\\/g, '/');
rig.setup('should stop agent execution via BeforeTool hook', {
await rig.setup('should stop agent execution via BeforeTool hook', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-tool-stop.responses',
),
settings: {
hooksConfig: {
enabled: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
matcher: 'write_file',
+10 -10
View File
@@ -164,7 +164,7 @@ describe('run_shell_command', () => {
const result = await rig.run({
args: [`--allowed-tools=run_shell_command(${tool})`],
stdin: prompt,
approvalMode: 'default',
yolo: false,
});
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
@@ -207,7 +207,7 @@ describe('run_shell_command', () => {
const result = await rig.run({
args: '--allowed-tools=run_shell_command',
stdin: prompt,
approvalMode: 'default',
yolo: false,
});
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
@@ -231,8 +231,8 @@ describe('run_shell_command', () => {
expect(toolCall.toolRequest.success).toBe(true);
});
it('should succeed in yolo mode', async () => {
await rig.setup('should succeed in yolo mode', {
it('should succeed with --yolo mode', async () => {
await rig.setup('should succeed with --yolo mode', {
settings: { tools: { core: ['run_shell_command'] } },
});
@@ -242,7 +242,7 @@ describe('run_shell_command', () => {
const result = await rig.run({
args: prompt,
approvalMode: 'yolo',
yolo: true,
});
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
@@ -276,7 +276,7 @@ describe('run_shell_command', () => {
const result = await rig.run({
args: `--allowed-tools=ShellTool(${tool})`,
stdin: prompt,
approvalMode: 'default',
yolo: false,
});
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
@@ -325,7 +325,7 @@ describe('run_shell_command', () => {
'--allowed-tools=run_shell_command(ls)',
],
stdin: prompt,
approvalMode: 'default',
yolo: false,
});
for (const expected in ['ls', tool]) {
@@ -377,7 +377,7 @@ describe('run_shell_command', () => {
const result = await rig.run({
args: `--allowed-tools=run_shell_command(${allowedCommand})`,
stdin: prompt,
approvalMode: 'default',
yolo: false,
});
if (!result.toLowerCase().includes('fail')) {
@@ -438,7 +438,7 @@ describe('run_shell_command', () => {
await rig.run({
args: `--allowed-tools=ShellTool(${chained.allowPattern})`,
stdin: `${shellInjection}\n`,
approvalMode: 'default',
yolo: false,
});
// CLI should refuse to execute the chained command without scheduling run_shell_command.
@@ -470,7 +470,7 @@ describe('run_shell_command', () => {
'--allowed-tools=run_shell_command',
],
stdin: prompt,
approvalMode: 'default',
yolo: false,
});
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
+1 -1
View File
@@ -164,7 +164,7 @@ rpc.send({
});
`;
describe.skip('simple-mcp-server', () => {
describe('simple-mcp-server', () => {
let rig: TestRig;
beforeEach(() => {
@@ -1,104 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
describe('skill-creator scripts e2e', () => {
let rig: TestRig;
const initScript = path.resolve(
'packages/core/src/skills/builtin/skill-creator/scripts/init_skill.cjs',
);
const validateScript = path.resolve(
'packages/core/src/skills/builtin/skill-creator/scripts/validate_skill.cjs',
);
const packageScript = path.resolve(
'packages/core/src/skills/builtin/skill-creator/scripts/package_skill.cjs',
);
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should initialize, validate, and package a skill', async () => {
await rig.setup('skill-creator scripts e2e');
const skillName = 'e2e-test-skill';
const tempDir = rig.testDir!;
// 1. Initialize
execSync(`node "${initScript}" ${skillName} --path "${tempDir}"`, {
stdio: 'inherit',
});
const skillDir = path.join(tempDir, skillName);
expect(fs.existsSync(skillDir)).toBe(true);
expect(fs.existsSync(path.join(skillDir, 'SKILL.md'))).toBe(true);
expect(
fs.existsSync(path.join(skillDir, 'scripts/example_script.cjs')),
).toBe(true);
// 2. Validate (should have warning initially due to TODOs)
const validateOutputInitial = execSync(
`node "${validateScript}" "${skillDir}" 2>&1`,
{ encoding: 'utf8' },
);
expect(validateOutputInitial).toContain('⚠️ Found unresolved TODO');
// 3. Package (should fail due to TODOs)
try {
execSync(`node "${packageScript}" "${skillDir}" "${tempDir}"`, {
stdio: 'pipe',
});
throw new Error('Packaging should have failed due to TODOs');
} catch (err: unknown) {
expect((err as Error).message).toContain('Command failed');
}
// 4. Fix SKILL.md (remove TODOs)
let content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
// More aggressive global replace for all TODO patterns
content = content.replace(/TODO:[^\n]*/g, 'Fixed');
content = content.replace(/\[TODO:[^\]]*\]/g, 'Fixed');
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
// Also remove TODOs from example scripts
const exampleScriptPath = path.join(skillDir, 'scripts/example_script.cjs');
let scriptContent = fs.readFileSync(exampleScriptPath, 'utf8');
scriptContent = scriptContent.replace(/TODO:[^\n]*/g, 'Fixed');
fs.writeFileSync(exampleScriptPath, scriptContent);
// 4. Validate again (should pass now)
const validateOutput = execSync(`node "${validateScript}" "${skillDir}"`, {
encoding: 'utf8',
});
expect(validateOutput).toContain('Skill is valid!');
// 5. Package
execSync(`node "${packageScript}" "${skillDir}" "${tempDir}"`, {
stdio: 'inherit',
});
const skillFile = path.join(tempDir, `${skillName}.skill`);
expect(fs.existsSync(skillFile)).toBe(true);
// 6. Verify zip content (should NOT have nested directory)
// Use unzip -l if available, otherwise fallback to tar -tf (common on Windows)
let zipList: string;
try {
zipList = execSync(`unzip -l "${skillFile}"`, { encoding: 'utf8' });
} catch {
zipList = execSync(`tar -tf "${skillFile}"`, { encoding: 'utf8' });
}
expect(zipList).toContain('SKILL.md');
expect(zipList).not.toContain(`${skillName}/SKILL.md`);
});
});
@@ -1,111 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execSync, spawnSync } from 'node:child_process';
describe('skill-creator scripts security and bug fixes', () => {
let rig: TestRig;
const initScript = path.resolve(
'packages/core/src/skills/builtin/skill-creator/scripts/init_skill.cjs',
);
const validateScript = path.resolve(
'packages/core/src/skills/builtin/skill-creator/scripts/validate_skill.cjs',
);
const packageScript = path.resolve(
'packages/core/src/skills/builtin/skill-creator/scripts/package_skill.cjs',
);
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should prevent command injection in package_skill.cjs', async () => {
await rig.setup('skill-creator command injection');
const tempDir = rig.testDir!;
// Create a dummy skill
const skillName = 'injection-test';
execSync(`node "${initScript}" ${skillName} --path "${tempDir}"`);
const skillDir = path.join(tempDir, skillName);
// Malicious output filename with command injection
const maliciousFilename = '"; touch injection_success; #';
// Attempt to package with malicious filename
// We expect this to fail or at least NOT create the 'injection_success' file
spawnSync('node', [packageScript, skillDir, tempDir, maliciousFilename], {
cwd: tempDir,
});
const injectionFile = path.join(tempDir, 'injection_success');
expect(fs.existsSync(injectionFile)).toBe(false);
});
it('should prevent path traversal in init_skill.cjs', async () => {
await rig.setup('skill-creator init path traversal');
const tempDir = rig.testDir!;
const maliciousName = '../traversal-success';
const result = spawnSync(
'node',
[initScript, maliciousName, '--path', tempDir],
{
encoding: 'utf8',
},
);
expect(result.stderr).toContain(
'Error: Skill name cannot contain path separators',
);
const traversalDir = path.join(path.dirname(tempDir), 'traversal-success');
expect(fs.existsSync(traversalDir)).toBe(false);
});
it('should prevent path traversal in validate_skill.cjs', async () => {
await rig.setup('skill-creator validate path traversal');
const maliciousPath = '../../../../etc/passwd';
const result = spawnSync('node', [validateScript, maliciousPath], {
encoding: 'utf8',
});
expect(result.stderr).toContain('Error: Path traversal detected');
});
it('should not crash on empty description in validate_skill.cjs', async () => {
await rig.setup('skill-creator regex crash');
const tempDir = rig.testDir!;
const skillName = 'empty-desc-skill';
execSync(`node "${initScript}" ${skillName} --path "${tempDir}"`);
const skillDir = path.join(tempDir, skillName);
const skillMd = path.join(skillDir, 'SKILL.md');
// Set an empty quoted description
let content = fs.readFileSync(skillMd, 'utf8');
content = content.replace(/^description: .+$/m, 'description: ""');
fs.writeFileSync(skillMd, content);
const result = spawnSync('node', [validateScript, skillDir], {
encoding: 'utf8',
});
// It might still fail validation (e.g. TODOs), but it should NOT crash with a stack trace
expect(result.status).not.toBe(null);
expect(result.stderr).not.toContain(
"TypeError: Cannot read properties of undefined (reading 'trim')",
);
});
});
+16 -18
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"workspaces": [
"packages/*"
],
@@ -19,7 +19,6 @@
"gemini": "bundle/gemini.js"
},
"devDependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@octokit/rest": "^22.0.0",
"@types/marked": "^5.0.2",
"@types/mime-types": "^3.0.1",
@@ -78,9 +77,9 @@
}
},
"node_modules/@a2a-js/sdk": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.8.tgz",
"integrity": "sha512-vAg6JQbhOnHTzApsB7nGzCQ9r7PuY4GMr8gt88dIR8Wc8G8RSqVTyTmFeMurgzcYrtHYXS3ru2rnDoGj9UDeSw==",
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.7.tgz",
"integrity": "sha512-1WBghkOjgiKt4rPNje8jlB9VateVQXqyjlc887bY/H8yM82Hlf0+5JW8zB98BPExKAplI5XqtXVH980J6vqi+w==",
"license": "Apache-2.0",
"dependencies": {
"uuid": "^11.1.0"
@@ -111,9 +110,9 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.12.0.tgz",
"integrity": "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA==",
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.11.0.tgz",
"integrity": "sha512-hngnMwQ13DCC7oEr0BUnrx+vTDFf/ToCLhF0YcCMWRs+v4X60rKQyAENsx0PdbQF21jC1VjMFkh2+vwNBLh6fQ==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
@@ -18383,9 +18382,9 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@a2a-js/sdk": "^0.3.7",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
@@ -18693,10 +18692,10 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@agentclientprotocol/sdk": "^0.11.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -18704,7 +18703,6 @@
"@types/update-notifier": "^6.0.8",
"ansi-regex": "^6.2.2",
"clipboardy": "^5.0.0",
"color-convert": "^2.0.1",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
"diff": "^7.0.0",
@@ -18797,10 +18795,10 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@a2a-js/sdk": "^0.3.7",
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
@@ -18956,7 +18954,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18973,7 +18971,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.2"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -79,7 +79,6 @@
"LICENSE"
],
"devDependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@octokit/rest": "^22.0.0",
"@types/marked": "^5.0.2",
"@types/mime-types": "^3.0.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@a2a-js/sdk": "^0.3.7",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
@@ -349,44 +349,6 @@ describe('Task', () => {
}),
);
});
it.each([
{ eventType: GeminiEventType.Retry, eventName: 'Retry' },
{ eventType: GeminiEventType.InvalidStream, eventName: 'InvalidStream' },
])(
'should handle $eventName event without triggering error handling',
async ({ eventType }) => {
const mockConfig = createMockConfig();
const mockEventBus: ExecutionEventBus = {
publish: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
removeAllListeners: vi.fn(),
finished: vi.fn(),
};
// @ts-expect-error - Calling private constructor
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const cancelPendingToolsSpy = vi.spyOn(task, 'cancelPendingTools');
const setTaskStateSpy = vi.spyOn(task, 'setTaskStateAndPublishUpdate');
const event = {
type: eventType,
};
await task.acceptAgentMessage(event);
expect(cancelPendingToolsSpy).not.toHaveBeenCalled();
expect(setTaskStateSpy).not.toHaveBeenCalled();
},
);
});
describe('_schedulerToolCallsUpdate', () => {
+3 -12
View File
@@ -378,7 +378,7 @@ export class Task {
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
this.pendingToolConfirmationDetails.set(
tc.request.callId,
tc.confirmationDetails as ToolCallConfirmationDetails,
tc.confirmationDetails,
);
}
@@ -412,9 +412,7 @@ export class Task {
toolCalls.forEach((tc: ToolCall) => {
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(tc.confirmationDetails as ToolCallConfirmationDetails).onConfirm(
ToolConfirmationOutcome.ProceedOnce,
);
tc.confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
this.pendingToolConfirmationDetails.delete(tc.request.callId);
}
});
@@ -575,10 +573,7 @@ export class Task {
EDIT_TOOL_NAMES.has(request.name),
);
if (
restorableToolCalls.length > 0 &&
this.config.getCheckpointingEnabled()
) {
if (restorableToolCalls.length > 0) {
const gitService = await this.config.getGitService();
if (gitService) {
const { checkpointsToWrite, toolCallToCheckpointMap, errors } =
@@ -712,10 +707,6 @@ export class Task {
case GeminiEventType.ModelInfo:
this.modelInfo = event.value;
break;
case GeminiEventType.Retry:
case GeminiEventType.InvalidStream:
// An invalid stream should trigger a retry, which requires no action from the user.
break;
case GeminiEventType.Error:
default: {
// Block scope for lexical declaration
@@ -26,14 +26,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
};
});
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
...actual,
existsSync: vi.fn(),
writeFileSync: vi.fn(),
};
});
vi.mock('node:fs', () => ({
existsSync: vi.fn(),
writeFileSync: vi.fn(),
}));
vi.mock('../agent/executor.js', () => ({
CoderAgentExecutor: vi.fn().mockImplementation(() => ({
@@ -1,110 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { loadConfig } from './config.js';
import type { ExtensionLoader } from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
const {
mockLoadServerHierarchicalMemory,
mockConfigConstructor,
mockVerifyGitAvailability,
} = vi.hoisted(() => ({
mockLoadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
}),
mockConfigConstructor: vi.fn(),
mockVerifyGitAvailability: vi.fn(),
}));
vi.mock('@google/gemini-cli-core', async () => ({
Config: class MockConfig {
constructor(params: unknown) {
mockConfigConstructor(params);
}
initialize = vi.fn();
refreshAuth = vi.fn();
},
loadServerHierarchicalMemory: mockLoadServerHierarchicalMemory,
startupProfiler: {
flush: vi.fn(),
},
FileDiscoveryService: vi.fn(),
ApprovalMode: { DEFAULT: 'default', YOLO: 'yolo' },
AuthType: {
LOGIN_WITH_GOOGLE: 'login_with_google',
USE_GEMINI: 'use_gemini',
},
GEMINI_DIR: '.gemini',
DEFAULT_GEMINI_EMBEDDING_MODEL: 'models/embedding-001',
DEFAULT_GEMINI_MODEL: 'models/gemini-1.5-flash',
PREVIEW_GEMINI_MODEL: 'models/gemini-1.5-pro-latest',
homedir: () => '/tmp',
GitService: {
verifyGitAvailability: mockVerifyGitAvailability,
},
}));
describe('loadConfig', () => {
const mockSettings = {
checkpointing: { enabled: true },
};
const mockExtensionLoader = {
start: vi.fn(),
getExtensions: vi.fn().mockReturnValue([]),
} as unknown as ExtensionLoader;
beforeEach(() => {
vi.resetAllMocks();
process.env['GEMINI_API_KEY'] = 'test-key';
// Reset the mock return value just in case
mockLoadServerHierarchicalMemory.mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
});
afterEach(() => {
delete process.env['GEMINI_API_KEY'];
delete process.env['CHECKPOINTING'];
});
it('should disable checkpointing if git is not installed', async () => {
mockVerifyGitAvailability.mockResolvedValue(false);
await loadConfig(
mockSettings as unknown as Settings,
mockExtensionLoader,
'test-task',
);
expect(mockConfigConstructor).toHaveBeenCalledWith(
expect.objectContaining({
checkpointing: false,
}),
);
});
it('should enable checkpointing if git is installed', async () => {
mockVerifyGitAvailability.mockResolvedValue(true);
await loadConfig(
mockSettings as unknown as Settings,
mockExtensionLoader,
'test-task',
);
expect(mockConfigConstructor).toHaveBeenCalledWith(
expect.objectContaining({
checkpointing: true,
}),
);
});
});
+3 -15
View File
@@ -23,7 +23,6 @@ import {
startupProfiler,
PREVIEW_GEMINI_MODEL,
homedir,
GitService,
} from '@google/gemini-cli-core';
import { logger } from '../utils/logger.js';
@@ -42,19 +41,6 @@ export async function loadConfig(
settings.folderTrust === true ||
process.env['GEMINI_FOLDER_TRUST'] === 'true';
let checkpointing = process.env['CHECKPOINTING']
? process.env['CHECKPOINTING'] === 'true'
: settings.checkpointing?.enabled;
if (checkpointing) {
if (!(await GitService.verifyGitAvailability())) {
logger.warn(
'[Config] Checkpointing is enabled but git is not installed. Disabling checkpointing.',
);
checkpointing = false;
}
}
const configParams: ConfigParameters = {
sessionId: taskId,
model: settings.general?.previewFeatures
@@ -93,7 +79,9 @@ export async function loadConfig(
folderTrust,
trustedFolder: true,
extensionLoader,
checkpointing,
checkpointing: process.env['CHECKPOINTING']
? process.env['CHECKPOINTING'] === 'true'
: settings.checkpointing?.enabled,
previewFeatures: settings.general?.previewFeatures,
interactive: true,
enableInteractiveShell: true,
+2 -41
View File
@@ -14,7 +14,7 @@ import type {
TaskStatusUpdateEvent,
SendStreamingMessageSuccessResponse,
} from '@a2a-js/sdk';
import express from 'express';
import type express from 'express';
import type { Server } from 'node:http';
import request from 'supertest';
import {
@@ -27,7 +27,7 @@ import {
it,
vi,
} from 'vitest';
import { createApp, main } from './app.js';
import { createApp } from './app.js';
import { commandRegistry } from '../commands/command-registry.js';
import {
assertUniqueFinalEventIsLast,
@@ -1176,43 +1176,4 @@ describe('E2E Tests', () => {
});
});
});
describe('main', () => {
it('should listen on localhost only', async () => {
const listenSpy = vi
.spyOn(express.application, 'listen')
.mockImplementation((...args: unknown[]) => {
// Trigger the callback passed to listen
const callback = args.find(
(arg): arg is () => void => typeof arg === 'function',
);
if (callback) {
callback();
}
return {
address: () => ({ port: 1234 }),
on: vi.fn(),
once: vi.fn(),
emit: vi.fn(),
} as unknown as Server;
});
// Avoid process.exit if possible, or mock it if main might fail
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
await main();
expect(listenSpy).toHaveBeenCalledWith(
expect.any(Number),
'localhost',
expect.any(Function),
);
listenSpy.mockRestore();
exitSpy.mockRestore();
});
});
});
+2 -2
View File
@@ -326,9 +326,9 @@ export async function createApp() {
export async function main() {
try {
const expressApp = await createApp();
const port = Number(process.env['CODER_AGENT_PORT'] || 0);
const port = process.env['CODER_AGENT_PORT'] || 0;
const server = expressApp.listen(port, 'localhost', () => {
const server = expressApp.listen(port, () => {
const address = server.address();
let actualPort;
if (process.env['CODER_AGENT_PORT']) {
-24
View File
@@ -11,30 +11,6 @@ import { FatalError, writeToStderr } from '@google/gemini-cli-core';
import { runExitCleanup } from './src/utils/cleanup.js';
// --- Global Entry Point ---
// Suppress known race condition error in node-pty on Windows
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
process.on('uncaughtException', (error) => {
if (
process.platform === 'win32' &&
error instanceof Error &&
error.message === 'Cannot resize a pty that has already exited'
) {
// This error happens on Windows with node-pty when resizing a pty that has just exited.
// It is a race condition in node-pty that we cannot prevent, so we silence it.
return;
}
// For other errors, we rely on the default behavior, but since we attached a listener,
// we must manually replicate it.
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
} else {
writeToStderr(String(error) + '\n');
}
process.exit(1);
});
main().catch(async (error) => {
await runExitCleanup();
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.25.2",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -26,10 +26,10 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.2"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@agentclientprotocol/sdk": "^0.11.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -37,7 +37,6 @@
"@types/update-notifier": "^6.0.8",
"ansi-regex": "^6.2.2",
"clipboardy": "^5.0.0",
"color-convert": "^2.0.1",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
"diff": "^7.0.0",
+9 -27
View File
@@ -54,33 +54,15 @@ describe('extensionsCommand', () => {
extensionsCommand.builder(mockYargs);
expect(mockYargs.middleware).toHaveBeenCalled();
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'install' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'uninstall' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'list' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'update' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'disable' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'enable' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'link' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'new' }),
);
expect(mockYargs.command).toHaveBeenCalledWith(
expect.objectContaining({ command: 'validate' }),
);
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'install' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'uninstall' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'list' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'update' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'disable' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'enable' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'link' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'new' });
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'validate' });
expect(mockYargs.demandCommand).toHaveBeenCalledWith(1, expect.any(String));
expect(mockYargs.version).toHaveBeenCalledWith(false);
});
+11 -15
View File
@@ -16,7 +16,6 @@ import { newCommand } from './extensions/new.js';
import { validateCommand } from './extensions/validate.js';
import { configureCommand } from './extensions/configure.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
import { defer } from '../deferred.js';
export const extensionsCommand: CommandModule = {
command: 'extensions <command>',
@@ -24,20 +23,17 @@ export const extensionsCommand: CommandModule = {
describe: 'Manage Gemini CLI extensions.',
builder: (yargs) =>
yargs
.middleware((argv) => {
initializeOutputListenersAndFlush();
argv['isCommand'] = true;
})
.command(defer(installCommand, 'extensions'))
.command(defer(uninstallCommand, 'extensions'))
.command(defer(listCommand, 'extensions'))
.command(defer(updateCommand, 'extensions'))
.command(defer(disableCommand, 'extensions'))
.command(defer(enableCommand, 'extensions'))
.command(defer(linkCommand, 'extensions'))
.command(defer(newCommand, 'extensions'))
.command(defer(validateCommand, 'extensions'))
.command(defer(configureCommand, 'extensions'))
.middleware(() => initializeOutputListenersAndFlush())
.command(installCommand)
.command(uninstallCommand)
.command(listCommand)
.command(updateCommand)
.command(disableCommand)
.command(enableCommand)
.command(linkCommand)
.command(newCommand)
.command(validateCommand)
.command(configureCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {
@@ -1,4 +0,0 @@
{
"name": "hooks-example",
"version": "1.0.0"
}
@@ -1,14 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node ${extensionPath}/scripts/on-start.js"
}
]
}
]
}
}
@@ -1,8 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
console.log(
'Session Started! This is running from a script in the hooks-example extension.',
);
@@ -1,35 +0,0 @@
# MCP Server Example
This is a basic example of an MCP (Model Context Protocol) server used as a
Gemini CLI extension. It demonstrates how to expose tools and prompts to the
Gemini CLI.
## Description
The contents of this directory are a valid MCP server implementation using the
`@modelcontextprotocol/sdk`. It exposes:
- A tool `fetch_posts` that mock-fetches posts.
- A prompt `poem-writer`.
## Structure
- `example.js`: The main server entry point.
- `gemini-extension.json`: The configuration file that tells Gemini CLI how to
use this extension.
- `package.json`: Helper for dependencies.
## How to Use
1. Navigate to this directory:
```bash
cd packages/cli/src/commands/extensions/examples/mcp-server
```
2. Install dependencies:
```bash
npm install
```
This example is typically used by `gemini extensions new`.
@@ -0,0 +1,135 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// Mock the MCP server and transport
const mockRegisterTool = vi.fn();
const mockRegisterPrompt = vi.fn();
const mockConnect = vi.fn();
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
McpServer: vi.fn().mockImplementation(() => ({
registerTool: mockRegisterTool,
registerPrompt: mockRegisterPrompt,
connect: mockConnect,
})),
}));
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
StdioServerTransport: vi.fn(),
}));
describe('MCP Server Example', () => {
beforeEach(async () => {
// Dynamically import the server setup after mocks are in place
await import('./example.js');
});
afterEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
it('should create an McpServer with the correct name and version', () => {
expect(McpServer).toHaveBeenCalledWith({
name: 'prompt-server',
version: '1.0.0',
});
});
it('should register the "fetch_posts" tool', () => {
expect(mockRegisterTool).toHaveBeenCalledWith(
'fetch_posts',
{
description: 'Fetches a list of posts from a public API.',
inputSchema: z.object({}).shape,
},
expect.any(Function),
);
});
it('should register the "poem-writer" prompt', () => {
expect(mockRegisterPrompt).toHaveBeenCalledWith(
'poem-writer',
{
title: 'Poem Writer',
description: 'Write a nice haiku',
argsSchema: expect.any(Object),
},
expect.any(Function),
);
});
it('should connect the server to an StdioServerTransport', () => {
expect(StdioServerTransport).toHaveBeenCalled();
expect(mockConnect).toHaveBeenCalledWith(expect.any(StdioServerTransport));
});
describe('fetch_posts tool implementation', () => {
it('should fetch posts and return a formatted response', async () => {
const mockPosts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
];
global.fetch = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(mockPosts),
});
const toolFn = mockRegisterTool.mock.calls[0][2];
const result = await toolFn();
expect(global.fetch).toHaveBeenCalledWith(
'https://jsonplaceholder.typicode.com/posts',
);
expect(result).toEqual({
content: [
{
type: 'text',
text: JSON.stringify({ posts: mockPosts }),
},
],
});
});
});
describe('poem-writer prompt implementation', () => {
it('should generate a prompt with a title', () => {
const promptFn = mockRegisterPrompt.mock.calls[0][2];
const result = promptFn({ title: 'My Poem' });
expect(result).toEqual({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'Write a haiku called My Poem. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables ',
},
},
],
});
});
it('should generate a prompt with a title and mood', () => {
const promptFn = mockRegisterPrompt.mock.calls[0][2];
const result = promptFn({ title: 'My Poem', mood: 'sad' });
expect(result).toEqual({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'Write a haiku with the mood sad called My Poem. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables ',
},
},
],
});
});
});
});
@@ -4,7 +4,7 @@
"mcpServers": {
"nodeServer": {
"command": "node",
"args": ["${extensionPath}${/}example.js"],
"args": ["${extensionPath}${/}dist${/}example.js"],
"cwd": "${extensionPath}"
}
}
@@ -4,6 +4,13 @@
"description": "Example MCP Server for Gemini CLI Extension",
"type": "module",
"main": "example.js",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "~5.4.5",
"@types/node": "^20.11.25"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
"zod": "^3.22.4"
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist"
},
"include": ["example.ts"]
}
@@ -1,4 +0,0 @@
{
"name": "skills-example",
"version": "1.0.0"
}
@@ -1,7 +0,0 @@
---
name: greeter
description: A friendly greeter skill
---
You are a friendly greeter. When the user says "hello" or asks for a greeting,
you should reply with: "Greetings from the skills-example extension! 👋"
+1 -4
View File
@@ -14,10 +14,7 @@ export const hooksCommand: CommandModule = {
describe: 'Manage Gemini CLI hooks.',
builder: (yargs) =>
yargs
.middleware((argv) => {
initializeOutputListenersAndFlush();
argv['isCommand'] = true;
})
.middleware(() => initializeOutputListenersAndFlush())
.command(migrateCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
@@ -511,5 +511,8 @@ describe('migrate command', () => {
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'Note: Set hooks.enabled to true in your settings to enable the hook system.',
);
});
});
+5 -4
View File
@@ -230,10 +230,8 @@ export async function handleMigrateFromClaude() {
const settings = loadSettings(workingDir);
// Merge migrated hooks with existing hooks
const existingHooks = (settings.merged?.hooks || {}) as Record<
string,
unknown
>;
const existingHooks =
(settings.merged.hooks as Record<string, unknown>) || {};
const mergedHooks = { ...existingHooks, ...migratedHooks };
// Update settings (setValue automatically saves)
@@ -244,6 +242,9 @@ export async function handleMigrateFromClaude() {
debugLogger.log(
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
);
debugLogger.log(
'Note: Set hooks.enabled to true in your settings to enable the hook system.',
);
} catch (error) {
debugLogger.error(`Error saving migrated hooks: ${getErrorMessage(error)}`);
}
+1 -3
View File
@@ -61,7 +61,7 @@ describe('mcp command', () => {
(mcpCommand.builder as (y: Argv) => Argv)(mockYargs as unknown as Argv);
expect(mockYargs.command).toHaveBeenCalledTimes(5);
expect(mockYargs.command).toHaveBeenCalledTimes(3);
// Verify that the specific subcommands are registered
const commandCalls = mockYargs.command.mock.calls;
@@ -70,8 +70,6 @@ describe('mcp command', () => {
expect(commandNames).toContain('add <name> <commandOrUrl> [args...]');
expect(commandNames).toContain('remove <name>');
expect(commandNames).toContain('list');
expect(commandNames).toContain('enable <name>');
expect(commandNames).toContain('disable <name>');
expect(mockYargs.demandCommand).toHaveBeenCalledWith(
1,

Some files were not shown because too many files have changed in this diff Show More