Compare commits

...

170 Commits

Author SHA1 Message Date
gemini-cli-robot b56a111594 chore(release): v0.24.0 2026-01-14 13:22:02 +00:00
gemini-cli-robot 48ee9bb308 chore(release): v0.24.0-preview.3 2026-01-14 06:24:32 +00:00
gemini-cli-robot 4b2701195a fix(patch): cherry-pick eda47f5 to release/v0.24.0-preview.2-pr-16557 [CONFLICTS] (#16577)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
2026-01-14 14:04:14 +08:00
gemini-cli-robot df72e3af3d chore(release): v0.24.0-preview.2 2026-01-13 23:34:59 +00:00
gemini-cli-robot 334b813d81 fix(patch): cherry-pick 356f76e to release/v0.24.0-preview.1-pr-16252 to patch version v0.24.0-preview.1 and create version 0.24.0-preview.2 (#16552)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
2026-01-13 22:40:08 +00:00
gemini-cli-robot 314f67a326 chore(release): v0.24.0-preview.1 2026-01-13 05:44:29 +00:00
gemini-cli-robot 0f0d1d8fc0 fix(patch): cherry-pick b54e688 to release/v0.24.0-preview.0-pr-16284 to patch version v0.24.0-preview.0 and create version 0.24.0-preview.1 (#16466)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-01-13 11:43:02 +08:00
gemini-cli-robot cb15a238fe chore(release): v0.24.0-preview.0 2026-01-07 01:43:58 +00:00
N. Taylor Mullen 59a18e710d Agent Skills: Initial Documentation & Tutorial (#15869) 2026-01-07 00:59:56 +00:00
Krushna Korade 5f027cb63a fix: hide broken skills object from settings dialog (#15766) 2026-01-07 00:55:13 +00:00
Christian Gunderman 4086abf375 Fix test. (#16011) 2026-01-07 00:25:16 +00:00
Jerop Kipruto 8f5bf33eac ci(github-actions): triage all new issues automatically (#16018) 2026-01-07 00:15:00 +00:00
Jerop Kipruto 7eeb7bd74c fix: limit scheduled issue triage queries to prevent argument list too long error (#16021) 2026-01-07 00:03:23 +00:00
Adam Weidman 96b9be3ec4 feat(agents): add support for remote agents (#16013) 2026-01-06 23:45:05 +00:00
joshualitt 1e31427da8 Remove trailing whitespace in yaml. (#16036) 2026-01-06 15:41:58 -08:00
Bryan Morgan 7feb2f8f42 Add 'reopened' type to issue labeling workflow 2026-01-06 18:17:12 -05:00
Bryan Morgan 2122604b32 Refactor parent issue check to use URLs 2026-01-06 18:00:39 -05:00
Bryan Morgan d4b4aede2f Add debugging logs for issue parent checks
Added debugging logs to inspect issue object and parent information.
2026-01-06 17:56:39 -05:00
Bryan Morgan 4b5c044272 Fix label-backlog-child-issues workflow logic 2026-01-06 17:48:08 -05:00
Shreya Keshive 2fe45834dd feat(admin): Introduce remote admin settings & implement secureModeEnabled/mcpEnabled (#15935) 2026-01-06 21:38:07 +00:00
joshualitt 56092bd782 feat(hooks): Add a hooks.enabled setting. (#15933) 2026-01-06 21:33:37 +00:00
Jack Wotherspoon c31f05356a fix: image token estimation (#16004) 2026-01-06 21:19:29 +00:00
Abhi 61dbab03e0 feat(ui): add visual indicators for hook execution (#15408) 2026-01-06 20:52:12 +00:00
Bryan Morgan 86b5995f12 Add workflow to label child issues for rollup (#16002) 2026-01-06 16:00:47 -05:00
Tommaso Sciortino d2849fda8a properly disable keyboard modes on exit (#16006) 2026-01-06 20:11:43 +00:00
Tommaso Sciortino a61fb058b7 fix: writeTodo construction (#16014) 2026-01-06 20:08:25 +00:00
christine betts 7edd803034 Fix settings command fallback (#15926) 2026-01-06 19:50:27 +00:00
N. Taylor Mullen 0c54136244 Agent Skills: Add skill directory to WorkspaceContext upon activation (#15870) 2026-01-06 19:24:37 +00:00
Ishaan Gupta cce4574143 Use GetOperation to poll for OnboardUser completion (#15827)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-01-06 19:08:59 +00:00
Sehoon Shon 9172e28315 Add description for each settings item in /settings (#15936) 2026-01-06 19:06:15 +00:00
Gal Zahavi 6f4b2ad0b9 fix: default folder trust to untrusted for enhanced security (#15943) 2026-01-06 18:09:09 +00:00
N. Taylor Mullen 9a3ff6510f feat(policy): allow 'modes' in user and admin policies (#15977) 2026-01-06 17:56:50 +00:00
christine betts 4c67eef0f2 Inform user of missing settings on extensions update (#15944)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-06 17:26:01 +00:00
ppergame 7d4f97de7a fix(core): use correct interactive check for system prompt (#15020)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: Billy Biggs <bbiggs@google.com>
2026-01-06 16:21:36 +00:00
Sehoon Shon cbd2eee9c4 remove manual string when displaying manual model in the footer (#15967) 2026-01-06 04:07:44 +00:00
Tommaso Sciortino 384fb6a465 Add setting to support OSC 52 paste (#15336) 2026-01-06 00:11:50 +00:00
N. Taylor Mullen 2cb33b2f76 Agent Skills: Implement /skills reload (#15865) 2026-01-05 23:12:51 +00:00
Tommaso Sciortino 8f0324d868 fix(cli): resolve paste issue on Windows terminals. (#15932) 2026-01-05 22:46:23 +00:00
Christian Gunderman fd7b6bf40a Fix failing unit tests (#15940) 2026-01-05 21:41:35 +00:00
Tommaso Sciortino ed8bad8c26 Fix order of preflight (#15941) 2026-01-05 21:32:02 +00:00
Christian Gunderman 6d1e27633a Support context injection via SessionStart hook. (#15746) 2026-01-05 21:27:53 +00:00
Manoj Naik 2da911e4a0 fix: prevent /copy crash on Windows by skipping /dev/tty (#15657)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-01-05 20:33:03 +00:00
Sehoon Shon e5d183031a Opt-in to persist model from /model (#15820) 2026-01-05 20:31:13 +00:00
Abhi b13c6b57ae chore: rename smart-edit to edit (#15923) 2026-01-05 20:25:54 +00:00
Dev Randalpura 3c92666ec2 Make default settings apply (#15354) 2026-01-05 20:00:20 +00:00
Vedant Mahajan dc6dda5c37 fix: avoid SDK warning by not accessing .text getter in logging (#15706)
Co-authored-by: Ishaan Gupta <ishaankone@gmail.com>
2026-01-05 19:03:00 +00:00
Christian Gunderman 3997c7ff80 Fix terminal hang when user exits browser without logging in (#15748) 2026-01-05 19:02:55 +00:00
Abhi b4b49e7029 refactor(core): Extract and integrate ToolExecutor (#15900) 2026-01-05 05:48:41 +00:00
N. Taylor Mullen 615b218ff7 fix(cli): mock fs.readdir in consent tests for Windows compatibility (#15904) 2026-01-04 21:19:33 -08:00
Abhi f3625aab13 refactor: consolidate EditTool and SmartEditTool (#15857) 2026-01-05 04:52:14 +00:00
N. Taylor Mullen d3563e2f0e Agent Skills: Add gemini skills CLI management command (#15837) 2026-01-05 04:40:21 +00:00
Sandy Tao dd84c2fb83 feat(hooks): implement granular stop and block behavior for agent hooks (#15824) 2026-01-05 02:58:34 +00:00
N. Taylor Mullen bdb349e7f6 Agent Skills: Extension Support & Security Disclosure (#15834) 2026-01-04 22:45:07 +00:00
Abhi 12c7c9cc42 feat(core,cli): enforce mandatory MessageBus injection (Phase 3 Hard Migration) (#15776) 2026-01-04 22:11:43 +00:00
Abhi 90be9c3587 feat(core): Standardize Tool and Agent Invocation constructors (Phase 2) (#15775) 2026-01-04 20:51:23 +00:00
Abhi eec5d5ebf8 feat(core): restore MessageBus optionality for soft migration (Phase 1) (#15774) 2026-01-04 19:59:35 +00:00
Abhi d3c206c677 Unify shell security policy and remove legacy logic (#15770) 2026-01-04 05:19:00 +00:00
N. Taylor Mullen f0a039f7c0 Agent Skills: Unify Representation & Centralize Loading (#15833) 2026-01-03 16:24:36 -08:00
Gal Zahavi 30f5c4af4a fix(core): mock powershell output in shell-utils test (#15831) 2026-01-02 23:20:26 +00:00
N. Taylor Mullen e78c3fe4f0 Agent Skills: Status Bar Integration for Skill Counts (#15741) 2026-01-02 23:01:31 +00:00
Abhi c0ccb22460 chore: cleanup old smart edit settings (#15832) 2026-01-02 22:28:28 +00:00
N. Taylor Mullen 764b195977 Agent Skills: Implement Agent Integration and System Prompt Awareness (#15728) 2026-01-02 21:47:46 +00:00
Jack Wotherspoon 0f3555a4d2 feat: add folder suggestions to /dir add command (#15724) 2026-01-02 21:08:11 +00:00
Abhi 006de1dd31 Add security docs (#15739) 2026-01-02 20:22:30 +00:00
Gal Zahavi 18fef0db31 fix(core): improve shell command with redirection detection (#15683) 2026-01-02 19:36:59 +00:00
N. Taylor Mullen 958284dc24 Agent Skills: Implement Autonomous Activation Tool & Context Injection (#15725) 2026-01-02 19:15:06 +00:00
Sehoon Shon 788bb04f5c log fallback mode (#15817) 2026-01-02 18:19:43 +00:00
HyeongHo Jun 8a0190ca3b fix(core): handle unhandled promise rejection in mcp-client-manager (#14701) 2026-01-02 16:06:56 +00:00
Christian Gunderman c29a8c12b3 Fix build issues caused by year-specific linter rule (#15780) 2026-01-01 19:01:03 +00:00
Sandy Tao 05049b5abf feat(hooks): implement STOP_EXECUTION and enhance hook decision handling (#15685) 2025-12-30 23:22:53 +00:00
Sehoon Shon 3ebe4e6a8f feat(agents): Add remote agents to agent registry (#15711) 2025-12-30 23:11:51 +00:00
Abhi 0eb84f5133 chore: remove cot style comments (#15735) 2025-12-30 21:43:33 +00:00
N. Taylor Mullen de1233b8ca Agent Skills: Implement Core Skill Infrastructure & Tiered Discovery (#15698) 2025-12-30 13:35:52 -08:00
christine betts ec11b8afbf Add extension settings info to /extensions list (#14905) 2025-12-30 21:09:48 +00:00
christine betts ec79fe1ab2 Add instructions to the extensions update info notification (#14907) 2025-12-30 19:50:33 +00:00
Christian Gunderman 90eb1e0281 Implement support for tool input modification (#15492) 2025-12-30 19:37:43 +00:00
Abhi 15c9f88da6 fix(hooks): deduplicate agent hooks and add cross-platform integration tests (#15701) 2025-12-30 19:13:16 +00:00
Abhi 4e6fee7fcd Revert "Update wittyPhrases.ts (#15697)" (#15719) 2025-12-30 16:50:28 +00:00
Shreya Keshive 1e08b150f7 refactor(auth): Refactor non-interactive mode auth validation & refresh (#15679) 2025-12-30 16:09:00 +00:00
segyges fb22f5b8ee Update wittyPhrases.ts (#15697)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2025-12-30 07:18:44 +00:00
Adam Weidman 848e8485cd feat(agents): add support for remote agents and multi-agent TOML files (#15437) 2025-12-30 00:12:16 +00:00
Sehoon Shon 07e597de40 Exponential back-off retries for retryable error without a specified … (#15684) 2025-12-29 22:23:23 +00:00
Adib234 10ae84869a Migrate console to coreEvents.emitFeedback or debugLogger (#15219) 2025-12-29 20:46:10 +00:00
Abhi dcd2449b1a refactor: deprecate legacy confirmation settings and enforce Policy Engine (#15626) 2025-12-29 19:22:42 +00:00
gemini-cli-robot a3d214f8d7 chore/release: bump version to 0.24.0-nightly.20251227.37be16243 (#15612) 2025-12-27 05:29:40 +00:00
Abhi 37be162435 fix(core): enable granular shell command allowlisting in policy engine (#15601) 2025-12-26 23:48:44 +00:00
Abhi 5566292cc8 refactor(core): extract static concerns from CoreToolScheduler (#15589) 2025-12-26 20:51:39 +00:00
Gal Zahavi 65e2144b3d Manual nightly version bump to 0.24.0-nightly.20251226.546baf993 (#15594) 2025-12-26 19:40:46 +00:00
Maple! a26d195404 fix(cli): add enableShellOutputEfficiency to settings schema (#15560) 2025-12-26 19:07:33 +00:00
Maple! 21388a0a40 fix(core): handle checkIsRepo failure in GitService.initialize (#15574) 2025-12-26 18:37:57 +00:00
Adib234 acecd80afa Resolve unhandled promise rejection in ide-client.ts (#15587) 2025-12-26 16:37:13 +00:00
Sehoon Shon 69fc75c0b2 do not persist the fallback model (#15483) 2025-12-26 16:08:44 +00:00
Sehoon Shon 9cdb267ba5 feat: Show snowfall animation for holiday theme (#15494)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2025-12-26 16:07:39 +00:00
Maple! 56b050422d chore(core): fix comment typo (#15558) 2025-12-26 04:31:43 +00:00
Vedant Mahajan 546baf9934 Added modifyOtherKeys protocol support for tmux (#15524)
Co-authored-by: Ishaan Gupta <ishaankone@gmail.com>
2025-12-25 19:09:38 +00:00
Sehoon Shon e6344a8c24 Security: Project-level hook warnings (#15470) 2025-12-23 21:10:46 +00:00
Pyush Sinha 873d10df42 feat: terse transformations of image paths in text buffer (#4924)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
Co-authored-by: owenofbrien <86964623+owenofbrien@users.noreply.github.com>
2025-12-23 20:46:09 +00:00
Adam Weidman 02a36afc38 feat: Add A2A Client Manager and tests (#15485) 2025-12-23 20:27:16 +00:00
christine betts 563d81e08e Add experimental in-CLI extension install and uninstall subcommands (#15178)
Co-authored-by: Christine Betts <chrstn@google.com>
2025-12-23 19:48:27 +00:00
Jack Wotherspoon 5f28614760 chore: limit MCP resources display to 10 by default (#15489) 2025-12-23 19:39:35 +00:00
Jacob Richman b6b0727e28 Make schema validation errors non-fatal (#15487) 2025-12-23 18:26:37 +00:00
Jack Wotherspoon e9a601c1fe fix: add missing type field to MCPServerConfig (#15465) 2025-12-23 16:59:32 +00:00
Stoyan Dimitrov 308aa70718 refactor(core): remove deprecated permission aliases from BeforeToolHookOutput (#14855)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
2025-12-23 16:13:20 +00:00
Niyas Hameed 6be034392f feat: automatic /model persistence across Gemini CLI sessions (#13199)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2025-12-23 14:23:43 +00:00
Christian Gunderman 3b1dbcd42d Implemented unified secrets sanitization and env. redaction options (#15348) 2025-12-23 03:18:27 +00:00
Jack Wotherspoon 2ac9fe08f7 chore: remove clipboard file (#15447) 2025-12-22 23:04:02 +00:00
Jack Wotherspoon 24c722454b chore: improve error messages for --resume (#15360) 2025-12-22 22:44:41 +00:00
ElecTwix 0a216b28f3 fix #15369, prevent crash on unhandled EIO error in readStdin cleanup (#15410)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
2025-12-22 20:50:43 +00:00
Sandy Tao 9c48cd849b feat(ui): Add security warning and improve layout for Hooks list (#15440) 2025-12-22 20:47:56 +00:00
N. Taylor Mullen 0843d9af58 fix(core): use debugLogger.debug for startup profiler logs (#15443) 2025-12-22 20:45:33 +00:00
Abhi b0d5c4c058 feat(policy): implement dynamic mode-aware policy evaluation (#15307) 2025-12-22 20:25:07 +00:00
Christian Gunderman d18c96d6a1 Record timestamp with code assist metrics. (#15439) 2025-12-22 20:04:06 +00:00
Sehoon Shon dced409ac4 Add Folder Trust Support To Hooks (#15325) 2025-12-22 16:46:38 +00:00
Adam Weidman d6a2f1d670 chore(core): refactor model resolution and cleanup fallback logic (#15228) 2025-12-22 15:18:51 +00:00
Serghei 58fd00a3df fix(core): Add .geminiignore support to SearchText tool (#13763)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
2025-12-22 04:25:26 +00:00
Abhi b923604602 feat(telemetry): add clearcut logging for hooks (#15405) 2025-12-21 21:18:42 +00:00
gemini-cli-robot 8643d60b88 chore/release: bump version to 0.21.0-nightly.20251220.41a1a3eed (#15352) 2025-12-21 03:59:46 +00:00
Ying-xi 8feeffb29b fix: prevent infinite relaunch loop when --resume fails (#14941) (#14951)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2025-12-20 03:22:01 +00:00
Jacob Richman e64146914a fix(policy): fix bug where accepting-edits continued after it was turned off (#15351) 2025-12-20 01:09:43 +00:00
Gal Zahavi 6084708cc2 fix(cli): fix right border overflow in trust dialogs (#15350) 2025-12-20 00:19:03 +00:00
Adam Weidman 8ed0f8981f fix(folder trust): add validation for trusted folder level (#12215)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
2025-12-20 00:13:03 +00:00
Sandy Tao 41a1a3eed1 fix(core): sanitize hook command expansion and prevent injection (#15343) 2025-12-19 22:31:42 +00:00
Tommaso Sciortino 10ba348a3a Introspection agent demo (#15232) 2025-12-19 22:11:32 +00:00
Tommaso Sciortino db67bb106a more robust command parsing logs (#15339) 2025-12-19 21:53:13 +00:00
Shehab b828b4754b docs(cli): add System Prompt Override (GEMINI_SYSTEM_MD) (#9515) 2025-12-19 20:11:37 +00:00
Gal Zahavi 9383b54d50 Validate OAuth resource parameter matches MCP server URL (#15289) 2025-12-19 19:33:20 +00:00
JAYADITYA 181da07dd9 fix(cli):change the placeholder of input during the shell mode (#15135)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
2025-12-19 18:07:45 +00:00
Jacob Richman 419464a8c2 feat(ui): Put "Allow for all future sessions" behind a setting off by default. (#15322) 2025-12-19 17:25:23 +00:00
Sandy Tao 2e229d3bb6 feat(core): Implement JIT context memory loading and UI sync (#14469) 2025-12-19 17:04:03 +00:00
gemini-cli-robot 3c92bdb1ad chore/release: bump version to 0.21.0-nightly.20251219.70696e364 (#15301) 2025-12-19 16:17:12 +00:00
N. Taylor Mullen e0f159085e feat: simplify tool confirmation labels for better UX (#15296) 2025-12-19 00:38:53 +00:00
Abhi 402148dbc4 feat(hooks): reduce log verbosity and improve error reporting in UI (#15297) 2025-12-19 00:30:45 +00:00
Sandy Tao 70696e364b fix(ui): show command suggestions even on perfect match and sort them (#15287) 2025-12-19 00:05:36 +00:00
Gal Zahavi 1e10492e55 fix: prevent infinite loop in prompt completion on error (#14548) 2025-12-18 23:34:46 +00:00
Sehoon Shon 9e6914d641 Handle all 429 as retryableQuotaError (#15288) 2025-12-18 23:25:42 +00:00
Tommaso Sciortino 0c4fb6afd2 Remove unnecessary dependencies (#15291) 2025-12-18 23:24:19 +00:00
Tommaso Sciortino edab97997a Remove unused code (#15290) 2025-12-18 23:04:29 +00:00
Gal Zahavi 2515b89e2b feat: Pass additional environment variables to shell execution (#15160) 2025-12-18 22:01:23 +00:00
Tommaso Sciortino 60f0f19d76 add 3.0 to allowed sensitive keywords (#15276) 2025-12-18 18:58:07 +00:00
Jacob Richman 322232e514 feat: Detect background color (#15132) 2025-12-18 18:36:48 +00:00
Abhi 54466a3ea8 feat(hooks): add support for friendly names and descriptions (#15174) 2025-12-18 16:09:24 +00:00
Abhi 7da060c149 (docs): Add reference section to hooks documentation (#15159) 2025-12-18 15:49:05 +00:00
Jacob Richman 088f48d60f Add core tool that adds all context from the core package. (#15238) 2025-12-18 15:45:35 +00:00
Abhi 2b426c1d91 feat: add agent toml parser (#15112) 2025-12-18 03:46:55 +00:00
Jacob Richman bc168bbae4 Change detailed model stats to use a new shared Table class to resolve robustness issues. (#15208) 2025-12-18 02:01:37 +00:00
gemini-cli-robot 124a6da743 chore/release: bump version to 0.21.0-nightly.20251218.739c02bd6 (#15231) 2025-12-17 16:54:26 -08:00
Sandy Tao 739c02bd6d fix(cli): correct initial history length handling for chat commands (#15223) 2025-12-18 00:04:02 +00:00
Christian Gunderman 5d13145995 Code Assist backend telemetry for user accept/reject of suggestions (#15206) 2025-12-17 23:12:59 +00:00
Jenna Inouye c28ff3d5a5 Docs: Update Changelog for Dec 17, 2025 (#15204)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-17 23:11:22 +00:00
Sandy Tao 6ddd5abd7b fix(ui): Prevent eager slash command completion hiding sibling commands (#15224) 2025-12-17 22:30:35 +00:00
Adam Weidman bf6d0485ce chore(core): remove legacy fallback flags and migrate loop detection (#15213) 2025-12-17 22:14:33 +00:00
Jacob Richman 3d486ec1e9 feat(ui): add Windows clipboard image support and Alt+V paste workaround (#15218)
Co-authored-by: sgeraldes <sgeraldes@users.noreply.github.com>
2025-12-17 22:05:25 +00:00
Sehoon Shon a6d1245a54 Respect previewFeatures value from the remote flag if undefined (#15214) 2025-12-17 21:36:50 +00:00
joshualitt de7e1937f6 feat(core): Late resolve GenerateContentConfigs and reduce mutation. (#14920) 2025-12-17 20:29:26 +00:00
Adam Weidman b465e12747 chore(core): remove redundant isModelAvailabilityServiceEnabled toggle and clean up dead code (#15207) 2025-12-17 19:23:56 +00:00
Adam Weidman 948401a450 chore: update a2a-js to 0.3.7 (#15197) 2025-12-17 18:07:52 +00:00
Adib234 bb8f181ef1 Refactor: Migrate console.error in ripGrep.ts to debugLogger (#15201) 2025-12-17 17:40:28 +00:00
Tommaso Sciortino bf90b59935 feat: launch Gemini 3 Flash in Gemini CLI ️ (#15196)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
Co-authored-by: joshualitt <joshualitt@google.com>
Co-authored-by: Sehoon Shon <sshon@google.com>
Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com>
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
Co-authored-by: Jenna Inouye <jinouye@google.com>
2025-12-17 09:43:21 -08:00
Daan Versavel 18698d6929 (fix) Automated pr labeller (#14885) 2025-12-17 17:17:26 +00:00
Scars 80c4225286 feat(cli): Add /auth logout command to clear credentials and auth state (#13383) 2025-12-17 17:07:13 +00:00
Adam Weidman d02f3f6809 feat(core): introduce remote agent infrastructure and rename local executor (#15110) 2025-12-17 17:06:38 +00:00
Jacob Richman da85aed5aa Add one to the padding in settings dialog to avoid flicker. (#15173) 2025-12-17 16:53:51 +00:00
Christian Gunderman 7f2d33458a Disallow unnecessary awaits. (#15172) 2025-12-17 05:28:18 +00:00
Jack Wotherspoon 3e9a0a7628 chore: remove user query from footer in debug mode (#15169) 2025-12-17 01:44:04 +00:00
Jack Wotherspoon 26c115a4fb chore: remove repo specific tips (#15164) 2025-12-17 00:34:42 +00:00
Jacob Richman db643e9166 Remove foreground for themes other than shades of purple and holiday. (#14606)
Co-authored-by: Miguel Solorio <miguelsolorio@google.com>
2025-12-16 23:44:46 +00:00
Steffen Deusch ba100642e3 Use official ACP SDK and support HTTP/SSE based MCP servers (#13856) 2025-12-16 23:28:27 +00:00
Roaim cc52839f19 Docs (#15103)
Co-authored-by: Abhi <abhipatel@google.com>
2025-12-16 21:51:47 +00:00
gemini-cli-robot e79b149985 chore/release: bump version to 0.21.0-nightly.20251216.bb0c0d8ee (#15121) 2025-12-16 17:46:42 +00:00
Christian Gunderman 5e21c8c03c Code assist service metrics. (#15024) 2025-12-16 17:34:05 +00:00
545 changed files with 33065 additions and 13731 deletions
+29
View File
@@ -0,0 +1,29 @@
description="Injects context of all relevant cli files"
prompt = """
The following output contains the complete
source code of packages/core.
**Pay extremely close attention to these files.** They define the project's
core architecture, component patterns, and testing standards.
The source code contains the content of absolutely every source code file in
packages/core.
You should very rarely need to read any other files from packages/core to resolve
prompts.
!{find packages/core \\( -path packages/cli/dist -o -path packages/core/dist -o -name node_modules \\) -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\;}
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines when writing code in packages/core.
## Configuration & Settings
* **Settings vs Args**: Use settings for user-configurable options; do not add new CLI arguments.
* **Schema**: Add new settings to `packages/cli/src/config/settingsSchema.ts`.
* **Documentation**:
* If `showInDialog: true`, document in `docs/get-started/configuration.md`.
* Ensure `requiresRestart` is correctly set.
## General
* **Logging**: Use `debugLogger` for errors. NEVER leave `console.log/warn/error` in the code.
* **TypeScript**: Avoid the non-null assertion operator (`!`).
{{args}}.
"""
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- id: 'merge-queue-ci-skipper'
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
with:
secret: '${{ secrets.GITHUB_TOKEN }}'
secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
lint:
name: 'Lint'
@@ -39,7 +39,6 @@ jobs:
github.event_name == 'workflow_dispatch' ||
(
(github.event_name == 'issues' || github.event_name == 'issue_comment') &&
contains(github.event.issue.labels.*.name, 'status/need-triage') &&
(github.event_name != 'issue_comment' || (
contains(github.event.comment.body, '@gemini-cli /triage') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
@@ -75,11 +74,6 @@ jobs:
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number }}'
LABELS: '${{ steps.get_issue_data.outputs.labels }}'
run: |
if ! echo "${LABELS}" | grep -q 'status/need-triage'; then
echo "Issue #${ISSUE_NUMBER_INPUT} does not have the 'status/need-triage' label. Stopping workflow."
exit 1
fi
if echo "${LABELS}" | grep -q 'area/'; then
echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' label. Stopping workflow."
exit 1
@@ -32,7 +32,7 @@ jobs:
- name: 'Run Gemini PR size and complexity labeller'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # Use the specific commit SHA
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
PR_NUMBER: '${{ github.event.pull_request.number }}'
REPOSITORY: '${{ github.repository }}'
with:
@@ -45,11 +45,11 @@ jobs:
echo '🔍 Finding issues without labels...'
NO_LABEL_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue no:label' --json number,title,body)"
--search 'is:open is:issue no:label' --limit 10 --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 1000 --json number,title,body)"
--search "is:open is:issue label:\"status/need-triage\"" --limit 10 --json number,title,body)"
echo '🔄 Merging and deduplicating issues...'
ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
@@ -0,0 +1,40 @@
name: 'Label Child Issues for Project Rollup'
on:
issues:
types: ['opened', 'edited', 'reopened']
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 issue = context.payload.issue;
const labelToAdd = 'workstream-rollup';
// --- 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'
];
// 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.`);
}
+1
View File
@@ -55,3 +55,4 @@ gha-creds-*.json
patch_output.log
.genkit
.gemini-clipboard/
+9
View File
@@ -391,6 +391,15 @@ When working in the `/docs` directory, follow the guidelines in this section:
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
+36 -2
View File
@@ -18,6 +18,41 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.21.0 - 2025-12-15
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** Better, faster and cheaper than 2.5
Pro - and in some scenarios better than 3 Pro! For paid tiers + free tier
users who were on the wait list enable **Preview Features** in `/settings.`
- For more information:
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
- 🎉 Gemini CLI Extensions:
- Rill: Utilize natural language to analyze Rill data, enabling the
exploration of metrics and trends without the need for manual queries.
`gemini extensions install https://github.com/rilldata/rill-gemini-extension`
- Browserbase: Interact with web pages, take screenshots, extract information,
and perform automated actions with atomic precision.
`gemini extensions install https://github.com/browserbase/mcp-server-browserbase`
- Quota Visibility: The `/stats` command now displays quota information for all
available models, including those not used in the current session. (@sehoon38)
- Fuzzy Setting Search: Users can now quickly find settings using fuzzy search
within the settings dialog. (@sehoon38)
- MCP Resource Support: Users can now discover, view, and search through
resources using the @ command. (@MrLesk)
- Auto-execute Simple Slash Commands: Simple slash commands are now executed
immediately on enter. (@jackwotherspoon)
## Announcements: v0.20.0 - 2025-12-01
- **Multi-file Drag & Drop:** Users can now drag and drop multiple files into
the terminal, and the CLI will automatically prefix each valid path with `@`.
([pr](https://github.com/google-gemini/gemini-cli/pull/14832) by
[@jackwotherspoon](https://github.com/jackwotherspoon))
- **Persistent "Always Allow" Policies:** Users can now save "Always Allow"
decisions for tool executions, with granular control over specific shell
commands and multi-cloud platform tools.
([pr](https://github.com/google-gemini/gemini-cli/pull/14737) by
[@allenhutchison](https://github.com/allenhutchison))
## Announcements: v0.19.0 - 2025-11-24
- 🎉 **New extensions:**
@@ -54,8 +89,7 @@ on GitHub.
- **Experimental permission improvements:** We are now experimenting with a new
policy engine in Gemini CLI. This allows users and administrators to create
fine-grained policy for tool calls. Currently behind a flag. See
[https://geminicli.com/docs/core/policy-engine/](../core/policy-engine.md) for
more information.
[policy engine documentation](../core/policy-engine.md) for more information.
- Blog:
[https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/](https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/)
- **Gemini 3 support for paid:** Gemini 3 support has been rolled out to all API
+209 -138
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.19.0 - v0.19.4
# Latest stable release: v0.21.0 - v0.21.1
Released: December 1, 2025
Released: December 16, 2025
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,144 +11,215 @@ npm install -g @google/gemini-cli
## Highlights
- **Zed integration:** Users can now leverage Gemini 3 within the Zed
integration after enabling "Preview Features" in their CLIs `/settings`.
- **Interactive shell:**
- **Click-to-Focus:** Go to `/settings` and enable **Use Alternate Buffer** to
click within the embedded shell output to focus it for input.
- **Loading phrase:** Clearly indicates when the interactive shell is awaiting
user input. ([vid](https://imgur.com/a/kjK8bUK),
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
[@jackwotherspoon](https://github.com/jackwotherspoon))
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** If you are a paid user, you can now
enable Gemini 3 Pro and Gemini 3 Flash. Go to `/settings` and set **Preview
Features** to `true` to enable Gemini 3. For more information:
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
## What's Changed
- Use lenient MCP output schema validator by @cornmander in
https://github.com/google-gemini/gemini-cli/pull/13521
- Update persistence state to track counts of messages instead of times banner
has been displayed by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13428
- update docs for http proxy by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13538
- move stdio by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13528
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
https://github.com/google-gemini/gemini-cli/pull/13488
- fix(ui): Correct mouse click cursor positioning for wide characters by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
https://github.com/google-gemini/gemini-cli/pull/13544
- Optimize and improve test coverage for cli/src/config by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13485
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13493
- docs: fix typos in source code and documentation by @fancive in
https://github.com/google-gemini/gemini-cli/pull/13577
- Improved code coverage for cli/src/zed-integration by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13570
- feat(ui): build interactive session browser component by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13351
- Fix multiple bugs with auth flow including using the implemented but unused
restart support. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13565
- feat(core): add modelAvailabilityService for managing and tracking model
health by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13426
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
https://github.com/google-gemini/gemini-cli/pull/13595
- feat: custom loading phrase when interactive shell requires input by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
- docs: Update uninstall command to reflect multiple extension support by
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13596
- Remove useModelRouter experimental flag by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13593
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13535
- Fix exp id logging by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13430
- Moved client id logging into createBasicLogEvent by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13607
- Restore bracketed paste mode after external editor exit by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13606
- feat(core): Add support for custom aliases for model configs. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13546
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13591
- Turn off alternate buffer mode by default. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13623
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13600
- Improve test coverage for cli/src/ui/components by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13598
- Update ink version to 6.4.6 by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13631
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
https://github.com/google-gemini/gemini-cli/pull/13548
- feat :rephrasing the extension logging messages to run the explore command
when there are no extensions installed by @JayadityaGit in
https://github.com/google-gemini/gemini-cli/pull/13740
- Improve code coverage for cli package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13724
- Add session subtask in /stats command by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13750
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12863
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9082
- fix: (some minor improvements to configs and getPackageJson return behaviour)
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
https://github.com/google-gemini/gemini-cli/pull/12510
- feat(hooks): Hook Event Handling by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9097
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9105
- feat(core): Land bool for alternate system prompt. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13764
- bug(core): Add default chat compression config. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13766
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
- feat(hooks): Hook System Orchestration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9102
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
in https://github.com/google-gemini/gemini-cli/pull/13777
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
- chore: remove console.error by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13779
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
https://github.com/google-gemini/gemini-cli/pull/12763
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
in https://github.com/google-gemini/gemini-cli/pull/13800
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
- feat(sessions): add /resume slash command to open the session browser by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
- docs(sessions): add documentation for chat recording and session management by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
in https://github.com/google-gemini/gemini-cli/pull/13698
- fallback to flash for TerminalQuota errors by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13791
- Update Code Wiki README badge by @PatoBeltran in
https://github.com/google-gemini/gemini-cli/pull/13768
- Add Databricks auth support and custom header option to gemini cli by
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
https://github.com/google-gemini/gemini-cli/pull/13827
- fix(patch): cherry-pick 576fda1 to release/v0.19.0-preview.0-pr-14099
[CONFLICTS] by @gemini-cli-robot in
https://github.com/google-gemini/gemini-cli/pull/14402
- refactor(stdio): always patch stdout and use createWorkingStdio for clean
output by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14159
- chore(release): bump version to 0.21.0-nightly.20251202.2d935b379 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14409
- implement fuzzy search inside settings by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13864
- feat: enable message bus integration by default by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14329
- docs: Recommend using --debug intead of --verbose for CLI debugging by @bbiggs
in https://github.com/google-gemini/gemini-cli/pull/14334
- feat: consolidate remote MCP servers to use `url` in config by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/13762
- Restrict integration tests tools by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14403
- track github repository names in telemetry events by @IamRiddhi in
https://github.com/google-gemini/gemini-cli/pull/13670
- Allow telemetry exporters to GCP to utilize user's login credentials, if
requested by @mboshernitsan in
https://github.com/google-gemini/gemini-cli/pull/13778
- refactor(editor): use const assertion for editor types with single source of
truth by @amsminn in https://github.com/google-gemini/gemini-cli/pull/8604
- fix(security): Fix npm audit vulnerabilities in glob and body-parser by
@afarber in https://github.com/google-gemini/gemini-cli/pull/14090
- Add new enterprise instructions by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/8641
- feat(hooks): Hook Session Lifecycle & Compression Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/14151
- Avoid triggering refreshStatic unless there really is a banner to display. by
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14328
- feat(hooks): Hooks Commands Panel, Enable/Disable, and Migrate by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/14225
- fix: Bundle default policies for npx distribution by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14457
- feat(hooks): Hook System Documentation by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/14307
- Fix tests by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14458
- feat: add scheduled workflow to close stale issues by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/14404
- feat: Support Extension Hooks with Security Warning by @abhipatel12 in
https://github.com/google-gemini/gemini-cli/pull/14460
- feat: Add enableAgents experimental flag by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/14371
- docs: fix typo 'socus' to 'focus' in todos.md by @Viktor286 in
https://github.com/google-gemini/gemini-cli/pull/14374
- Markdown export: move the emoji to the end of the line by @mhansen in
https://github.com/google-gemini/gemini-cli/pull/12278
- fix(acp): prevent unnecessary credential cache clearing on re-authent… by
@h-michael in https://github.com/google-gemini/gemini-cli/pull/9410
- fix(cli): Fix word navigation for CJK characters by @SandyTao520 in
https://github.com/google-gemini/gemini-cli/pull/14475
- Remove example extension by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/14376
- Add commands for listing and updating per-extension settings by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/12664
- chore(tests): remove obsolete test for hierarchical memory by @pareshjoshij in
https://github.com/google-gemini/gemini-cli/pull/13122
- feat(cli): support /copy in remote sessions using OSC52 by @ismellpillows in
https://github.com/google-gemini/gemini-cli/pull/13471
- Update setting search UX by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/14451
- Fix(cli): Improve Homebrew update instruction to specify gemini-cli by
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/14502
- do not toggle the setting item when entering space by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/14489
- fix: improve retry logic for fetch errors and network codes by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/14439
- remove unused isSearching field by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/14509
- feat(mcp): add `--type` alias for `--transport` flag in gemini mcp add by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14503
- feat(cli): Move key restore logic to core by @cocosheng-g in
https://github.com/google-gemini/gemini-cli/pull/13013
- feat: add auto-execute on Enter behavior to argumentless MCP prompts by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14510
- fix(shell): cursor visibility when using interactive mode by @aswinashok44 in
https://github.com/google-gemini/gemini-cli/pull/14095
- Adding session id as part of json o/p by @MJjainam in
https://github.com/google-gemini/gemini-cli/pull/14504
- fix(extensions): resolve GitHub API 415 error for source tarballs by
@jpoehnelt in https://github.com/google-gemini/gemini-cli/pull/13319
- fix(client): Correctly latch hasFailedCompressionAttempt flag by @pareshjoshij
in https://github.com/google-gemini/gemini-cli/pull/13002
- Disable flaky extension reloading test on linux by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/14528
- Add support for MCP dynamic tool update by `notifications/tools/list_changed`
by @Adib234 in https://github.com/google-gemini/gemini-cli/pull/14375
- Fix privacy screen for legacy tier users by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14522
- feat: Exclude maintainer labeled issues from stale issue closer by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/14532
- Grant chained workflows proper permission. by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14534
- Make trigger_e2e manually fireable. by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14547
- Write e2e status to local repo not forked repo by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14549
- Fixes [API Error: Cannot read properties of undefined (reading 'error')] by
@silviojr in https://github.com/google-gemini/gemini-cli/pull/14553
- Trigger chained e2e tests on all pull requests by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14551
- Fix bug in the shellExecutionService resulting in both truncation and 3X bloat
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14545
- Fix issue where we were passing the model content reflecting terminal line
wrapping. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/14566
- chore/release: bump version to 0.21.0-nightly.20251204.3da4fd5f7 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14476
- feat(sessions): use 1-line generated session summary to describe sessions by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14467
- Use Robot PAT for chained e2e merge queue skipper by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14585
- fix(core): improve API response error handling and retry logic by @mattKorwel
in https://github.com/google-gemini/gemini-cli/pull/14563
- Docs: Model routing clarification by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14373
- expose previewFeatures flag in a2a by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/14550
- Fix emoji width in debug console. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/14593
- Fully detach autoupgrade process by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14595
- Docs: Update Gemini 3 on Gemini CLI documentation by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14601
- Disallow floating promises. by @gundermanc in
https://github.com/google-gemini/gemini-cli/pull/14605
- chore/release: bump version to 0.21.0-nightly.20251207.025e450ac by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14662
- feat(modelAvailabilityService): integrate model availability service into
backend logic by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/14470
- Add prompt_id propagation in a2a-server task by @koxkox111 in
https://github.com/google-gemini/gemini-cli/pull/14581
- Fix: Prevent freezing in non-interactive Gemini CLI when debug mode is enabled
by @parthasaradhie in https://github.com/google-gemini/gemini-cli/pull/14580
- fix(audio): improve reading of audio files by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/14658
- Update automated triage workflow to stop assigning priority labels by
@skeshive in https://github.com/google-gemini/gemini-cli/pull/14717
- set failed status when chained e2e fails by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14725
- feat(github action) Triage and Label Pull Requests by Size and Comple… by
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/5571
- refactor(telemetry): Improve previous PR that allows telemetry to use the CLI
auth and add testing by @mboshernitsan in
https://github.com/google-gemini/gemini-cli/pull/14589
- Always set status in chained_e2e workflow by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14730
- feat: Add OTEL log event `gemini_cli.startup_stats` for startup stats. by
@kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/14734
- feat: auto-execute on slash command completion functions by @jackwotherspoon
in https://github.com/google-gemini/gemini-cli/pull/14584
- Docs: Proper release notes by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14405
- Add support for user-scoped extension settings by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13748
- refactor(core): Improve environment variable handling in shell execution by
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14742
- Remove old E2E Workflows by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14749
- fix: handle missing local extension config and skip hooks when disabled by
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14744
- chore/release: bump version to 0.21.0-nightly.20251209.ec9a8c7a7 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14751
- feat: Add support for MCP Resources by @MrLesk in
https://github.com/google-gemini/gemini-cli/pull/13178
- Always set pending status in E2E tests by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14756
- fix(lint): upgrade pip and use public pypi for yamllint by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14746
- fix: use Gemini API supported image formats for clipboard by @jackwotherspoon
in https://github.com/google-gemini/gemini-cli/pull/14762
- feat(a2a): Introduce restore command for a2a server by @cocosheng-g in
https://github.com/google-gemini/gemini-cli/pull/13015
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
https://github.com/google-gemini/gemini-cli/pull/14747
- (fix) Automated pr labeller by @DaanVersavel in
https://github.com/google-gemini/gemini-cli/pull/14788
- Update CODEOWNERS by @kklashtorny1 in
https://github.com/google-gemini/gemini-cli/pull/14830
- Docs: Fix errors preventing site rebuild. by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14842
- chore(deps): bump express from 5.1.0 to 5.2.0 by @dependabot[bot] in
https://github.com/google-gemini/gemini-cli/pull/14325
- fix(patch): cherry-pick 3f5f030 to release/v0.21.0-preview.0-pr-14843 to patch
version v0.21.0-preview.0 and create version 0.21.0-preview.1 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14851
- fix(patch): cherry-pick ee6556c to release/v0.21.0-preview.1-pr-14691 to patch
version v0.21.0-preview.1 and create version 0.21.0-preview.2 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14908
- fix(patch): cherry-pick 54de675 to release/v0.21.0-preview.2-pr-14961 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14968
- fix(patch): cherry-pick 12cbe32 to release/v0.21.0-preview.3-pr-15000 to patch
version v0.21.0-preview.3 and create version 0.21.0-preview.4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15003
- fix(patch): cherry-pick edbe548 to release/v0.21.0-preview.4-pr-15007 to patch
version v0.21.0-preview.4 and create version 0.21.0-preview.5 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15015
- fix(patch): cherry-pick 2995af6 to release/v0.21.0-preview.5-pr-15131 to patch
version v0.21.0-preview.5 and create version 0.21.0-preview.6 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15153
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
https://github.com/google-gemini/gemini-cli/compare/v0.20.2...v0.21.0
+115 -127
View File
@@ -1,6 +1,6 @@
# Preview release: Release v0.19.0-preview.0
# Preview release: Release v0.22.0-preview.0
Released: November 25, 2025
Released: December 16, 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,131 +11,119 @@ To install the preview release:
npm install -g @google/gemini-cli@preview
```
## What's changed
## What's Changed
- Use lenient MCP output schema validator by @cornmander in
https://github.com/google-gemini/gemini-cli/pull/13521
- Update persistence state to track counts of messages instead of times banner
has been displayed by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13428
- update docs for http proxy by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13538
- move stdio by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13528
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
https://github.com/google-gemini/gemini-cli/pull/13488
- fix(ui): Correct mouse click cursor positioning for wide characters by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
https://github.com/google-gemini/gemini-cli/pull/13544
- Optimize and improve test coverage for cli/src/config by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13485
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13493
- docs: fix typos in source code and documentation by @fancive in
https://github.com/google-gemini/gemini-cli/pull/13577
- Improved code coverage for cli/src/zed-integration by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13570
- feat(ui): build interactive session browser component by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13351
- Fix multiple bugs with auth flow including using the implemented but unused
restart support. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13565
- feat(core): add modelAvailabilityService for managing and tracking model
health by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13426
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
https://github.com/google-gemini/gemini-cli/pull/13595
- feat: custom loading phrase when interactive shell requires input by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
- docs: Update uninstall command to reflect multiple extension support by
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13596
- Remove useModelRouter experimental flag by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13593
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13535
- Fix exp id logging by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13430
- Moved client id logging into createBasicLogEvent by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13607
- Restore bracketed paste mode after external editor exit by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13606
- feat(core): Add support for custom aliases for model configs. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13546
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13591
- Turn off alternate buffer mode by default. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13623
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13600
- Improve test coverage for cli/src/ui/components by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13598
- Update ink version to 6.4.6 by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13631
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
https://github.com/google-gemini/gemini-cli/pull/13548
- feat :rephrasing the extension logging messages to run the explore command
when there are no extensions installed by @JayadityaGit in
https://github.com/google-gemini/gemini-cli/pull/13740
- Improve code coverage for cli package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13724
- Add session subtask in /stats command by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13750
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12863
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9082
- fix: (some minor improvements to configs and getPackageJson return behaviour)
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
https://github.com/google-gemini/gemini-cli/pull/12510
- feat(hooks): Hook Event Handling by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9097
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9105
- feat(core): Land bool for alternate system prompt. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13764
- bug(core): Add default chat compression config. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13766
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
- feat(hooks): Hook System Orchestration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9102
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
in https://github.com/google-gemini/gemini-cli/pull/13777
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
- chore: remove console.error by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13779
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
https://github.com/google-gemini/gemini-cli/pull/12763
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
in https://github.com/google-gemini/gemini-cli/pull/13800
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
- feat(sessions): add /resume slash command to open the session browser by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
- docs(sessions): add documentation for chat recording and session management by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
in https://github.com/google-gemini/gemini-cli/pull/13698
- fallback to flash for TerminalQuota errors by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13791
- Update Code Wiki README badge by @PatoBeltran in
https://github.com/google-gemini/gemini-cli/pull/13768
- Add Databricks auth support and custom header option to gemini cli by
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
https://github.com/google-gemini/gemini-cli/pull/13827
- 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
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.18.0-preview.4...v0.19.0-preview.0
https://github.com/google-gemini/gemini-cli/compare/v0.21.0-preview.6...v0.22.0-preview.0
+475 -9
View File
@@ -12,13 +12,479 @@ on GitHub.
## Current Releases
| Release channel | Notes |
| :----------------------------------------- | :---------------------------------------------- |
| Nightly | Nightly release with the most recent changes. |
| [Preview](#release-v0190-preview0-preview) | Experimental features ready for early feedback. |
| [Latest](#release-v0190---v0194-latest) | Stable, recommended for general use. |
| Release channel | Notes |
| :------------------------------------------ | :---------------------------------------------- |
| Nightly | Nightly release with the most recent changes. |
| [Preview](#release-v0220-preview-0-preview) | Experimental features ready for early feedback. |
| [Latest](#release-v0210---v0211-latest) | Stable, recommended for general use. |
## Release v0.19.0 - v0.19.4 (Latest)
## Release v0.21.0 - v0.21.1 (Latest)
### Highlights
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** If you are a paid user, you can now
enable Gemini 3 Pro and Gemini 3 Flash. Go to `/settings` and set **Preview
Features** to `true` to enable Gemini 3. For more information:
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
### What's Changed
- refactor(stdio): always patch stdout and use createWorkingStdio for clean
output by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14159
- chore(release): bump version to 0.21.0-nightly.20251202.2d935b379 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14409
- implement fuzzy search inside settings by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13864
- feat: enable message bus integration by default by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14329
- docs: Recommend using --debug intead of --verbose for CLI debugging by @bbiggs
in https://github.com/google-gemini/gemini-cli/pull/14334
- feat: consolidate remote MCP servers to use `url` in config by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/13762
- Restrict integration tests tools by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14403
- track github repository names in telemetry events by @IamRiddhi in
https://github.com/google-gemini/gemini-cli/pull/13670
- Allow telemetry exporters to GCP to utilize user's login credentials, if
requested by @mboshernitsan in
https://github.com/google-gemini/gemini-cli/pull/13778
- refactor(editor): use const assertion for editor types with single source of
truth by @amsminn in https://github.com/google-gemini/gemini-cli/pull/8604
- fix(security): Fix npm audit vulnerabilities in glob and body-parser by
@afarber in https://github.com/google-gemini/gemini-cli/pull/14090
- Add new enterprise instructions by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/8641
- feat(hooks): Hook Session Lifecycle & Compression Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/14151
- Avoid triggering refreshStatic unless there really is a banner to display. by
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14328
- feat(hooks): Hooks Commands Panel, Enable/Disable, and Migrate by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/14225
- fix: Bundle default policies for npx distribution by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14457
- feat(hooks): Hook System Documentation by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/14307
- Fix tests by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14458
- feat: add scheduled workflow to close stale issues by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/14404
- feat: Support Extension Hooks with Security Warning by @abhipatel12 in
https://github.com/google-gemini/gemini-cli/pull/14460
- feat: Add enableAgents experimental flag by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/14371
- docs: fix typo 'socus' to 'focus' in todos.md by @Viktor286 in
https://github.com/google-gemini/gemini-cli/pull/14374
- Markdown export: move the emoji to the end of the line by @mhansen in
https://github.com/google-gemini/gemini-cli/pull/12278
- fix(acp): prevent unnecessary credential cache clearing on re-authent… by
@h-michael in https://github.com/google-gemini/gemini-cli/pull/9410
- fix(cli): Fix word navigation for CJK characters by @SandyTao520 in
https://github.com/google-gemini/gemini-cli/pull/14475
- Remove example extension by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/14376
- Add commands for listing and updating per-extension settings by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/12664
- chore(tests): remove obsolete test for hierarchical memory by @pareshjoshij in
https://github.com/google-gemini/gemini-cli/pull/13122
- feat(cli): support /copy in remote sessions using OSC52 by @ismellpillows in
https://github.com/google-gemini/gemini-cli/pull/13471
- Update setting search UX by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/14451
- Fix(cli): Improve Homebrew update instruction to specify gemini-cli by
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/14502
- do not toggle the setting item when entering space by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/14489
- fix: improve retry logic for fetch errors and network codes by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/14439
- remove unused isSearching field by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/14509
- feat(mcp): add `--type` alias for `--transport` flag in gemini mcp add by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14503
- feat(cli): Move key restore logic to core by @cocosheng-g in
https://github.com/google-gemini/gemini-cli/pull/13013
- feat: add auto-execute on Enter behavior to argumentless MCP prompts by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14510
- fix(shell): cursor visibility when using interactive mode by @aswinashok44 in
https://github.com/google-gemini/gemini-cli/pull/14095
- Adding session id as part of json o/p by @MJjainam in
https://github.com/google-gemini/gemini-cli/pull/14504
- fix(extensions): resolve GitHub API 415 error for source tarballs by
@jpoehnelt in https://github.com/google-gemini/gemini-cli/pull/13319
- fix(client): Correctly latch hasFailedCompressionAttempt flag by @pareshjoshij
in https://github.com/google-gemini/gemini-cli/pull/13002
- Disable flaky extension reloading test on linux by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/14528
- Add support for MCP dynamic tool update by `notifications/tools/list_changed`
by @Adib234 in https://github.com/google-gemini/gemini-cli/pull/14375
- Fix privacy screen for legacy tier users by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14522
- feat: Exclude maintainer labeled issues from stale issue closer by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/14532
- Grant chained workflows proper permission. by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14534
- Make trigger_e2e manually fireable. by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14547
- Write e2e status to local repo not forked repo by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14549
- Fixes [API Error: Cannot read properties of undefined (reading 'error')] by
@silviojr in https://github.com/google-gemini/gemini-cli/pull/14553
- Trigger chained e2e tests on all pull requests by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14551
- Fix bug in the shellExecutionService resulting in both truncation and 3X bloat
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14545
- Fix issue where we were passing the model content reflecting terminal line
wrapping. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/14566
- chore/release: bump version to 0.21.0-nightly.20251204.3da4fd5f7 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14476
- feat(sessions): use 1-line generated session summary to describe sessions by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14467
- Use Robot PAT for chained e2e merge queue skipper by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14585
- fix(core): improve API response error handling and retry logic by @mattKorwel
in https://github.com/google-gemini/gemini-cli/pull/14563
- Docs: Model routing clarification by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14373
- expose previewFeatures flag in a2a by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/14550
- Fix emoji width in debug console. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/14593
- Fully detach autoupgrade process by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14595
- Docs: Update Gemini 3 on Gemini CLI documentation by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14601
- Disallow floating promises. by @gundermanc in
https://github.com/google-gemini/gemini-cli/pull/14605
- chore/release: bump version to 0.21.0-nightly.20251207.025e450ac by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14662
- feat(modelAvailabilityService): integrate model availability service into
backend logic by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/14470
- Add prompt_id propagation in a2a-server task by @koxkox111 in
https://github.com/google-gemini/gemini-cli/pull/14581
- Fix: Prevent freezing in non-interactive Gemini CLI when debug mode is enabled
by @parthasaradhie in https://github.com/google-gemini/gemini-cli/pull/14580
- fix(audio): improve reading of audio files by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/14658
- Update automated triage workflow to stop assigning priority labels by
@skeshive in https://github.com/google-gemini/gemini-cli/pull/14717
- set failed status when chained e2e fails by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14725
- feat(github action) Triage and Label Pull Requests by Size and Comple… by
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/5571
- refactor(telemetry): Improve previous PR that allows telemetry to use the CLI
auth and add testing by @mboshernitsan in
https://github.com/google-gemini/gemini-cli/pull/14589
- Always set status in chained_e2e workflow by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14730
- feat: Add OTEL log event `gemini_cli.startup_stats` for startup stats. by
@kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/14734
- feat: auto-execute on slash command completion functions by @jackwotherspoon
in https://github.com/google-gemini/gemini-cli/pull/14584
- Docs: Proper release notes by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14405
- Add support for user-scoped extension settings by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13748
- refactor(core): Improve environment variable handling in shell execution by
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14742
- Remove old E2E Workflows by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14749
- fix: handle missing local extension config and skip hooks when disabled by
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14744
- chore/release: bump version to 0.21.0-nightly.20251209.ec9a8c7a7 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14751
- feat: Add support for MCP Resources by @MrLesk in
https://github.com/google-gemini/gemini-cli/pull/13178
- Always set pending status in E2E tests by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14756
- fix(lint): upgrade pip and use public pypi for yamllint by @allenhutchison in
https://github.com/google-gemini/gemini-cli/pull/14746
- fix: use Gemini API supported image formats for clipboard by @jackwotherspoon
in https://github.com/google-gemini/gemini-cli/pull/14762
- feat(a2a): Introduce restore command for a2a server by @cocosheng-g in
https://github.com/google-gemini/gemini-cli/pull/13015
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
https://github.com/google-gemini/gemini-cli/pull/14747
- (fix) Automated pr labeller by @DaanVersavel in
https://github.com/google-gemini/gemini-cli/pull/14788
- Update CODEOWNERS by @kklashtorny1 in
https://github.com/google-gemini/gemini-cli/pull/14830
- Docs: Fix errors preventing site rebuild. by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/14842
- chore(deps): bump express from 5.1.0 to 5.2.0 by @dependabot[bot] in
https://github.com/google-gemini/gemini-cli/pull/14325
- fix(patch): cherry-pick 3f5f030 to release/v0.21.0-preview.0-pr-14843 to patch
version v0.21.0-preview.0 and create version 0.21.0-preview.1 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14851
- fix(patch): cherry-pick ee6556c to release/v0.21.0-preview.1-pr-14691 to patch
version v0.21.0-preview.1 and create version 0.21.0-preview.2 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14908
- fix(patch): cherry-pick 54de675 to release/v0.21.0-preview.2-pr-14961 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14968
- fix(patch): cherry-pick 12cbe32 to release/v0.21.0-preview.3-pr-15000 to patch
version v0.21.0-preview.3 and create version 0.21.0-preview.4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15003
- fix(patch): cherry-pick edbe548 to release/v0.21.0-preview.4-pr-15007 to patch
version v0.21.0-preview.4 and create version 0.21.0-preview.5 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15015
- fix(patch): cherry-pick 2995af6 to release/v0.21.0-preview.5-pr-15131 to patch
version v0.21.0-preview.5 and create version 0.21.0-preview.6 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15153
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.20.2...v0.21.0
## Release v0.22.0-preview-0 (Preview)
### What's Changed
- 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
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.21.0-preview.6...v0.22.0-preview.0
## Release v0.20.0 - v0.20.2
### What's Changed
- Update error codes when process exiting the gemini cli by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13728
- chore(release): bump version to 0.20.0-nightly.20251126.d2a6cff4d by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13835
- feat(core): Improve request token calculation accuracy by @SandyTao520 in
https://github.com/google-gemini/gemini-cli/pull/13824
- Changes in system instruction to adapt to gemini 3.0 to ensure that the CLI
explains its actions before calling tools by @silviojr in
https://github.com/google-gemini/gemini-cli/pull/13810
- feat(hooks): Hook Tool Execution Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9108
- Add support for MCP server instructions behind config option by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13432
- Update System Instructions for interactive vs non-interactive mode. by
@aishaneeshah in https://github.com/google-gemini/gemini-cli/pull/12315
- Add consent flag to Link command by @kevinjwang1 in
https://github.com/google-gemini/gemini-cli/pull/13832
- feat(mcp): Inject GoogleCredentialProvider headers in McpClient by
@sai-sunder-s in https://github.com/google-gemini/gemini-cli/pull/13783
- feat(core): implement towards policy-driven model fallback mechanism by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13781
- feat(core): Add configurable inactivity timeout for shell commands by @galz10
in https://github.com/google-gemini/gemini-cli/pull/13531
- fix(auth): improve API key authentication flow by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/13829
- feat(hooks): Hook LLM Request/Response Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9110
- feat(ui): Show waiting MCP servers in ConfigInitDisplay by @werdnum in
https://github.com/google-gemini/gemini-cli/pull/13721
- Add usage limit remaining in /stats by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13843
- feat(shell): Standardize pager to 'cat' for shell execution by model by
@galz10 in https://github.com/google-gemini/gemini-cli/pull/13878
- chore/release: bump version to 0.20.0-nightly.20251127.5bed97064 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13877
- Revert to default LICENSE (Revert #13449) by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13876
- update(telemetry): OTel API response event with finish reasons by @kiranani in
https://github.com/google-gemini/gemini-cli/pull/13849
- feat(hooks): Hooks Comprehensive Integration Testing by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9112
- chore: fix session browser test and skip hook system tests by @jackwotherspoon
in https://github.com/google-gemini/gemini-cli/pull/14099
- feat(telemetry): Add Semantic logging for to ApiRequestEvents by @kiranani in
https://github.com/google-gemini/gemini-cli/pull/13912
- test: Add verification for $schema property in settings schema by
@maryamariyan in https://github.com/google-gemini/gemini-cli/pull/13497
- Fixes `/clear` command to preserve input history for up-arrow navigation while
still clearing the context window and screen by @korade-krushna in
https://github.com/google-gemini/gemini-cli/pull/14182
- fix(core): handle EPIPE error in hook runner when writing to stdin by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/14231
- fix: Exclude web-fetch tool from executing in default non-interactive mode to
avoid CLI hang. by @MayV in
https://github.com/google-gemini/gemini-cli/pull/14244
- Always use MCP server instructions by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/14297
- feat: auto-execute simple slash commands on Enter by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/13985
- chore/release: bump version to 0.20.0-nightly.20251201.2fe609cb6 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14304
- feat: Add startup profiler to measure and record application initialization
phases. by @kevin-ramdass in
https://github.com/google-gemini/gemini-cli/pull/13638
- bug(core): Avoid stateful tool use in `executor`. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/14305
- feat(themes): add built-in holiday theme 🎁 by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/14301
- Updated ToC on docs intro; updated title casing to match Google style by
@pcoet in https://github.com/google-gemini/gemini-cli/pull/13717
- feat(a2a): Urgent fix - Process modelInfo agent message by @cocosheng-g in
https://github.com/google-gemini/gemini-cli/pull/14315
- feat(core): enhance availability routing with wrapped fallback and
single-model policies by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13874
- chore(logging): log the problematic event for #12122 by @briandealwis in
https://github.com/google-gemini/gemini-cli/pull/14092
- fix: remove invalid type key in bug_report.yml by @fancive in
https://github.com/google-gemini/gemini-cli/pull/13576
- update screenshot by @Transient-Onlooker in
https://github.com/google-gemini/gemini-cli/pull/13976
- docs: Fix grammar error in Release Cadence (Nightly section) by @JuanCS-Dev in
https://github.com/google-gemini/gemini-cli/pull/13866
- fix(async): prevent missed async errors from bypassing catch handlers by
@amsminn in https://github.com/google-gemini/gemini-cli/pull/13714
- fix(zed-integration): remove extra field from acp auth request by
@marcocondrache in https://github.com/google-gemini/gemini-cli/pull/13646
- feat(cli): Documentation for model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12967
- fix(ui): misaligned markdown table rendering by @dumbbellcode in
https://github.com/google-gemini/gemini-cli/pull/8336
- docs: Update 4 files by @g-samroberts in
https://github.com/google-gemini/gemini-cli/pull/13628
- fix: Conditionally add set -eEuo pipefail in setup-github command by @Smetalo
in https://github.com/google-gemini/gemini-cli/pull/8550
- fix(cli): fix issue updating a component while rendering a different component
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14319
- Increase flakey test timeout by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/14377
- Remove references to deleted kind/bug label by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14383
- Don't fail test if we can't cleanup by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/14389
- feat(core): Implement JIT context manager and setting by @SandyTao520 in
https://github.com/google-gemini/gemini-cli/pull/14324
- Use polling for extensions-reload integration test by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/14391
- Add docs directive to GEMINI.md by @g-samroberts in
https://github.com/google-gemini/gemini-cli/pull/14327
- Hide sessions that don't have user messages by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13994
- chore(ci): mark GitHub release as pre-release if not on "latest" npm channel
by @ljxfstorm in https://github.com/google-gemini/gemini-cli/pull/7386
- fix(patch): cherry-pick d284fa6 to release/v0.20.0-preview.0-pr-14545
[CONFLICTS] by @gemini-cli-robot in
https://github.com/google-gemini/gemini-cli/pull/14559
- fix(patch): cherry-pick 828afe1 to release/v0.20.0-preview.1-pr-14159 to patch
version v0.20.0-preview.1 and create version 0.20.0-preview.2 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14733
- fix(patch): cherry-pick 171103a to release/v0.20.0-preview.2-pr-14742 to patch
version v0.20.0-preview.2 and create version 0.20.0-preview.5 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14752
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.19.4...v0.20.0
## Release v0.19.0 - v0.19.4
## Highlights
@@ -26,10 +492,10 @@ on GitHub.
integration after enabling "Preview Features" in their CLIs `/settings`.
- **Interactive shell:**
- **Click-to-Focus:** Go to `/settings` and enable **Use Alternate Buffer**
WhenUse Alternate Buffer" setting is enabled users can click within the
When "Use Alternate Buffer" setting is enabled users can click within the
embedded shell output to focus it for input.
- **Loading phrase:** Clearly indicates when the interactive shell is awaiting
user input. ([vid](https://imgur.com/a/kjK8bUK),
user input. ([vid](https://imgur.com/a/kjK8bUK)
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
[@jackwotherspoon](https://github.com/jackwotherspoon))
@@ -165,7 +631,7 @@ on GitHub.
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
## Release v0.19.0-preview.0 (Preview)
## Release v0.19.0-preview.0
### What's Changed
+2
View File
@@ -38,6 +38,8 @@ overview of Gemini CLI, see the [main documentation page](../index.md).
files and directories from being accessed by tools.
- **[Context files (GEMINI.md)](./gemini-md.md):** Provide persistent,
hierarchical context to the model.
- **[System prompt override](./system-prompt.md):** Replace the builtin system
instructions using `GEMINI_SYSTEM_MD`.
## Non-interactive mode
+4 -4
View File
@@ -84,10 +84,10 @@ available combinations.
#### External Tools
| Action | Keys |
| ---------------------------------------------- | ---------- |
| Open the current prompt in an external editor. | `Ctrl + X` |
| Paste from the clipboard. | `Ctrl + V` |
| Action | Keys |
| ---------------------------------------------- | ------------------------- |
| Open the current prompt in an external editor. | `Ctrl + X` |
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V` |
#### App Controls
+11 -15
View File
@@ -6,24 +6,20 @@ and provides resilience when the primary model is unavailable.
## How it works
Model routing is not based on prompt complexity, but is a fallback mechanism.
Here's how it works:
Model routing is managed by the `ModelAvailabilityService`, which monitors model
health and automatically routes requests to available models based on defined
policies.
1. **Model failure:** If the currently selected model fails to respond (for
example, due to a server error or other issue), the CLI will initiate the
fallback process.
1. **Model failure:** If the currently selected model fails (e.g., due to quota
or server errors), the CLI will iniate the fallback process.
2. **User consent:** The CLI will prompt you to ask if you want to switch to
the fallback model. This is handled by the `fallbackModelHandler`.
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).
3. **Fallback activation:** If you consent, the CLI will activate the fallback
mode by calling `config.setFallbackMode(true)`.
4. **Model switch:** On the next request, the CLI will use the
`DEFAULT_GEMINI_FLASH_MODEL` as the fallback model. This is handled by the
`resolveModel` function in
`packages/cli/src/zed-integration/zedIntegration.ts` which checks if
`isInFallbackMode()` is true.
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.
### Model selection precedence
+30 -30
View File
@@ -1,12 +1,13 @@
# Gemini CLI model selection (`/model` command)
Select your Gemini CLI model. The `/model` command opens a dialog where you can
configure the model used by Gemini CLI, giving you more control over your
results.
Select your Gemini CLI model. The `/model` command lets you configure the model
used by Gemini CLI, giving you more control over your results. Use **Pro**
models for complex tasks and reasoning, **Flash** models for high speed results,
or the (recommended) **Auto** setting to choose the best model for your tasks.
**Note:** The `/model` command (and the `--model` flag) does not override the
model used by sub-agents. Consequently, even when using the `/model` flag you
may see other models used in your model usage reports.
> **Note:** The `/model` command (and the `--model` flag) does not override the
> model used by sub-agents. Consequently, even when using the `/model` flag you
> may see other models used in your model usage reports.
## How to use the `/model` command
@@ -16,26 +17,25 @@ Use the following command in Gemini CLI:
/model
```
Running this command will open a dialog with your model options:
Running this command will open a dialog with your options:
| Option | Description | Models |
| ------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Auto (recommended) | Let the system choose the best model for your task. | gemini-3-pro-preview (if enabled), gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite |
| Pro | For complex tasks that require deep reasoning and creativity. | gemini-3-pro-preview (if enabled), gemini-2.5-pro |
| Flash | For tasks that need a balance of speed and reasoning. | gemini-2.5-flash |
| Flash-Lite | For simple tasks that need to be done quickly. | gemini-2.5-flash-lite |
| Option | Description | Models |
| ----------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Auto (Gemini 3) | Let the system choose the best Gemini 3 model for your task. | gemini-3-pro-preview (if enabled), gemini-3-flash-preview (if enabled) |
| Auto (Gemini 2.5) | Let the system choose the best Gemini 2.5 model for your task. | gemini-2.5-pro, gemini-2.5-flash |
| Manual | Select a specific model. | Any available model. |
### Gemini 3 Pro and preview features
We recommend selecting one of the above **Auto** options. However, you can
select **Manual** to select a specific model from those available.
Note: Gemini 3 is not currently available on all account types. To learn more
about Gemini 3 access, refer to
[Gemini 3 Pro on Gemini CLI](../get-started/gemini-3.md).
### Gemini 3 and preview features
To enable Gemini 3 Pro (if available), enable
[**Preview features** by using the `settings` command](../cli/settings.md). Once
enabled, Gemini CLI will attempt to use Gemini 3 Pro when you select **Auto** or
**Pro**. Both **Auto** and **Pro** will try to use Gemini 3 Pro before falling
back to Gemini 2.5 Pro.
> **Note:** Gemini 3 is not currently available on all account types. To learn
> more about Gemini 3 access, refer to
> [Gemini 3 on Gemini CLI](../get-started/gemini-3.md).
To enable Gemini 3 Pro and Gemini 3 Flash (if available), enable
[**Preview Features** by using the `settings` command](../cli/settings.md).
You can also use the `--model` flag to specify a particular Gemini model on
startup. For more details, refer to the
@@ -46,16 +46,16 @@ Gemini CLI.
## Best practices for model selection
- **Default to Auto (recommended).** For most users, the _Auto (recommended)_
model provides a balance between speed and performance, automatically
selecting the correct model based on the complexity of the task. Example:
Developing a web application could include a mix of complex tasks (building
architecture and scaffolding the project) and simple tasks (generating CSS).
- **Default to Auto.** For most users, the _Auto_ option model provides a
balance between speed and performance, automatically selecting the correct
model based on the complexity of the task. Example: Developing a web
application could include a mix of complex tasks (building architecture and
scaffolding the project) and simple tasks (generating CSS).
- **Switch to Pro if you aren't getting the results you want.** If you think you
need your model to be a little "smarter," use Pro. Pro will provide you with
the highest levels of reasoning and creativity. Example: A complex or
multi-stage debugging task.
need your model to be a little "smarter," you can manually select Pro. Pro
will provide you with the highest levels of reasoning and creativity. Example:
A complex or multi-stage debugging task.
- **Switch to Flash or Flash-Lite if you need faster results.** If you need a
simple response quickly, Flash or Flash-Lite is the best option. Example:
+8 -6
View File
@@ -94,15 +94,16 @@ 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. | `10000` |
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `100` |
| Enable Message Bus Integration | `tools.enableMessageBusIntegration` | Enable policy-based tool confirmation via message bus integration. | `true` |
### Security
| UI Label | Setting | Description | Default |
| -------------------------- | ------------------------------ | -------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
| UI Label | Setting | Description | Default |
| ----------------------------- | ----------------------------------------------- | --------------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
| Allowed Environment Variables | `security.environmentVariableRedaction.allowed` | Environment variables to always allow (bypass redaction). | `[]` |
| Blocked Environment Variables | `security.environmentVariableRedaction.blocked` | Environment variables to always redact. | `[]` |
### Experimental
@@ -110,3 +111,4 @@ they appear in the UI.
| ----------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | ------- |
| 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` |
| Agent Skills | `experimental.skills` | Enable Agent Skills (experimental). | `false` |
+156
View File
@@ -0,0 +1,156 @@
# Agent Skills
_Note: This is an experimental feature enabled via `experimental.skills`. You
can also search for "Skills" within the `/settings` interactive UI to toggle
this and manage other skill-related settings._
Agent Skills allow you to extend Gemini CLI with specialized expertise,
procedural workflows, and task-specific resources. Based on the
[Agent Skills](https://agentskills.io) open standard, a "skill" is a
self-contained directory that packages instructions and assets into a
discoverable capability.
## Overview
Unlike general context files ([`GEMINI.md`](./gemini-md.md)), which provide
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.
Gemini autonomously decides when to employ a skill based on your request and the
skill's description. When a relevant skill is identified, the model "pulls in"
the full instructions and resources required to complete the task using the
`activate_skill` tool.
## Key Benefits
- **Shared Expertise:** Package complex workflows (like a specific team's PR
review process) into a folder that anyone can use.
- **Repeatable Workflows:** Ensure complex multi-step tasks are performed
consistently by providing a procedural framework.
- **Resource Bundling:** Include scripts, templates, or example data alongside
instructions so the agent has everything it needs.
- **Progressive Disclosure:** Only skill metadata (name and description) is
loaded initially. Detailed instructions and resources are only disclosed when
the model explicitly activates the skill, saving context tokens.
## Skill Discovery Tiers
Gemini CLI discovers skills from three primary locations:
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 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: **Project > User > Extension**.
## Managing Skills
### In an Interactive Session
Use the `/skills` slash command to view and manage available expertise:
- `/skills list` (default): Shows all discovered skills and their status.
- `/skills disable <name>`: Prevents a specific skill from being used.
- `/skills enable <name>`: Re-enables a disabled skill.
- `/skills reload`: Refreshes the list of discovered skills from all tiers.
_Note: `/skills disable` and `/skills enable` default to the `user` scope. Use
`--scope project` to manage project-specific settings._
### From the Terminal
The `gemini skills` command provides management utilities:
```bash
# List all discovered skills
gemini skills list
# Enable/disable skills. Can use --scope to specify project or user
gemini skills enable my-expertise
gemini skills disable my-expertise
```
## Creating a Skill
A skill is a directory containing a `SKILL.md` file at its root. This file uses
YAML frontmatter for metadata and Markdown for instructions.
### Basic Structure
```markdown
---
name: <unique-name>
description: <what the skill does and when Gemini should use it>
---
<your instructions for how the agent should behave / use the skill>
```
- **`name`**: A unique identifier (lowercase, alphanumeric, and dashes).
- **`description`**: The most critical field. Gemini uses this to decide when
the skill is relevant. Be specific about the expertise provided.
- **Body**: Everything below the second `---` is injected as expert procedural
guidance for the model.
### Example: Team Code Reviewer
```markdown
---
name: code-reviewer
description:
Expertise in reviewing code for style, security, and performance. Use when the
user asks for "feedback," a "review," or to "check" their changes.
---
# Code Reviewer
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 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
the test coverage adequately validates the changes.
Provide your feedback as a concise bulleted list of "Strengths" and
"Opportunities."
```
### Resource Conventions
While you can structure your skill directory however you like, the Agent Skills
standard encourages these conventions:
- **`scripts/`**: Executable scripts (bash, python, node) the agent can run.
- **`references/`**: Static documentation, schemas, or example data for the
agent to consult.
- **`assets/`**: Code templates, boilerplate, or binary resources.
When a skill is activated, Gemini CLI provides the model with a tree view of the
entire skill directory, allowing it to discover and utilize these assets.
## How it Works (Security & Privacy)
1. **Discovery**: At the start of a session, Gemini CLI scans the discovery
tiers and injects the name and description of all enabled skills into the
system prompt.
2. **Activation**: When Gemini identifies a task matching a skill's
description, it calls the `activate_skill` tool.
3. **Consent**: You will see a confirmation prompt in the UI detailing the
skill's name, purpose, and the directory path it will gain access to.
4. **Injection**: Upon your approval:
- The `SKILL.md` body and folder structure is added to the conversation
history.
- The skill's directory is added to the agent's allowed file paths, granting
it permission to read any bundled assets.
5. **Execution**: The model proceeds with the specialized expertise active. It
is instructed to prioritize the skill's procedural guidance within reason.
+93
View File
@@ -0,0 +1,93 @@
# System Prompt Override (GEMINI_SYSTEM_MD)
The core system instructions that guide Gemini CLI can be completely replaced
with your own Markdown file. This feature is controlled via the
`GEMINI_SYSTEM_MD` environment variable.
## Overview
The `GEMINI_SYSTEM_MD` variable instructs the CLI to use an external Markdown
file for its system prompt, completely overriding the built-in default. This is
a full replacement, not a merge. If you use a custom file, none of the original
core instructions will apply unless you include them yourself.
This feature is intended for advanced users who need to enforce strict,
project-specific behavior or create a customized persona.
> Tip: You can export the current default system prompt to a file first, review
> it, and then selectively modify or replace it (see
> [“Export the default prompt”](#export-the-default-prompt-recommended)).
## How to enable
You can set the environment variable temporarily in your shell, or persist it
via a `.gemini/.env` file. See
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
- Use the project default path (`.gemini/system.md`):
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
- The CLI reads `./.gemini/system.md` (relative to your current project
directory).
- Use a custom file path:
- `GEMINI_SYSTEM_MD=/absolute/path/to/my-system.md`
- Relative paths are supported and resolved from the current working
directory.
- Tilde expansion is supported (e.g., `~/my-system.md`).
- Disable the override (use builtin prompt):
- `GEMINI_SYSTEM_MD=false` or `GEMINI_SYSTEM_MD=0` or unset the variable.
If the override is enabled but the target file does not exist, the CLI will
error with: `missing system prompt file '<path>'`.
## Quick examples
- Oneoff session using a project file:
- `GEMINI_SYSTEM_MD=1 gemini`
- Persist for a project using `.gemini/.env`:
- Create `.gemini/system.md`, then add to `.gemini/.env`:
- `GEMINI_SYSTEM_MD=1`
- Use a custom file under your home directory:
- `GEMINI_SYSTEM_MD=~/prompts/SYSTEM.md gemini`
## UI indicator
When `GEMINI_SYSTEM_MD` is active, the CLI shows a `|⌐■_■|` indicator in the UI
to signal custom systemprompt mode.
## Export the default prompt (recommended)
Before overriding, export the current default prompt so you can review required
safety and workflow rules.
- Write the builtin prompt to the project default path:
- `GEMINI_WRITE_SYSTEM_MD=1 gemini`
- Or write to a custom path:
- `GEMINI_WRITE_SYSTEM_MD=~/prompts/DEFAULT_SYSTEM.md gemini`
This creates the file and writes the current builtin system prompt to it.
## Best practices: SYSTEM.md vs GEMINI.md
- SYSTEM.md (firmware):
- Nonnegotiable operational rules: safety, tooluse protocols, approvals, and
mechanics that keep the CLI reliable.
- Stable across tasks and projects (or per project when needed).
- GEMINI.md (strategy):
- Persona, goals, methodologies, and project/domain context.
- Evolves per task; relies on SYSTEM.md for safe execution.
Keep SYSTEM.md minimal but complete for safety and tool operation. Keep
GEMINI.md focused on highlevel guidance and project specifics.
## Troubleshooting
- Error: `missing system prompt file '…'`
- Ensure the referenced path exists and is readable.
- For `GEMINI_SYSTEM_MD=1|true`, create `./.gemini/system.md` in your project.
- Override not taking effect
- Confirm the variable is loaded (use `.gemini/.env` or export in your shell).
- Paths are resolved from the current working directory; try an absolute path.
- Restore defaults
- Unset `GEMINI_SYSTEM_MD` or set it to `0`/`false`.
+3 -3
View File
@@ -297,7 +297,7 @@ Captures startup configuration and user prompt submissions.
#### Tools
Captures tool executions, output truncation, and Smart Edit behavior.
Captures tool executions, output truncation, and Edit behavior.
- `gemini_cli.tool_call`: Emitted for each tool (function) call.
- **Attributes**:
@@ -325,11 +325,11 @@ Captures tool executions, output truncation, and Smart Edit behavior.
- `lines` (int)
- `prompt_id` (string)
- `gemini_cli.smart_edit_strategy`: Smart Edit strategy chosen.
- `gemini_cli.edit_strategy`: Edit strategy chosen.
- **Attributes**:
- `strategy` (string)
- `gemini_cli.smart_edit_correction`: Smart Edit correction result.
- `gemini_cli.edit_correction`: Edit correction result.
- **Attributes**:
- `correction` ("success" | "failure")
+4
View File
@@ -2,6 +2,10 @@
This page contains tutorials for interacting with Gemini CLI.
## Agent Skills
- [Getting Started with Agent Skills](./tutorials/skills-getting-started.md)
## Setting up a Model Context Protocol (MCP) server
> [!CAUTION] Before using a third-party MCP server, ensure you trust its source
@@ -0,0 +1,124 @@
# Getting Started with Agent Skills
Agent Skills allow you to extend Gemini CLI with specialized expertise. This
tutorial will guide you through creating your first skill, enabling it, and
using it in a session.
## 1. Enable Agent Skills
Agent Skills are currently an experimental feature and must be enabled in your
settings.
### Via the interactive UI
1. Start a Gemini CLI session by running `gemini`.
2. Type `/settings` to open the interactive settings dialog.
3. Search for "Skills".
4. Toggle **Agent Skills** to `true`.
5. Press `Esc` to save and exit. You may need to restart the CLI for the
changes to take effect.
### Via `settings.json`
Alternatively, you can manually edit your global settings file at
`~/.gemini/settings.json` (create it if it doesn't exist):
```json
{
"experimental": {
"skills": true
}
}
```
## 2. Create Your First Skill
A skill is a directory containing a `SKILL.md` file. Let's create an **API
Auditor** skill that helps you verify if local or remote endpoints are
responding correctly.
1. **Create the skill directory structure:**
```bash
mkdir -p .gemini/skills/api-auditor/scripts
```
2. **Create the `SKILL.md` file:** Create a file at
`.gemini/skills/api-auditor/SKILL.md` with the following content:
```markdown
---
name: api-auditor
description:
Expertise in auditing and testing API endpoints. Use when the user asks to
"check", "test", or "audit" a URL or API.
---
# API Auditor Instructions
You act as a QA engineer specialized in API reliability. When this skill is
active, you MUST:
1. **Audit**: Use the bundled `scripts/audit.js` utility to check the
status of the provided URL.
2. **Report**: Analyze the output (status codes, latency) and explain any
failures in plain English.
3. **Secure**: Remind the user if they are testing a sensitive endpoint
without an `https://` protocol.
```
3. **Create the bundled Node.js script:** Create a file at
`.gemini/skills/api-auditor/scripts/audit.js`. This script will be used by
the agent to perform the actual check:
```javascript
// .gemini/skills/api-auditor/scripts/audit.js
const url = process.argv[2];
if (!url) {
console.error('Usage: node audit.js <url>');
process.exit(1);
}
console.log(`Auditing ${url}...`);
fetch(url, { method: 'HEAD' })
.then((r) => console.log(`Result: Success (Status ${r.status})`))
.catch((e) => console.error(`Result: Failed (${e.message})`));
```
## 3. Verify the Skill is Discovered
Use the `/skills` slash command (or `gemini skills list` from your terminal) to
see if Gemini CLI has found your new skill.
In a Gemini CLI session:
```
/skills list
```
You should see `api-auditor` in the list of available skills.
## 4. Use the Skill in a Chat
Now, let's see the skill in action. Start a new session and ask a question about
an endpoint.
**User:** "Can you audit http://geminili.com"
Gemini will recognize the request matches the `api-auditor` description and will
ask for your permission to activate it.
**Model:** (After calling `activate_skill`) "I've activated the **api-auditor**
skill. I'll run the audit script now..."
Gemini will then use the `run_shell_command` tool to execute your bundled Node
script:
`node .gemini/skills/api-auditor/scripts/audit.js http://geminili.com`
## Next Steps
- Explore [Agent Skills Authoring Guide](../skills.md#creating-a-skill) to learn
about more advanced skill features.
- Learn how to share skills via [Extensions](../../extensions/index.md).
+146 -25
View File
@@ -367,6 +367,12 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-3-pro-preview"
}
},
"gemini-3-flash-preview": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemini-3-flash-preview"
}
},
"gemini-2.5-pro": {
"extends": "chat-base-2.5",
"modelConfig": {
@@ -496,6 +502,11 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-3-pro-preview"
}
},
"chat-compression-3-flash": {
"modelConfig": {
"model": "gemini-3-flash-preview"
}
},
"chat-compression-2.5-pro": {
"modelConfig": {
"model": "gemini-2.5-pro"
@@ -610,6 +621,11 @@ their corresponding top-level category object in your `settings.json` file.
shell command. Defaults to 5 minutes.
- **Default:** `300`
- **`tools.shell.enableShellOutputEfficiency`** (boolean):
- **Description:** Enable shell output efficiency optimizations for better
performance.
- **Default:** `true`
- **`tools.autoAccept`** (boolean):
- **Description:** Automatically accept and execute tool calls that are
considered safe (e.g., read-only operations).
@@ -668,20 +684,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `1000`
- **Requires restart:** Yes
- **`tools.enableMessageBusIntegration`** (boolean):
- **Description:** Enable policy-based tool confirmation via message bus
integration. When enabled, tools automatically respect policy engine
decisions (ALLOW/DENY/ASK_USER) without requiring individual tool
implementations.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.enableHooks`** (boolean):
- **Description:** Enable the hooks system for intercepting and customizing
Gemini CLI behavior. When enabled, hooks configured in settings will execute
at appropriate lifecycle events (BeforeTool, AfterTool, BeforeModel, etc.).
Requires MessageBus integration.
- **Default:** `false`
- **Description:** Enables the hooks system experiment. When disabled, the
hooks system is completely deactivated regardless of other settings.
- **Default:** `true`
- **Requires restart:** Yes
#### `mcp`
@@ -701,12 +707,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `undefined`
- **Requires restart:** Yes
#### `useSmartEdit`
- **`useSmartEdit`** (boolean):
- **Description:** Enable the smart-edit tool instead of the replace tool.
- **Default:** `true`
#### `useWriteTodos`
- **`useWriteTodos`** (boolean):
@@ -720,6 +720,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.enablePermanentToolApproval`** (boolean):
- **Description:** Enable the "Allow for all future sessions" option in tool
confirmation dialogs.
- **Default:** `false`
- **`security.blockGitExtensions`** (boolean):
- **Description:** Blocks installing and loading extensions from Git.
- **Default:** `false`
@@ -730,6 +735,22 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.environmentVariableRedaction.allowed`** (array):
- **Description:** Environment variables to always allow (bypass redaction).
- **Default:** `[]`
- **Requires restart:** Yes
- **`security.environmentVariableRedaction.blocked`** (array):
- **Description:** Environment variables to always redact.
- **Default:** `[]`
- **Requires restart:** Yes
- **`security.environmentVariableRedaction.enabled`** (boolean):
- **Description:** Enable redaction of environment variables that may contain
secrets.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.auth.selectedType`** (string):
- **Description:** The currently selected authentication type.
- **Default:** `undefined`
@@ -773,7 +794,8 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents.
- **Description:** Enable local and remote subagents. Warning: Experimental
feature, uses YOLO mode for subagents
- **Default:** `false`
- **Requires restart:** Yes
@@ -787,13 +809,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.isModelAvailabilityServiceEnabled`** (boolean):
- **Description:** Enable model routing using new availability service.
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading.
- **`experimental.skills`** (boolean):
- **Description:** Enable Agent Skills (experimental).
- **Default:** `false`
- **Requires restart:** Yes
@@ -821,16 +843,42 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.codebaseInvestigatorSettings.model`** (string):
- **Description:** The model to use for the Codebase Investigator agent.
- **Default:** `"pro"`
- **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.introspectionAgentSettings.enabled`** (boolean):
- **Description:** Enable the Introspection Agent.
- **Default:** `false`
- **Requires restart:** Yes
#### `skills`
- **`skills.disabled`** (array):
- **Description:** List of disabled skills.
- **Default:** `[]`
- **Requires restart:** Yes
#### `hooks`
- **`hooks.enabled`** (boolean):
- **Description:** Canonical toggle for the hooks system. When disabled, no
hooks will be executed.
- **Default:** `false`
- **`hooks.disabled`** (array):
- **Description:** List of hook names (commands) that should be disabled.
Hooks in this list will not execute even if configured.
- **Default:** `[]`
- **`hooks.notifications`** (boolean):
- **Description:** Show visual indicators when hooks are executing.
- **Default:** `true`
- **`hooks.BeforeTool`** (array):
- **Description:** Hooks that execute before tool execution. Can intercept,
validate, or modify tool calls.
@@ -885,6 +933,21 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Hooks that execute before tool selection. Can filter or
prioritize available tools dynamically.
- **Default:** `[]`
#### `admin`
- **`admin.secureModeEnabled`** (boolean):
- **Description:** If true, disallows yolo mode from being used.
- **Default:** `false`
- **`admin.extensions.enabled`** (boolean):
- **Description:** If false, disallows extensions from being installed or
used.
- **Default:** `true`
- **`admin.mcp.enabled`** (boolean):
- **Description:** If false, disallows MCP servers from being used.
- **Default:** `true`
<!-- SETTINGS-AUTOGEN:END -->
#### `mcpServers`
@@ -1117,6 +1180,18 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GEMINI_SANDBOX`**:
- Alternative to the `sandbox` setting in `settings.json`.
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
- **`GEMINI_SYSTEM_MD`**:
- Replaces the builtin system prompt with content from a Markdown file.
- `true`/`1`: Use project default path `./.gemini/system.md`.
- Any other string: Treat as a path (relative/absolute supported, `~`
expands).
- `false`/`0` or unset: Use the builtin prompt. See
[System Prompt Override](../cli/system-prompt.md).
- **`GEMINI_WRITE_SYSTEM_MD`**:
- Writes the current builtin system prompt to a file for review.
- `true`/`1`: Write to `./.gemini/system.md`. Otherwise treat the value as a
path.
- Run the CLI once with this set to generate the file.
- **`SEATBELT_PROFILE`** (macOS specific):
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
- `permissive-open`: (Default) Restricts writes to the project folder (and a
@@ -1142,6 +1217,52 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- Specifies the endpoint for the code assist server.
- This is useful for development and testing.
### Environment variable redaction
To prevent accidental leakage of sensitive information, Gemini CLI automatically
redacts potential secrets from environment variables when executing tools (such
as shell commands). This "best effort" redaction applies to variables inherited
from the system or loaded from `.env` files.
**Default Redaction Rules:**
- **By Name:** Variables are redacted if their names contain sensitive terms
like `TOKEN`, `SECRET`, `PASSWORD`, `KEY`, `AUTH`, `CREDENTIAL`, `PRIVATE`, or
`CERT`.
- **By Value:** Variables are redacted if their values match known secret
patterns, such as:
- Private keys (RSA, OpenSSH, PGP, etc.)
- Certificates
- URLs containing credentials
- API keys and tokens (GitHub, Google, AWS, Stripe, Slack, etc.)
- **Specific Blocklist:** Certain variables like `CLIENT_ID`, `DB_URI`,
`DATABASE_URL`, and `CONNECTION_STRING` are always redacted by default.
**Allowlist (Never Redacted):**
- Common system variables (e.g., `PATH`, `HOME`, `USER`, `SHELL`, `TERM`,
`LANG`).
- Variables starting with `GEMINI_CLI_`.
- GitHub Action specific variables.
**Configuration:**
You can customize this behavior in your `settings.json` file:
- **`security.allowedEnvironmentVariables`**: A list of variable names to
_never_ redact, even if they match sensitive patterns.
- **`security.blockedEnvironmentVariables`**: A list of variable names to
_always_ redact, even if they don't match sensitive patterns.
```json
{
"security": {
"allowedEnvironmentVariables": ["MY_PUBLIC_KEY", "NOT_A_SECRET_TOKEN"],
"blockedEnvironmentVariables": ["INTERNAL_IP_ADDRESS"]
}
}
```
## Command-line arguments
Arguments passed directly when running the CLI can override other configurations
+1 -1
View File
@@ -24,7 +24,7 @@ Rename the photos in my "photos" directory based on their contents.
Result: Gemini will ask for permission to rename your files.
Select **Yes, allow once.** and your files will be renamed:
Select **Allow once** and your files will be renamed:
```bash
photos/yellow_flowers.png
+29 -48
View File
@@ -1,56 +1,37 @@
# Gemini 3 Pro on Gemini CLI
# Gemini 3 Pro and Gemini 3 Flash on Gemini CLI
Were excited to bring Gemini 3 Pro to Gemini CLI. Gemini 3 Pro is **currently
available** on Gemini CLI to all of the following subscribers:
Gemini 3 Pro and Gemini 3 Flash are now available on Gemini CLI! Currently, most
paid customers of Gemini CLI will have access to both Gemini 3 Pro and Gemini 3
Flash, including the following subscribers:
- Google AI Ultra (except Google AI Ultra for Business).
- Google AI Pro.
- Gemini Code Assist Standard (requires
- Google AI Pro and Google AI Ultra (excluding business customers).
- Gemini Code Assist Standard and Enterprise (requires
[administrative enablement](#administrator-instructions)).
- Gemini Code Assist Enterprise (requires
[administrative enablement](#administrator-instructions)).
- Paid Gemini API key holders.
- Paid Vertex API key holders.
- Paid Gemini API and Vertex API key holders.
For **everyone else**, we're gradually expanding access
[through a waitlist](https://goo.gle/geminicli-waitlist-signup). If you don't
have one of the listed subscriptions, sign up for the waitlist to access Gemini
3 Pro once approved.
For free tier users:
**Note:** Whether youre automatically granted access or accepted from the
waitlist, youll still need to enable Gemini 3 Pro
[using the `/settings` command](../cli/settings.md).
- If you signed up for the waitlist, please check your email for details. Weve
onboarded everyone who signed up to the previously available waitlist.
- If you were not on our waitlist, were rolling out additional access gradually
to ensure the experience remains fast and reliable. Stay tuned for more
details.
## How to join the waitlist
## How to get started with Gemini 3 on Gemini CLI
Users not automatically granted access will need to join the waitlist. Follow
these instructions to sign up:
Get started by upgrading Gemini CLI to the latest version (0.21.1):
- Install Gemini CLI.
- Authenticate using the **Login with Google** option. Youll see a banner that
says “Gemini 3 is now available.” If you do not see this banner, update your
installation of Gemini CLI to the most recent version.
- Fill out this Google form:
[Access Gemini 3 in Gemini CLI](https://goo.gle/geminicli-waitlist-signup).
Provide the email address of the account you used to authenticate with Gemini
CLI.
```bash
npm install -g @google/gemini-cli@latest
```
Users will be onboarded in batches, subject to availability. When youve been
granted access to Gemini 3 Pro, youll receive an acceptance email to your
submitted email address.
After youve confirmed your version is 0.21.1 or later:
**Note:** Please wait until you have been approved to use Gemini 3 Pro to enable
**Preview Features**. If enabled early, the CLI will fallback to Gemini 2.5 Pro.
1. Use the `/settings` command in Gemini CLI.
2. Toggle **Preview Features** to `true`.
3. Run `/model` and select **Auto (Gemini 3)**.
## How to use Gemini 3 Pro with Gemini CLI
Once you receive your acceptance emailor if you are automatically granted
accessyou still need to enable Gemini 3 Pro within Gemini CLI.
To enable Gemini 3 Pro, use the `/settings` command in Gemini CLI and set
**Preview Features** to `true`.
For more information, see [Gemini CLI Settings](../cli/settings.md).
For more information, see [Gemini CLI model selection](../cli/model.md).
### Usage limits and fallback
@@ -68,10 +49,10 @@ There may be times when the Gemini 3 Pro model is overloaded. When that happens,
Gemini CLI will ask you to decide whether you want to keep trying Gemini 3 Pro
or fallback to Gemini 2.5 Pro.
**Note:** The **Keep trying** option uses exponential backoff, in which Gemini
CLI waits longer between each retry, when the system is busy. If the retry
doesn't happen immediately, please wait a few minutes for the request to
process.
> **Note:** The **Keep trying** option uses exponential backoff, in which Gemini
> CLI waits longer between each retry, when the system is busy. If the retry
> doesn't happen immediately, please wait a few minutes for the request to
> process.
### Model selection and routing types
@@ -92,7 +73,7 @@ manage your usage limits:
To learn more about selecting a model and routing, refer to
[Gemini CLI Model Selection](../cli/model.md).
## How to enable Gemini 3 Pro with Gemini CLI on Gemini Code Assist
## How to enable Gemini 3 with Gemini CLI on Gemini Code Assist
If you're using Gemini Code Assist Standard or Gemini Code Assist Enterprise,
enabling Gemini 3 Pro on Gemini CLI requires configuring your release channels.
@@ -123,7 +104,7 @@ then:
- Use the `/settings` command.
- Set **Preview Features** to `true`.
Restart Gemini CLI and you should have access to Gemini 3 Pro.
Restart Gemini CLI and you should have access to Gemini 3.
## Need help?
+183 -133
View File
@@ -4,128 +4,6 @@ This guide covers security considerations, performance optimization, debugging
techniques, and privacy considerations for developing and deploying hooks in
Gemini CLI.
## Security considerations
### Validate all inputs
Never trust data from hooks without validation. Hook inputs may contain
user-provided data that could be malicious:
```bash
#!/usr/bin/env bash
input=$(cat)
# Validate JSON structure
if ! echo "$input" | jq empty 2>/dev/null; then
echo "Invalid JSON input" >&2
exit 1
fi
# Validate required fields
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
if [ -z "$tool_name" ]; then
echo "Missing tool_name field" >&2
exit 1
fi
```
### Use timeouts
Set reasonable timeouts to prevent hooks from hanging indefinitely:
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "*",
"hooks": [
{
"name": "slow-validator",
"type": "command",
"command": "./hooks/validate.sh",
"timeout": 5000
}
]
}
]
}
}
```
**Recommended timeouts:**
- Fast validation: 1000-5000ms
- Network requests: 10000-30000ms
- Heavy computation: 30000-60000ms
### Limit permissions
Run hooks with minimal required permissions:
```bash
#!/usr/bin/env bash
# Don't run as root
if [ "$EUID" -eq 0 ]; then
echo "Hook should not run as root" >&2
exit 1
fi
# Check file permissions before writing
if [ -w "$file_path" ]; then
# Safe to write
else
echo "Insufficient permissions" >&2
exit 1
fi
```
### Scan for secrets
Use `BeforeTool` hooks to prevent committing sensitive data:
```javascript
const SECRET_PATTERNS = [
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
/password\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/i,
/secret\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
/AKIA[0-9A-Z]{16}/, // AWS access key
/ghp_[a-zA-Z0-9]{36}/, // GitHub personal access token
/sk-[a-zA-Z0-9]{48}/, // OpenAI API key
];
function containsSecret(content) {
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
}
```
### Review external scripts
Always review hook scripts from untrusted sources before enabling them:
```bash
# Review before installing
cat third-party-hook.sh | less
# Check for suspicious patterns
grep -E 'curl|wget|ssh|eval' third-party-hook.sh
# Verify hook source
ls -la third-party-hook.sh
```
### Sandbox untrusted hooks
For maximum security, consider running untrusted hooks in isolated environments:
```bash
# Run hook in Docker container
docker run --rm \
-v "$GEMINI_PROJECT_DIR:/workspace:ro" \
-i untrusted-hook-image \
/hook-script.sh < input.json
```
## Performance
### Keep hooks fast
@@ -140,11 +18,13 @@ const data2 = await fetch(url2).then((r) => r.json());
const data3 = await fetch(url3).then((r) => r.json());
// Prefer parallel operations for better performance
const [data1, data2, data3] = await Promise.all([
fetch(url1).then((r) => r.json()),
fetch(url2).then((r) => r.json()),
fetch(url3).then((r) => r.json()),
]);
// 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, data3] = await Promise.all([p1, p2, p3]);
```
### Cache expensive operations
@@ -219,7 +99,7 @@ all tools with `*`, specify only the tools you need:
```json
{
"matcher": "WriteFile|Edit",
"matcher": "write_file|replace",
"hooks": [
{
"name": "validate-writes",
@@ -298,7 +178,7 @@ cat > test-input.json << 'EOF'
"session_id": "test-123",
"cwd": "/tmp/test",
"hook_event_name": "BeforeTool",
"tool_name": "WriteFile",
"tool_name": "write_file",
"tool_input": {
"file_path": "test.txt",
"content": "Test content"
@@ -437,7 +317,7 @@ Add descriptions to help others understand your hooks:
"hooks": {
"BeforeTool": [
{
"matcher": "WriteFile|Edit",
"matcher": "write_file|replace",
"hooks": [
{
"name": "secret-scanner",
@@ -484,7 +364,7 @@ Verify the hook appears in the list and is enabled.
```bash
# Test regex pattern
echo "WriteFile" | grep -E "Write.*|Edit"
echo "write_file|replace" | grep -E "write_.*|replace"
```
**Check disabled list:**
@@ -559,7 +439,7 @@ async function getCachedData(key) {
"hooks": {
"BeforeTool": [
{
"matcher": "WriteFile",
"matcher": "write_file",
"hooks": [
{
"name": "quick-check",
@@ -569,7 +449,7 @@ async function getCachedData(key) {
]
},
{
"matcher": "WriteFile",
"matcher": "write_file",
"hooks": [
{
"name": "deep-check",
@@ -714,6 +594,176 @@ if [ -f "$GEMINI_PROJECT_DIR/.env" ]; then
fi
```
## Using Hooks Securely
### Threat Model
Understanding where hooks come from and what they can do is critical for secure
usage.
| Hook Source | Description |
| :---------------------------- | :------------------------------------------------------------------------------------------------------------------------- |
| **System** | Configured by system administrators (e.g., `/etc/gemini-cli/settings.json`, `/Library/...`). Assumed to be the **safest**. |
| **User** (`~/.gemini/...`) | Configured by you. You are responsible for ensuring they are safe. |
| **Extensions** | You explicitly approve and install these. Security depends on the extension source (integrity). |
| **Project** (`./.gemini/...`) | **Untrusted by default.** Safest in trusted internal repos; higher risk in third-party/public repos. |
#### Project Hook Security
When you open a project with hooks defined in `.gemini/settings.json`:
1. **Detection**: Gemini CLI detects the hooks.
2. **Identification**: A unique identity is generated for each hook based on its
`name` and `command`.
3. **Warning**: If this specific hook identity has not been seen before, a
**warning** is displayed.
4. **Execution**: The hook is executed (unless specific security settings block
it).
5. **Trust**: The hook is marked as "trusted" for this project.
> [!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
| Risk | Description |
| :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
| **Arbitrary Code Execution** | Hooks run as your user. They can do anything you can do (delete files, install software). |
| **Data Exfiltration** | A hook could read your input (prompts), output (code), or environment variables (`GEMINI_API_KEY`) and send them to a remote server. |
| **Prompt Injection** | Malicious content in a file or web page could trick an LLM into running a tool that triggers a hook in an unexpected way. |
### Mitigation Strategies
#### Verify the source
**Verify the source** of any project hooks or extensions before enabling them.
- For open-source projects, a quick review of the hook scripts is recommended.
- For extensions, ensure you trust the author or publisher (e.g., verified
publishers, well-known community members).
- Be cautious with obfuscated scripts or compiled binaries from unknown sources.
#### Sanitize Environment
Hooks inherit the environment of the Gemini CLI process, which may include
sensitive API keys. Gemini CLI attempts to sanitize sensitive variables, but you
should be cautious.
- **Avoid printing environment variables** to stdout/stderr unless necessary.
- **Use `.env` files** to securely manage sensitive variables, ensuring they are
excluded from version control.
**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,
"blocked": ["MY_SECRET_KEY"],
"allowed": ["SAFE_VAR"]
}
}
}
```
## Authoring Secure Hooks
When writing your own hooks, follow these practices to ensure they are robust
and secure.
### Validate all inputs
Never trust data from hooks without validation. Hook inputs often come from the
LLM or user prompts, which can be manipulated.
```bash
#!/usr/bin/env bash
input=$(cat)
# Validate JSON structure
if ! echo "$input" | jq empty 2>/dev/null; then
echo "Invalid JSON input" >&2
exit 1
fi
# Validate tool_name explicitly
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
if [[ "$tool_name" != "write_file" && "$tool_name" != "read_file" ]]; then
echo "Unexpected tool: $tool_name" >&2
exit 1
fi
```
### Use timeouts
Prevent denial-of-service (hanging agents) by enforcing timeouts. Gemini CLI
defaults to 60 seconds, but you should set stricter limits for fast hooks.
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "*",
"hooks": [
{
"name": "fast-validator",
"command": "./hooks/validate.sh",
"timeout": 5000 // 5 seconds
}
]
}
]
}
}
```
### Limit permissions
Run hooks with minimal required permissions:
```bash
#!/usr/bin/env bash
# Don't run as root
if [ "$EUID" -eq 0 ]; then
echo "Hook should not run as root" >&2
exit 1
fi
# Check file permissions before writing
if [ -w "$file_path" ]; then
# Safe to write
else
echo "Insufficient permissions" >&2
exit 1
fi
```
### Example: Secret Scanner
Use `BeforeTool` hooks to prevent committing sensitive data. This is a powerful
pattern for enhancing security in your workflow.
```javascript
const SECRET_PATTERNS = [
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
/password\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/i,
/secret\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
/AKIA[0-9A-Z]{16}/, // AWS access key
/ghp_[a-zA-Z0-9]{36}/, // GitHub personal access token
/sk-[a-zA-Z0-9]{48}/, // OpenAI API key
];
function containsSecret(content) {
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
}
```
## Privacy considerations
Hook inputs and outputs may contain sensitive information. Gemini CLI respects
+150 -23
View File
@@ -7,6 +7,9 @@ the CLI's source code.
See [writing hooks guide](writing-hooks.md) for a tutorial on creating your
first hook and a comprehensive example.
See [hooks reference](reference.md) for the technical specification of the I/O
schemas.
See [best practices](best-practices.md) for guidelines on security, performance,
and debugging.
@@ -24,6 +27,28 @@ With hooks, you can:
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.
## Security and Risks
> [!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.
See [Security Considerations](best-practices.md#using-hooks-securely) for a
detailed threat model and mitigation strategies.
## Core concepts
### Hook events
@@ -70,7 +95,7 @@ trigger the hook:
"hooks": {
"BeforeTool": [
{
"matcher": "WriteFile|Edit",
"matcher": "write_file|replace",
"hooks": [
/* hooks for write operations */
]
@@ -82,8 +107,8 @@ trigger the hook:
**Matcher patterns:**
- **Exact match:** `"ReadFile"` matches only `ReadFile`
- **Regex:** `"Write.*|Edit"` matches `WriteFile`, `WriteBinary`, `Edit`
- **Exact match:** `"read_file"` matches only `read_file`
- **Regex:** `"write_.*|replace"` matches `write_file`, `replace`
- **Wildcard:** `"*"` or `""` matches all tools
**Session event matchers:**
@@ -115,6 +140,7 @@ 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"
@@ -130,7 +156,7 @@ Every hook receives these base fields:
```json
{
"tool_name": "WriteFile",
"tool_name": "write_file",
"tool_input": {
"file_path": "/path/to/file.ts",
"content": "..."
@@ -159,7 +185,7 @@ Or simple exit codes:
```json
{
"tool_name": "ReadFile",
"tool_name": "read_file",
"tool_input": { "file_path": "..." },
"tool_response": "file contents..."
}
@@ -212,7 +238,7 @@ Or simple exit codes:
"toolConfig": {
"functionCallingConfig": {
"mode": "AUTO",
"allowedFunctionNames": ["ReadFile", "WriteFile"]
"allowedFunctionNames": ["read_file", "write_file"]
}
}
}
@@ -316,7 +342,7 @@ Or simple exit codes:
"toolConfig": {
"functionCallingConfig": {
"mode": "ANY",
"allowedFunctionNames": ["ReadFile", "WriteFile", "Edit"]
"allowedFunctionNames": ["read_file", "write_file", "replace"]
}
}
}
@@ -326,7 +352,7 @@ Or simple exit codes:
Or simple output (comma-separated tool names sets mode to ANY):
```bash
echo "ReadFile,WriteFile,Edit"
echo "read_file,write_file,replace"
```
#### SessionStart
@@ -410,13 +436,22 @@ precedence rules.
### Configuration layers
Hook configurations are applied in the following order of precedence (higher
numbers override lower numbers):
Hook configurations are applied in the following order of execution (lower
numbers run first):
1. **System defaults:** Built-in default settings (lowest precedence)
2. **User settings:** `~/.gemini/settings.json`
3. **Project settings:** `.gemini/settings.json` in your project directory
4. **System settings:** `/etc/gemini-cli/settings.json` (highest precedence)
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)
#### 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.
@@ -446,8 +481,9 @@ configuration.
**Configuration properties:**
- **`name`** (string, required): Unique identifier for the hook used in
`/hooks enable/disable` commands
- **`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
@@ -494,6 +530,8 @@ You can temporarily enable or disable individual hooks using commands:
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 global User settings
(`~/.gemini/settings.json`).
### Disabled hooks configuration
@@ -524,7 +562,7 @@ This command:
- Reads `.claude/settings.json`
- Converts event names (`PreToolUse``BeforeTool`, etc.)
- Translates tool names (`Bash``RunShellCommand`, `Edit``Edit`)
- Translates tool names (`Bash``run_shell_command`, `replace``replace`)
- Updates matcher patterns
- Writes to `.gemini/settings.json`
@@ -543,12 +581,101 @@ This command:
### Tool name mapping
| Claude Code | Gemini CLI |
| ----------- | ----------------- |
| `Bash` | `RunShellCommand` |
| `Edit` | `Edit` |
| `Read` | `ReadFile` |
| `Write` | `WriteFile` |
| 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
+168
View File
@@ -0,0 +1,168 @@
# Hooks Reference
This document provides the technical specification for Gemini CLI hooks,
including the JSON schemas for input and output, exit code behaviors, and the
stable model API.
## Communication Protocol
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. |
---
## Input Schema (`stdin`)
Every hook receives a base JSON object. Extra fields are added depending on the
specific event.
### 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.
#### 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.
---
## Output Schema (`stdout`)
If the hook exits with `0`, the CLI attempts to parse `stdout` as JSON.
### 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 to the **user** in the CLI terminal. |
| `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). |
### `hookSpecificOutput` Reference
| 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 a decoupled format for model interactions to ensure hooks remain
stable even if the underlying Gemini SDK changes.
### `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 | Array<{ "type": string, [key: string]: any }>
}>,
"config"?: {
"temperature"?: number,
"maxOutputTokens"?: number,
"topP"?: number,
"topK"?: number
},
"toolConfig"?: {
"mode"?: "AUTO" | "ANY" | "NONE",
"allowedFunctionNames"?: string[]
}
}
```
### `LLMResponse` Object
Used in `AfterModel` and as a synthetic response in `BeforeModel`.
```typescript
{
"text"?: string,
"candidates": Array<{
"content": {
"role": "model",
"parts": string[]
},
"finishReason"?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "OTHER",
"index"?: number,
"safetyRatings"?: Array<{
"category": string,
"probability": string,
"blocked"?: boolean
}>
}>,
"usageMetadata"?: {
"promptTokenCount"?: number,
"candidatesTokenCount"?: number,
"totalTokenCount"?: number
}
}
```
+7 -7
View File
@@ -72,9 +72,9 @@ Run Gemini CLI and execute any command that uses tools:
```
> Read the README.md file
[Agent uses ReadFile tool]
[Agent uses read_file tool]
Logged: ReadFile
Logged: read_file
```
Check `.gemini/tool-log.txt` to see the logged tool executions.
@@ -110,7 +110,7 @@ exit 0
"hooks": {
"BeforeTool": [
{
"matcher": "WriteFile|Edit",
"matcher": "write_file|replace",
"hooks": [
{
"name": "secret-scanner",
@@ -167,7 +167,7 @@ exit 0
"hooks": {
"AfterTool": [
{
"matcher": "WriteFile|Edit",
"matcher": "write_file|replace",
"hooks": [
{
"name": "auto-test",
@@ -379,7 +379,7 @@ chmod +x .gemini/hooks/*.js
],
"BeforeTool": [
{
"matcher": "WriteFile|Edit",
"matcher": "write_file|replace",
"hooks": [
{
"name": "security-check",
@@ -392,7 +392,7 @@ chmod +x .gemini/hooks/*.js
],
"AfterTool": [
{
"matcher": "WriteFile|Edit",
"matcher": "write_file|replace",
"hooks": [
{
"name": "auto-test",
@@ -601,7 +601,7 @@ async function main() {
.map((k) => k.trim());
// Simple keyword-based filtering + core tools
const coreTools = ['ReadFile', 'WriteFile', 'Edit', 'RunShellCommand'];
const coreTools = ['read_file', 'write_file', 'replace', 'run_shell_command'];
const filtered = candidateTools.filter((tool) => {
if (coreTools.includes(tool)) return true;
const toolLower = tool.toLowerCase();
+2 -2
View File
@@ -128,8 +128,8 @@ editor.
You can also **modify the suggested changes** directly in the diff view before
accepting them.
If you select Yes, allow always in the CLI, changes will no longer show up in
the IDE as they will be auto-accepted.
If you select Allow for this session in the CLI, changes will no longer show
up in the IDE as they will be auto-accepted.
## Using with sandboxing
+3
View File
@@ -73,6 +73,9 @@ This documentation is organized into the following sections:
- **[Memport](./core/memport.md):** Using the Memory Import Processor.
- **[Tools API](./core/tools-api.md):** Information on how the core manages and
exposes tools.
- **[System Prompt Override](./cli/system-prompt.md):** Replace built-in system
instructions using `GEMINI_SYSTEM_MD`.
- **[Policy Engine](./core/policy-engine.md):** Use the Policy Engine for
fine-grained control over tool execution.
+13 -1
View File
@@ -24,7 +24,7 @@
"slug": "docs/get-started"
},
{
"label": "Gemini 3 Pro on Gemini CLI",
"label": "Gemini 3 on Gemini CLI",
"slug": "docs/get-started/gemini-3"
},
{
@@ -88,6 +88,10 @@
"label": "Session Management",
"slug": "docs/cli/session-management"
},
{
"label": "Agent Skills",
"slug": "docs/cli/skills"
},
{
"label": "Settings",
"slug": "docs/cli/settings"
@@ -115,6 +119,10 @@
{
"label": "Uninstall",
"slug": "docs/cli/uninstall"
},
{
"label": "System prompt override",
"slug": "docs/cli/system-prompt"
}
]
},
@@ -204,6 +212,10 @@
"label": "Writing hooks",
"slug": "docs/hooks/writing-hooks"
},
{
"label": "Hooks reference",
"slug": "docs/hooks/reference"
},
{
"label": "Best practices",
"slug": "docs/hooks/best-practices"
+19 -10
View File
@@ -12,7 +12,7 @@ import prettierConfig from 'eslint-config-prettier';
import importPlugin from 'eslint-plugin-import';
import vitest from '@vitest/eslint-plugin';
import globals from 'globals';
import licenseHeader from 'eslint-plugin-license-header';
import headers from 'eslint-plugin-headers';
import path from 'node:path';
import url from 'node:url';
@@ -164,7 +164,9 @@ export default tseslint.config(
'prefer-arrow-callback': 'error',
'prefer-const': ['error', { destructuring: 'all' }],
radix: 'error',
'no-console': 'error',
'default-case': 'error',
'@typescript-eslint/await-thenable': ['error'],
'@typescript-eslint/no-floating-promises': ['error'],
'@typescript-eslint/no-unnecessary-type-assertion': ['error'],
},
@@ -208,19 +210,26 @@ export default tseslint.config(
{
files: ['./**/*.{tsx,ts,js}'],
plugins: {
'license-header': licenseHeader,
headers,
import: importPlugin,
},
rules: {
'license-header/header': [
'headers/header-format': [
'error',
[
'/**',
' * @license',
' * Copyright 2025 Google LLC',
' * SPDX-License-Identifier: Apache-2.0',
' */',
],
{
source: 'string',
content: [
'@license',
'Copyright (year) Google LLC',
'SPDX-License-Identifier: Apache-2.0',
].join('\n'),
patterns: {
year: {
pattern: '202[5-6]',
defaultValue: '2026',
},
},
},
],
'import/enforce-node-protocol-usage': ['error', 'always'],
},
@@ -32,7 +32,7 @@ describe('Interactive Mode', () => {
await run.sendKeys(
'Write a 200 word story about a robot. The story MUST end with the text THE_END followed by a period.',
);
await run.sendKeys('\r');
await run.type('\r');
// Wait for the specific end marker.
await run.expectText('THE_END.', 30000);
+15 -6
View File
@@ -95,7 +95,10 @@ describe('extension reloading', () => {
// Poll for the updated list
await rig.pollCommand(
() => run.sendKeys('\u0015/mcp list\r'),
async () => {
await run.sendText('/mcp list');
await run.type('\r');
},
() => {
const output = stripAnsi(run.output);
return (
@@ -110,9 +113,9 @@ describe('extension reloading', () => {
// Update the extension, expect the list to update, and mcp servers as well.
await run.sendKeys('\u0015/extensions update test-extension');
await run.expectText('/extensions update test-extension');
await run.sendKeys('\r');
await run.type('\r');
await new Promise((resolve) => setTimeout(resolve, 500));
await run.sendKeys('\r');
await run.type('\r');
await run.expectText(
` * test-server (remote): http://localhost:${portB}/mcp`,
);
@@ -123,7 +126,10 @@ describe('extension reloading', () => {
// Poll for the updated extension version
await rig.pollCommand(
() => run.sendKeys('\u0015/extensions list\r'),
async () => {
await run.sendText('/extensions list');
await run.type('\r');
},
() =>
stripAnsi(run.output).includes(
'test-extension (v0.0.2) - active (updated)',
@@ -133,7 +139,10 @@ describe('extension reloading', () => {
// Poll for the updated mcp tool
await rig.pollCommand(
() => run.sendKeys('\u0015/mcp list\r'),
async () => {
await run.sendText('/mcp list');
await run.type('\r');
},
() => {
const output = stripAnsi(run.output);
return (
@@ -146,7 +155,7 @@ describe('extension reloading', () => {
);
await run.sendText('/quit');
await run.sendKeys('\r');
await run.type('\r');
// Clean things up.
await serverA.stop();
@@ -37,7 +37,7 @@ describe('Interactive file system', () => {
// Step 1: Read the file
const readPrompt = `Read the version from ${fileName}`;
await run.type(readPrompt);
await run.sendKeys('\r');
await run.type('\r');
const readCall = await rig.waitForToolCall('read_file', 30000);
expect(readCall, 'Expected to find a read_file tool call').toBe(true);
@@ -45,7 +45,7 @@ describe('Interactive file system', () => {
// Step 2: Write the file
const writePrompt = `now change the version to 1.0.1 in the file`;
await run.type(writePrompt);
await run.sendKeys('\r');
await run.type('\r');
// Check tool calls made with right args
await rig.expectToolCallSuccess(
@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"list_dir","args":{"path":"."}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Final Answer"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Responding**\n\nI will respond to the user's request.\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":100,"totalTokenCount":120,"promptTokensDetails":[{"modality":"TEXT","tokenCount":100}],"thoughtsTokenCount":20}},{"candidates":[{"content":{"parts":[{"text":"Response to: "}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":5,"totalTokenCount":125,"promptTokensDetails":[{"modality":"TEXT","tokenCount":100}],"thoughtsTokenCount":20}},{"candidates":[{"content":{"parts":[{"text":"Hello World"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":7,"totalTokenCount":127,"promptTokensDetails":[{"modality":"TEXT","tokenCount":100}],"thoughtsTokenCount":20}}]}
+214
View File
@@ -0,0 +1,214 @@
/**
* @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 { join } from 'node:path';
import { writeFileSync } from 'node:fs';
describe('Hooks Agent Flow', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
if (rig) {
await rig.cleanup();
}
});
describe('BeforeAgent Hooks', () => {
it('should inject additional context via BeforeAgent hook', async () => {
await rig.setup('should inject additional context via BeforeAgent hook', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-agent-flow.responses',
),
});
const hookScript = `
try {
const output = {
decision: "allow",
hookSpecificOutput: {
hookEventName: "BeforeAgent",
additionalContext: "SYSTEM INSTRUCTION: This is injected context."
}
};
process.stdout.write(JSON.stringify(output));
} catch (e) {
console.error('Failed to write stdout:', e);
process.exit(1);
}
console.error('DEBUG: BeforeAgent hook executed');
`;
const scriptPath = join(rig.testDir!, 'before_agent_context.cjs');
writeFileSync(scriptPath, hookScript);
await rig.setup('should inject additional context via BeforeAgent hook', {
settings: {
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
});
await rig.run({ args: 'Hello test' });
// Verify hook execution and telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
const hookLogs = rig.readHookLogs();
const beforeAgentLog = hookLogs.find(
(log) => log.hookCall.hook_event_name === 'BeforeAgent',
);
expect(beforeAgentLog).toBeDefined();
expect(beforeAgentLog?.hookCall.stdout).toContain('injected context');
expect(beforeAgentLog?.hookCall.stdout).toContain('"decision":"allow"');
expect(beforeAgentLog?.hookCall.stdout).toContain(
'SYSTEM INSTRUCTION: This is injected context.',
);
});
});
describe('AfterAgent Hooks', () => {
it('should receive prompt and response in AfterAgent hook', async () => {
await rig.setup('should receive prompt and response in AfterAgent hook', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-agent-flow.responses',
),
});
const hookScript = `
const fs = require('fs');
try {
const input = fs.readFileSync(0, 'utf-8');
console.error('DEBUG: AfterAgent hook input received');
process.stdout.write("Received Input: " + input);
} catch (err) {
console.error('Hook Failed:', err);
process.exit(1);
}
`;
const scriptPath = join(rig.testDir!, 'after_agent_verify.cjs');
writeFileSync(scriptPath, hookScript);
await rig.setup('should receive prompt and response in AfterAgent hook', {
settings: {
hooks: {
enabled: true,
AfterAgent: [
{
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
});
await rig.run({ args: 'Hello validation' });
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
const hookLogs = rig.readHookLogs();
const afterAgentLog = hookLogs.find(
(log) => log.hookCall.hook_event_name === 'AfterAgent',
);
expect(afterAgentLog).toBeDefined();
// Verify the hook stdout contains the input we echoed which proves the
// hook received the prompt and response
expect(afterAgentLog?.hookCall.stdout).toContain('Received Input');
expect(afterAgentLog?.hookCall.stdout).toContain('Hello validation');
// The fake response contains "Hello World"
expect(afterAgentLog?.hookCall.stdout).toContain('Hello World');
});
});
describe('Multi-step Loops', () => {
it('should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', async () => {
await rig.setup(
'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-agent-flow-multistep.responses',
),
settings: {
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: `node -e "console.log('BeforeAgent Fired')"`,
timeout: 5000,
},
],
},
],
AfterAgent: [
{
hooks: [
{
type: 'command',
command: `node -e "console.log('AfterAgent Fired')"`,
timeout: 5000,
},
],
},
],
},
},
},
);
await rig.run({ args: 'Do a multi-step task' });
const hookLogs = rig.readHookLogs();
const beforeAgentLogs = hookLogs.filter(
(log) => log.hookCall.hook_event_name === 'BeforeAgent',
);
const afterAgentLogs = hookLogs.filter(
(log) => log.hookCall.hook_event_name === 'AfterAgent',
);
expect(beforeAgentLogs).toHaveLength(1);
expect(afterAgentLogs).toHaveLength(1);
const afterAgentLog = afterAgentLogs[0];
expect(afterAgentLog).toBeDefined();
expect(afterAgentLog?.hookCall.stdout).toContain('AfterAgent Fired');
});
});
});
@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Hi there!"}],"role":"model"},"finishReason":"STOP","index":0}]}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Clarification: I am a bot."}],"role":"model"},"finishReason":"STOP","index":0}]}]}
@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Initializing File Creation**\n\nI'm starting to think about how to make a new file called `test.txt`. My plan is to use a `write_file` tool. I'll need to specify the location and what the file should contain. For now, it will be empty.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":13216,"totalTokenCount":13269,"promptTokensDetails":[{"modality":"TEXT","tokenCount":13216}],"thoughtsTokenCount":53}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"test.txt","content":""}},"thoughtSignature":"CiQBcsjafJ20Qbx0YvING6aZ0wYoGWJh3eqornOG4E4AfBLiVsQKXwFyyNp8UlwYs/pv9IRQQGhDlrmlOJF2hfQijryyUYLI+qjDYTpZ6KKIfZF4+vS0soL2BJ3eTXA6gaadFEfNQem3WQVeQoKLFoW4Hv4mbasXqQc0K3p15DuSAtZZENTbCnsBcsjafGK+BJyF/Npnd7gyU0TL5PXePT0nuDFjhJDxlSRUJHDP315TewD3PUYsXd10oWsfhy4B5AngyUiBPUoajdsxg8WxaxnOZYqcp8EIuwtGZrCTev6IihT5nE5jj7u0P9vtnCmkAc6p+4O7Q7Jku1uVGqeJChgzI4YKSAFyyNp8EXSdbttV4xzX+NLKkc276L8Y63tnKU6/Y7fc9/58tU29DSdrgwfe9qmvwtTsO0piFXSLazqHJt8h2bgR7A7GnKDiIA=="}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":13216,"candidatesTokenCount":21,"totalTokenCount":13290,"promptTokensDetails":[{"modality":"TEXT","tokenCount":13216}],"thoughtsTokenCount":53}},{"candidates":[{"content":{"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":13216,"candidatesTokenCount":21,"totalTokenCount":13290,"promptTokensDetails":[{"modality":"TEXT","tokenCount":13216}],"thoughtsTokenCount":53}}]}
@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"content":"original content","file_path":"original.txt"}}}],"role":"model"},"finishReason":"STOP","index":0}]}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I have created the file."}],"role":"model"},"finishReason":"STOP","index":0}]}]}
+438 -163
View File
@@ -32,10 +32,8 @@ describe('Hooks System Integration', () => {
'hooks-system.block-tool.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
matcher: 'write_file',
@@ -44,7 +42,7 @@ describe('Hooks System Integration', () => {
{
type: 'command',
command:
'echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"File writing blocked by security policy\\"}"',
"node -e \"console.log(JSON.stringify({decision: 'block', reason: 'File writing blocked by security policy'}))\"",
timeout: 5000,
},
],
@@ -86,10 +84,8 @@ describe('Hooks System Integration', () => {
'hooks-system.allow-tool.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
matcher: 'write_file',
@@ -97,7 +93,7 @@ describe('Hooks System Integration', () => {
{
type: 'command',
command:
'echo "{\\"decision\\": \\"allow\\", \\"reason\\": \\"File writing approved\\"}"',
"node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'File writing approved'}))\"",
timeout: 5000,
},
],
@@ -129,17 +125,15 @@ describe('Hooks System Integration', () => {
describe('Command Hooks - Additional Context', () => {
it('should add additional context from AfterTool hooks', async () => {
const command =
'echo "{\\"hookSpecificOutput\\": {\\"hookEventName\\": \\"AfterTool\\", \\"additionalContext\\": \\"Security scan: File content appears safe\\"}}"';
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'Security scan: File content appears safe'}}))\"";
await rig.setup('should add additional context from AfterTool hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-tool-context.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
AfterTool: [
{
matcher: 'read_file',
@@ -190,40 +184,35 @@ describe('Hooks System Integration', () => {
'hooks-system.before-model.responses',
),
});
const hookScript = `#!/bin/bash
echo '{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeModel",
"llm_request": {
"messages": [
const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
decision: "allow",
hookSpecificOutput: {
hookEventName: "BeforeModel",
llm_request: {
messages: [
{
"role": "user",
"content": "Please respond with exactly: The security hook modified this request successfully."
role: "user",
content: "Please respond with exactly: The security hook modified this request successfully."
}
]
}
}
}'`;
}));`;
const scriptPath = join(rig.testDir!, 'before_model_hook.sh');
const scriptPath = join(rig.testDir!, 'before_model_hook.cjs');
writeFileSync(scriptPath, hookScript);
// Make executable
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should modify LLM requests with BeforeModel hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeModel: [
{
hooks: [
{
type: 'command',
command: scriptPath,
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
@@ -250,7 +239,9 @@ echo '{
expect(hookTelemetryFound[0].hookCall.hook_event_name).toBe(
'BeforeModel',
);
expect(hookTelemetryFound[0].hookCall.hook_name).toBe(scriptPath);
expect(hookTelemetryFound[0].hookCall.hook_name).toBe(
`node "${scriptPath}"`,
);
expect(hookTelemetryFound[0].hookCall.hook_input).toBeDefined();
expect(hookTelemetryFound[0].hookCall.hook_output).toBeDefined();
expect(hookTelemetryFound[0].hookCall.exit_code).toBe(0);
@@ -270,43 +261,39 @@ echo '{
),
});
// Create a hook script that modifies the LLM response
const hookScript = `#!/bin/bash
echo '{
"hookSpecificOutput": {
"hookEventName": "AfterModel",
"llm_response": {
"candidates": [
const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: "AfterModel",
llm_response: {
candidates: [
{
"content": {
"role": "model",
"parts": [
content: {
role: "model",
parts: [
"[FILTERED] Response has been filtered for security compliance."
]
},
"finishReason": "STOP"
finishReason: "STOP"
}
]
}
}
}'`;
}));`;
const scriptPath = join(rig.testDir!, 'after_model_hook.sh');
const scriptPath = join(rig.testDir!, 'after_model_hook.cjs');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should modify LLM responses with AfterModel hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
AfterModel: [
{
hooks: [
{
type: 'command',
command: scriptPath,
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
@@ -343,17 +330,15 @@ echo '{
);
// Create inline hook command (works on both Unix and Windows)
const hookCommand =
'echo "{\\"hookSpecificOutput\\": {\\"hookEventName\\": \\"BeforeToolSelection\\", \\"toolConfig\\": {\\"mode\\": \\"ANY\\", \\"allowedFunctionNames\\": [\\"read_file\\", \\"run_shell_command\\"]}}}"';
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'BeforeToolSelection', toolConfig: {mode: 'ANY', allowedFunctionNames: ['read_file', 'run_shell_command']}}}))\"";
await rig.setup(
'should modify tool selection with BeforeToolSelection hooks',
{
settings: {
debugMode: true,
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeToolSelection: [
{
hooks: [
@@ -404,32 +389,28 @@ echo '{
),
});
// Create a hook script that adds context to the prompt
const hookScript = `#!/bin/bash
echo '{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "SYSTEM INSTRUCTION: You are in a secure environment. Always mention security compliance in your responses."
const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
decision: "allow",
hookSpecificOutput: {
hookEventName: "BeforeAgent",
additionalContext: "SYSTEM INSTRUCTION: You are in a secure environment. Always mention security compliance in your responses."
}
}'`;
}));`;
const scriptPath = join(rig.testDir!, 'before_agent_hook.sh');
const scriptPath = join(rig.testDir!, 'before_agent_hook.cjs');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should augment prompts with BeforeAgent hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: scriptPath,
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
@@ -452,9 +433,10 @@ echo '{
describe('Notification Hooks - Permission Handling', () => {
it('should handle notification hooks for tool permissions', async () => {
// Create inline hook command (works on both Unix and Windows)
// Create inline hook command (works on both Unix and Windows)
const hookCommand =
'echo "{\\"suppressOutput\\": false, \\"systemMessage\\": \\"Permission request logged by security hook\\"}"';
'node -e "console.log(JSON.stringify({suppressOutput: false, systemMessage: \'Permission request logged by security hook\'}))"';
await rig.setup('should handle notification hooks for tool permissions', {
fakeResponsesPath: join(
@@ -464,11 +446,11 @@ echo '{
settings: {
// Configure tools to enable hooks and require confirmation to trigger notifications
tools: {
enableHooks: true,
approval: 'ASK', // Disable YOLO mode to show permission prompts
confirmationRequired: ['run_shell_command'],
},
hooks: {
enabled: true,
Notification: [
{
matcher: 'ToolPermission',
@@ -548,9 +530,9 @@ echo '{
it('should execute hooks sequentially when configured', async () => {
// Create inline hook commands (works on both Unix and Windows)
const hook1Command =
'echo "{\\"decision\\": \\"allow\\", \\"hookSpecificOutput\\": {\\"hookEventName\\": \\"BeforeAgent\\", \\"additionalContext\\": \\"Step 1: Initial validation passed.\\"}}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 1: Initial validation passed.'}}))\"";
const hook2Command =
'echo "{\\"decision\\": \\"allow\\", \\"hookSpecificOutput\\": {\\"hookEventName\\": \\"BeforeAgent\\", \\"additionalContext\\": \\"Step 2: Security check completed.\\"}}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'Step 2: Security check completed.'}}))\"";
await rig.setup('should execute hooks sequentially when configured', {
fakeResponsesPath: join(
@@ -558,10 +540,8 @@ echo '{
'hooks-system.sequential-execution.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
sequential: true,
@@ -621,36 +601,33 @@ echo '{
),
});
// Create a hook script that validates the input format
const hookScript = `#!/bin/bash
# Read JSON input from stdin
input=$(cat)
const hookScript = `const fs = require('fs');
const input = fs.readFileSync(0, 'utf-8');
try {
const json = JSON.parse(input);
// Check fields
if (json.session_id && json.cwd && json.hook_event_name && json.timestamp && json.tool_name && json.tool_input) {
console.log(JSON.stringify({decision: "allow", reason: "Input format is correct"}));
} else {
console.log(JSON.stringify({decision: "block", reason: "Input format is invalid"}));
}
} catch (e) {
console.log(JSON.stringify({decision: "block", reason: "Invalid JSON"}));
}`;
# Check for required fields
if echo "$input" | jq -e '.session_id and .cwd and .hook_event_name and .timestamp and .tool_name and .tool_input' > /dev/null 2>&1; then
echo '{"decision": "allow", "reason": "Input format is correct"}'
exit 0
else
echo '{"decision": "block", "reason": "Input format is invalid"}'
exit 0
fi`;
const scriptPath = join(rig.testDir!, 'input_validation_hook.sh');
const scriptPath = join(rig.testDir!, 'input_validation_hook.cjs');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should provide correct input format to hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
{
type: 'command',
command: scriptPath,
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
@@ -682,11 +659,11 @@ fi`;
it('should handle hooks for all major event types', async () => {
// Create inline hook commands (works on both Unix and Windows)
const beforeToolCommand =
'echo "{\\"decision\\": \\"allow\\", \\"systemMessage\\": \\"BeforeTool: File operation logged\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'BeforeTool: File operation logged'}))\"";
const afterToolCommand =
'echo "{\\"hookSpecificOutput\\": {\\"hookEventName\\": \\"AfterTool\\", \\"additionalContext\\": \\"AfterTool: Operation completed successfully\\"}}"';
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'AfterTool', additionalContext: 'AfterTool: Operation completed successfully'}}))\"";
const beforeAgentCommand =
'echo "{\\"decision\\": \\"allow\\", \\"hookSpecificOutput\\": {\\"hookEventName\\": \\"BeforeAgent\\", \\"additionalContext\\": \\"BeforeAgent: User request processed\\"}}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', hookSpecificOutput: {hookEventName: 'BeforeAgent', additionalContext: 'BeforeAgent: User request processed'}}))\"";
await rig.setup('should handle hooks for all major event types', {
fakeResponsesPath: join(
@@ -694,10 +671,8 @@ fi`;
'hooks-system.multiple-events.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeAgent: [
{
hooks: [
@@ -802,17 +777,15 @@ fi`;
// Create a hook script that fails
// Create inline hook commands (works on both Unix and Windows)
// Failing hook: exits with non-zero code
const failingCommand = 'exit 1';
const failingCommand = 'node -e "process.exit(1)"';
// Working hook: returns success with JSON
const workingCommand =
'echo "{\\"decision\\": \\"allow\\", \\"reason\\": \\"Working hook succeeded\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'Working hook succeeded'}))\"";
await rig.setup('should handle hook failures gracefully', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
@@ -855,7 +828,7 @@ fi`;
it('should generate telemetry events for hook executions', async () => {
// Create inline hook command (works on both Unix and Windows)
const hookCommand =
'echo "{\\"decision\\": \\"allow\\", \\"reason\\": \\"Telemetry test hook\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', reason: 'Telemetry test hook'}))\"";
await rig.setup('should generate telemetry events for hook executions', {
fakeResponsesPath: join(
@@ -863,10 +836,8 @@ fi`;
'hooks-system.telemetry.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
@@ -898,7 +869,7 @@ fi`;
it('should fire SessionStart hook on app startup', async () => {
// Create inline hook command that outputs JSON
const sessionStartCommand =
'echo "{\\"decision\\": \\"allow\\", \\"systemMessage\\": \\"Session starting on startup\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting on startup'}))\"";
await rig.setup('should fire SessionStart hook on app startup', {
fakeResponsesPath: join(
@@ -906,10 +877,8 @@ fi`;
'hooks-system.session-startup.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
SessionStart: [
{
matcher: 'startup',
@@ -955,12 +924,172 @@ fi`;
}
});
it('should fire SessionStart hook and inject context', async () => {
// Create hook script that outputs JSON with additionalContext
const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
decision: 'allow',
systemMessage: 'Context injected via SessionStart hook',
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: 'If asked about your identity, you must respond with the following: I am a protocol droid that speaks the bacchi.'
}
}));`;
await rig.setup('should fire SessionStart hook and inject context', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.session-startup.responses',
),
});
const scriptPath = join(rig.testDir!, 'session_start_context_hook.cjs');
writeFileSync(scriptPath, hookScript);
await rig.setup('should fire SessionStart hook and inject context', {
settings: {
hooks: {
enabled: true,
SessionStart: [
{
matcher: 'startup',
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
});
// Run a query - the SessionStart hook will fire during app initialization
const result = await rig.run({ args: 'Who are you?' });
// Check if systemMessage was displayed (in stderr, which rig.run captures)
expect(result).toContain('Context injected via SessionStart hook');
// Check if additionalContext influenced the model response
// Note: We use fake responses, but the rig records interactions.
// If we are using fake responses, the model won't actually respond unless we provide a fake response for the injected context.
// But the test rig setup uses 'hooks-system.session-startup.responses'.
// If I'm adding a new test, I might need to generate new fake responses or expect the context to be sent to the model (verify API logs).
// Verify hook executed
const hookLogs = rig.readHookLogs();
const sessionStartLog = hookLogs.find(
(log) => log.hookCall.hook_event_name === 'SessionStart',
);
expect(sessionStartLog).toBeDefined();
// Verify the API request contained the injected context
// rig.readAllApiRequest() gives us telemetry on API requests.
const apiRequests = rig.readAllApiRequest();
// We expect at least one API request
expect(apiRequests.length).toBeGreaterThan(0);
// The injected context should be in the request text
// For non-interactive mode, I prepended it to input: "context\n\ninput"
// The telemetry `request_text` should contain it.
const requestText = apiRequests[0].attributes?.request_text || '';
expect(requestText).toContain('protocol droid');
});
it('should fire SessionStart hook and display systemMessage in interactive mode', async () => {
// Create hook script that outputs JSON with systemMessage and additionalContext
const hookScript = `const fs = require('fs');
console.log(JSON.stringify({
decision: 'allow',
systemMessage: 'Interactive Session Start Message',
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: 'The user is a Jedi Master.'
}
}));`;
await rig.setup(
'should fire SessionStart hook and display systemMessage in interactive mode',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.session-startup.responses',
),
},
);
const scriptPath = join(
rig.testDir!,
'session_start_interactive_hook.cjs',
);
writeFileSync(scriptPath, hookScript);
await rig.setup(
'should fire SessionStart hook and display systemMessage in interactive mode',
{
settings: {
hooks: {
enabled: true,
SessionStart: [
{
matcher: 'startup',
hooks: [
{
type: 'command',
command: `node "${scriptPath}"`,
timeout: 5000,
},
],
},
],
},
},
},
);
const run = await rig.runInteractive();
// Verify systemMessage is displayed
await run.expectText('Interactive Session Start Message', 10000);
// Send a prompt to establish a session and trigger an API call
await run.sendKeys('Hello');
await run.type('\r');
// Wait for response to ensure API call happened
await run.expectText('Hello', 15000);
// Wait for telemetry to be written to disk
await rig.waitForTelemetryReady();
// Verify the API request contained the injected context
// We may need to poll for API requests as they are written asynchronously
const pollResult = await poll(
() => {
const apiRequests = rig.readAllApiRequest();
return apiRequests.length > 0;
},
15000,
500,
);
expect(pollResult).toBe(true);
const apiRequests = rig.readAllApiRequest();
// The injected context should be in the request_text of the API request
const requestText = apiRequests[0].attributes?.request_text || '';
expect(requestText).toContain('Jedi Master');
});
it('should fire SessionEnd and SessionStart hooks on /clear command', async () => {
// Create inline hook commands for both SessionEnd and SessionStart
const sessionEndCommand =
'echo "{\\"decision\\": \\"allow\\", \\"systemMessage\\": \\"Session ending due to clear\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session ending due to clear'}))\"";
const sessionStartCommand =
'echo "{\\"decision\\": \\"allow\\", \\"systemMessage\\": \\"Session starting after clear\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'Session starting after clear'}))\"";
await rig.setup(
'should fire SessionEnd and SessionStart hooks on /clear command',
@@ -970,10 +1099,8 @@ fi`;
'hooks-system.session-clear.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
SessionEnd: [
{
matcher: '*',
@@ -1007,7 +1134,7 @@ fi`;
// Send an initial prompt to establish a session
await run.sendKeys('Say hello');
await run.sendKeys('\r');
await run.type('\r');
// Wait for the response
await run.expectText('Hello', 10000);
@@ -1017,14 +1144,14 @@ fi`;
const numClears = 3;
for (let i = 0; i < numClears; i++) {
await run.sendKeys('/clear');
await run.sendKeys('\r');
await run.type('\r');
// Wait a bit for clear to complete
await new Promise((resolve) => setTimeout(resolve, 2000));
// Send a prompt to establish an active session before next clear
await run.sendKeys('Say hello');
await run.sendKeys('\r');
await run.type('\r');
// Wait for response
await run.expectText('Hello', 10000);
@@ -1136,7 +1263,7 @@ fi`;
it('should fire PreCompress hook on automatic compression', async () => {
// Create inline hook command that outputs JSON
const preCompressCommand =
'echo "{\\"decision\\": \\"allow\\", \\"systemMessage\\": \\"PreCompress hook executed for automatic compression\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'PreCompress hook executed for automatic compression'}))\"";
await rig.setup('should fire PreCompress hook on automatic compression', {
fakeResponsesPath: join(
@@ -1144,10 +1271,8 @@ fi`;
'hooks-system.compress-auto.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
PreCompress: [
{
matcher: 'auto',
@@ -1203,7 +1328,7 @@ fi`;
describe('SessionEnd on Exit', () => {
it('should fire SessionEnd hook on graceful exit in non-interactive mode', async () => {
const sessionEndCommand =
'echo "{\\"decision\\": \\"allow\\", \\"systemMessage\\": \\"SessionEnd hook executed on exit\\"}"';
"node -e \"console.log(JSON.stringify({decision: 'allow', systemMessage: 'SessionEnd hook executed on exit'}))\"";
await rig.setup('should fire SessionEnd hook on graceful exit', {
fakeResponsesPath: join(
@@ -1211,10 +1336,8 @@ fi`;
'hooks-system.session-startup.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
SessionEnd: [
{
matcher: 'exit',
@@ -1297,44 +1420,39 @@ fi`;
});
// Create two hook scripts - one enabled, one disabled
const enabledHookScript = `#!/bin/bash
echo '{"decision": "allow", "systemMessage": "Enabled hook executed"}'`;
const enabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "allow", systemMessage: "Enabled hook executed"}));`;
const disabledHookScript = `#!/bin/bash
echo '{"decision": "block", "systemMessage": "Disabled hook should not execute", "reason": "This hook should be disabled"}'`;
const disabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook should be disabled"}));`;
const enabledPath = join(rig.testDir!, 'enabled_hook.sh');
const disabledPath = join(rig.testDir!, 'disabled_hook.sh');
const enabledPath = join(rig.testDir!, 'enabled_hook.cjs');
const disabledPath = join(rig.testDir!, 'disabled_hook.cjs');
writeFileSync(enabledPath, enabledHookScript);
writeFileSync(disabledPath, disabledHookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${enabledPath}"`);
execSync(`chmod +x "${disabledPath}"`);
await rig.setup('should not execute hooks disabled in settings file', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
{
type: 'command',
command: enabledPath,
command: `node "${enabledPath}"`,
timeout: 5000,
},
{
type: 'command',
command: disabledPath,
command: `node "${disabledPath}"`,
timeout: 5000,
},
],
},
],
disabled: [disabledPath], // Disable the second hook
disabled: [`node "${disabledPath}"`], // Disable the second hook
},
},
});
@@ -1358,10 +1476,10 @@ echo '{"decision": "block", "systemMessage": "Disabled hook should not execute",
// Check hook telemetry - only enabled hook should have executed
const hookLogs = rig.readHookLogs();
const enabledHookLog = hookLogs.find(
(log) => log.hookCall.hook_name === enabledPath,
(log) => log.hookCall.hook_name === `node "${enabledPath}"`,
);
const disabledHookLog = hookLogs.find(
(log) => log.hookCall.hook_name === disabledPath,
(log) => log.hookCall.hook_name === `node "${disabledPath}"`,
);
expect(enabledHookLog).toBeDefined();
@@ -1380,46 +1498,41 @@ echo '{"decision": "block", "systemMessage": "Disabled hook should not execute",
);
// Create two hook scripts - one that will be disabled, one that won't
const activeHookScript = `#!/bin/bash
echo '{"decision": "allow", "systemMessage": "Active hook executed"}'`;
const activeHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "allow", systemMessage: "Active hook executed"}));`;
const disabledHookScript = `#!/bin/bash
echo '{"decision": "block", "systemMessage": "Disabled hook should not execute", "reason": "This hook is disabled"}'`;
const disabledHookScript = `const fs = require('fs');
console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook should not execute", reason: "This hook is disabled"}));`;
const activePath = join(rig.testDir!, 'active_hook.sh');
const disabledPath = join(rig.testDir!, 'disabled_hook.sh');
const activePath = join(rig.testDir!, 'active_hook.cjs');
const disabledPath = join(rig.testDir!, 'disabled_hook.cjs');
writeFileSync(activePath, activeHookScript);
writeFileSync(disabledPath, disabledHookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${activePath}"`);
execSync(`chmod +x "${disabledPath}"`);
await rig.setup(
'should respect disabled hooks across multiple operations',
{
settings: {
tools: {
enableHooks: true,
},
hooks: {
enabled: true,
BeforeTool: [
{
hooks: [
{
type: 'command',
command: activePath,
command: `node "${activePath}"`,
timeout: 5000,
},
{
type: 'command',
command: disabledPath,
command: `node "${disabledPath}"`,
timeout: 5000,
},
],
},
],
disabled: [disabledPath], // Disable the second hook
disabled: [`node "${disabledPath}"`], // Disable the second hook
},
},
},
@@ -1441,10 +1554,10 @@ echo '{"decision": "block", "systemMessage": "Disabled hook should not execute",
// Check hook telemetry
const hookLogs1 = rig.readHookLogs();
const activeHookLog1 = hookLogs1.find(
(log) => log.hookCall.hook_name === activePath,
(log) => log.hookCall.hook_name === `node "${activePath}"`,
);
const disabledHookLog1 = hookLogs1.find(
(log) => log.hookCall.hook_name === disabledPath,
(log) => log.hookCall.hook_name === `node "${disabledPath}"`,
);
expect(activeHookLog1).toBeDefined();
@@ -1465,9 +1578,171 @@ echo '{"decision": "block", "systemMessage": "Disabled hook should not execute",
// Verify disabled hook still hasn't executed
const hookLogs2 = rig.readHookLogs();
const disabledHookCalls = hookLogs2.filter(
(log) => log.hookCall.hook_name === disabledPath,
(log) => log.hookCall.hook_name === `node "${disabledPath}"`,
);
expect(disabledHookCalls.length).toBe(0);
});
});
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
await rig.setup(
'should override tool input parameters via BeforeTool hook',
);
// Create a hook script that overrides the tool input
const hookOutput = {
decision: 'allow',
hookSpecificOutput: {
hookEventName: 'BeforeTool',
tool_input: {
file_path: 'modified.txt',
content: 'modified content',
},
},
};
const hookScript = `process.stdout.write(JSON.stringify(${JSON.stringify(
hookOutput,
)}));`;
const scriptPath = join(rig.testDir!, 'input_override_hook.js');
writeFileSync(scriptPath, hookScript);
// Ensure path is properly escaped for command line usage on all platforms
// On Windows, backslashes in the command string need to be handled carefully
// Using forward slashes works well with Node.js on all platforms
const commandPath = scriptPath.replace(/\\/g, '/');
// 2. Full setup with settings and fake responses
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"
await rig.run({
args: 'Create a file called original.txt with content "original content"',
});
// 1. Verify that 'modified.txt' was created with 'modified content' (Override successful)
const modifiedContent = rig.readFile('modified.txt');
expect(modifiedContent).toBe('modified content');
// 2. Verify that 'original.txt' was NOT created (Override replaced original)
let originalExists = false;
try {
rig.readFile('original.txt');
originalExists = true;
} catch {
originalExists = false;
}
expect(originalExists).toBe(false);
// 3. Verify hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
const hookLogs = rig.readHookLogs();
expect(hookLogs.length).toBe(1);
expect(hookLogs[0].hookCall.hook_name).toContain(
'input_override_hook.js',
);
// 4. Verify that the agent didn't try to work-around the hook input change
const toolLogs = rig.readToolLogs();
expect(toolLogs.length).toBe(1);
expect(toolLogs[0].toolRequest.name).toBe('write_file');
expect(JSON.parse(toolLogs[0].toolRequest.args).file_path).toBe(
'modified.txt',
);
});
});
describe('BeforeTool Hooks - Stop Execution', () => {
it('should stop agent execution via BeforeTool hook', async () => {
// Create a hook script that stops execution
const hookOutput = {
continue: false,
reason: 'Emergency Stop triggered by hook',
hookSpecificOutput: {
hookEventName: 'BeforeTool',
},
};
const hookScript = `console.log(JSON.stringify(${JSON.stringify(
hookOutput,
)}));`;
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, '/');
await rig.setup('should stop agent execution via BeforeTool hook', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-tool-stop.responses',
),
settings: {
hooks: {
enabled: true,
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: `node "${commandPath}"`,
timeout: 5000,
},
],
},
],
},
},
});
const result = await rig.run({
args: 'Use write_file to create test.txt',
});
// The hook should have stopped execution message (returned from tool)
expect(result).toContain(
'Agent execution stopped: Emergency Stop triggered by hook',
);
// Tool should NOT be called successfully (it was blocked/stopped)
const toolLogs = rig.readToolLogs();
const writeFileCalls = toolLogs.filter(
(t) =>
t.toolRequest.name === 'write_file' && t.toolRequest.success === true,
);
expect(writeFileCalls).toHaveLength(0);
});
});
});
@@ -200,7 +200,7 @@ describe('mcp server with cyclic tool schema is detected', () => {
const run = await rig.runInteractive();
await run.type('/mcp list');
await run.sendKeys('\r');
await run.type('\r');
await run.expectText('tool_with_cyclic_schema');
});
+4
View File
@@ -27,6 +27,10 @@ class MockConfig {
getDebugMode() {
return true;
}
getFileFilteringRespectGeminiIgnore() {
return true;
}
}
describe('ripgrep-real-direct', () => {
+12 -2
View File
@@ -209,6 +209,12 @@ export class InteractiveRun {
async type(text: string) {
let typedSoFar = '';
for (const char of text) {
if (char === '\r') {
// wait >30ms before `enter` to avoid fast return conversion
// from bufferFastReturn() in KeypressContent.tsx
await new Promise((resolve) => setTimeout(resolve, 50));
}
this.ptyProcess.write(char);
typedSoFar += char;
@@ -285,7 +291,9 @@ export class TestRig {
) {
this.testName = testName;
const sanitizedName = sanitizeTestName(testName);
this.testDir = join(env['INTEGRATION_TEST_FILE_DIR']!, sanitizedName);
const testFileDir =
env['INTEGRATION_TEST_FILE_DIR'] || join(os.tmpdir(), 'gemini-cli-tests');
this.testDir = join(testFileDir, sanitizedName);
mkdirSync(this.testDir, { recursive: true });
if (options.fakeResponsesPath) {
this.fakeResponsesPath = join(this.testDir, 'fake-responses.json');
@@ -323,7 +331,9 @@ export class TestRig {
ui: {
useAlternateBuffer: true,
},
model: DEFAULT_GEMINI_MODEL,
model: {
name: DEFAULT_GEMINI_MODEL,
},
sandbox:
env['GEMINI_SANDBOX'] !== 'false' ? env['GEMINI_SANDBOX'] : false,
// Don't show the IDE connection dialog when running from VsCode
+871 -58
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.21.0-nightly.20251213.977248e09",
"version": "0.24.0",
"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.21.0-nightly.20251213.977248e09"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.24.0"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -52,7 +52,7 @@
"lint:all": "node scripts/lint.js",
"format": "prettier --experimental-cli --write .",
"typecheck": "npm run typecheck --workspaces --if-present",
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
"preflight": "npm run clean && npm ci && npm run format && npm run build && npm run lint:ci && npm run typecheck && npm run test:ci",
"prepare": "husky && npm run bundle",
"prepare:package": "node scripts/prepare-package.js",
"release:version": "node scripts/version.js",
@@ -62,7 +62,7 @@
"pre-commit": "node scripts/pre-commit.js"
},
"overrides": {
"ink": "npm:@jrichman/ink@6.4.6",
"ink": "npm:@jrichman/ink@6.4.7",
"wrap-ansi": "9.0.2",
"cliui": {
"wrap-ansi": "7.0.0"
@@ -89,12 +89,13 @@
"@vitest/coverage-v8": "^3.1.1",
"@vitest/eslint-plugin": "^1.3.4",
"cross-env": "^7.0.3",
"depcheck": "^1.4.7",
"esbuild": "^0.25.0",
"esbuild-plugin-wasm": "^1.1.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-headers": "^1.3.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-license-header": "^0.8.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"glob": "^12.0.0",
@@ -113,13 +114,14 @@
"react-devtools-core": "^6.1.2",
"semver": "^7.7.2",
"strip-ansi": "^7.1.2",
"ts-prune": "^0.10.3",
"tsx": "^4.20.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.2.4",
"yargs": "^17.7.2"
},
"dependencies": {
"ink": "npm:@jrichman/ink@6.4.6",
"ink": "npm:@jrichman/ink@6.4.7",
"latest-version": "^9.0.0",
"simple-git": "^3.28.0"
},
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.21.0-nightly.20251213.977248e09",
"version": "0.24.0",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -25,13 +25,13 @@
"dist"
],
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@a2a-js/sdk": "^0.3.7",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"tar": "^7.5.2",
"uuid": "^11.1.0",
"uuid": "^13.0.0",
"winston": "^3.17.0"
},
"devDependencies": {
+1 -1
View File
@@ -123,7 +123,7 @@ export class Task {
// process. This is not scoped to the individual task but reflects the global connection
// state managed within the @gemini-cli/core module.
async getMetadata(): Promise<TaskMetadata> {
const toolRegistry = await this.config.getToolRegistry();
const toolRegistry = this.config.getToolRegistry();
const mcpServers = this.config.getMcpClientManager()?.getMcpServers() || {};
const serverStatuses = getAllMCPServerStatuses();
const servers = Object.keys(mcpServers).map((serverName) => ({
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { debugLogger } from '@google/gemini-cli-core';
import { ExtensionsCommand } from './extensions.js';
import { InitCommand } from './init.js';
import { RestoreCommand } from './restore.js';
@@ -20,7 +21,7 @@ class CommandRegistry {
register(command: Command) {
if (this.commands.has(command.name)) {
console.warn(`Command ${command.name} already registered. Skipping.`);
debugLogger.warn(`Command ${command.name} already registered. Skipping.`);
return;
}
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { loadSettings, USER_SETTINGS_PATH } from './settings.js';
import { debugLogger } from '@google/gemini-cli-core';
const mocks = vi.hoisted(() => {
const suffix = Math.random().toString(36).slice(2);
@@ -75,7 +76,7 @@ describe('loadSettings', () => {
fs.rmSync(mockWorkspaceDir, { recursive: true, force: true });
}
} catch (e) {
console.error('Failed to cleanup temp dirs', e);
debugLogger.error('Failed to cleanup temp dirs', e);
}
vi.restoreAllMocks();
});
+2 -1
View File
@@ -35,7 +35,8 @@ import {
createStreamMessageRequest,
createMockConfig,
} from '../utils/testing_utils.js';
import { MockTool } from '@google/gemini-cli-core';
// Import MockTool from specific path to avoid vitest dependency in main core bundle
import { MockTool } from '@google/gemini-cli-core/src/test-utils/mock-tool.js';
import type { Command, CommandContext } from '../commands/types.js';
const mockToolConfirmationFn = async () =>
+2 -2
View File
@@ -25,7 +25,7 @@ import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
import { loadSettings } from '../config/settings.js';
import { loadExtensions } from '../config/extension.js';
import { commandRegistry } from '../commands/command-registry.js';
import { SimpleExtensionLoader } from '@google/gemini-cli-core';
import { debugLogger, SimpleExtensionLoader } from '@google/gemini-cli-core';
import type { Command, CommandArgument } from '../commands/types.js';
import { GitService } from '@google/gemini-cli-core';
@@ -239,7 +239,7 @@ export async function createApp() {
): CommandResponse | undefined => {
const commandName = command.name;
if (visited.includes(commandName)) {
console.warn(
debugLogger.warn(
`Command ${commandName} already inserted in the response, skipping`,
);
return undefined;
@@ -59,7 +59,6 @@ export function createMockConfig(
getEmbeddingModel: vi.fn().mockReturnValue('text-embedding-004'),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getUserTier: vi.fn(),
getEnableMessageBusIntegration: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn(),
getPolicyEngine: vi.fn(),
getEnableExtensionReloading: vi.fn().mockReturnValue(false),
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.21.0-nightly.20251213.977248e09",
"version": "0.24.0",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -26,9 +26,10 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.21.0-nightly.20251213.977248e09"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.24.0"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.11.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -44,7 +45,7 @@
"fzf": "^0.5.2",
"glob": "^12.0.0",
"highlight.js": "^11.11.1",
"ink": "npm:@jrichman/ink@6.4.6",
"ink": "npm:@jrichman/ink@6.4.7",
"ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0",
"latest-version": "^9.0.0",
@@ -228,7 +228,9 @@ describe('extensions disable command', () => {
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => void)(argv);
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
argv,
);
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
@@ -205,7 +205,9 @@ describe('extensions enable command', () => {
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => void)(argv);
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
argv,
);
expect(
mockExtensionManager.prototype.enableExtension,
@@ -4,11 +4,23 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, type MockInstance, type Mock } from 'vitest';
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type MockInstance,
type Mock,
} from 'vitest';
import { handleInstall, installCommand } from './install.js';
import yargs from 'yargs';
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
import type { ExtensionManager } from '../../config/extension-manager.js';
import type {
ExtensionManager,
inferInstallMetadata,
} from '../../config/extension-manager.js';
import type { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import type * as fs from 'node:fs/promises';
import type { Stats } from 'node:fs';
@@ -20,12 +32,15 @@ const mockRequestConsentNonInteractive: Mock<
typeof requestConsentNonInteractive
> = vi.hoisted(() => vi.fn());
const mockStat: Mock<typeof fs.stat> = vi.hoisted(() => vi.fn());
const mockInferInstallMetadata: Mock<typeof inferInstallMetadata> = vi.hoisted(
() => vi.fn(),
);
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: mockRequestConsentNonInteractive,
}));
vi.mock('../../config/extension-manager.ts', async (importOriginal) => {
vi.mock('../../config/extension-manager.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../config/extension-manager.js')>();
return {
@@ -34,6 +49,7 @@ vi.mock('../../config/extension-manager.ts', async (importOriginal) => {
installOrUpdateExtension: mockInstallOrUpdateExtension,
loadExtensions: vi.fn(),
})),
inferInstallMetadata: mockInferInstallMetadata,
};
});
@@ -72,12 +88,31 @@ describe('handleInstall', () => {
processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
mockInferInstallMetadata.mockImplementation(async (source, args) => {
if (
source.startsWith('http://') ||
source.startsWith('https://') ||
source.startsWith('git@') ||
source.startsWith('sso://')
) {
return {
source,
type: 'git',
ref: args?.ref,
autoUpdate: args?.autoUpdate,
allowPreRelease: args?.allowPreRelease,
};
}
return { source, type: 'local' };
});
});
afterEach(() => {
mockInstallOrUpdateExtension.mockClear();
mockRequestConsentNonInteractive.mockClear();
mockStat.mockClear();
mockInferInstallMetadata.mockClear();
vi.clearAllMocks();
});
@@ -124,7 +159,9 @@ describe('handleInstall', () => {
});
it('throws an error from an unknown source', async () => {
mockStat.mockRejectedValue(new Error('ENOENT: no such file or directory'));
mockInferInstallMetadata.mockRejectedValue(
new Error('Install source not found.'),
);
await handleInstall({
source: 'test://google.com',
});
+10 -36
View File
@@ -5,17 +5,16 @@
*/
import type { CommandModule } from 'yargs';
import {
debugLogger,
type ExtensionInstallMetadata,
} from '@google/gemini-cli-core';
import { debugLogger } from '@google/gemini-cli-core';
import { getErrorMessage } from '../../utils/errors.js';
import { stat } from 'node:fs/promises';
import {
INSTALL_WARNING_MESSAGE,
requestConsentNonInteractive,
} from '../../config/extensions/consent.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import {
ExtensionManager,
inferInstallMetadata,
} from '../../config/extension-manager.js';
import { loadSettings } from '../../config/settings.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
import { exitCli } from '../utils.js';
@@ -30,37 +29,12 @@ interface InstallArgs {
export async function handleInstall(args: InstallArgs) {
try {
let installMetadata: ExtensionInstallMetadata;
const { source } = args;
if (
source.startsWith('http://') ||
source.startsWith('https://') ||
source.startsWith('git@') ||
source.startsWith('sso://')
) {
installMetadata = {
source,
type: 'git',
ref: args.ref,
autoUpdate: args.autoUpdate,
allowPreRelease: args.allowPreRelease,
};
} else {
if (args.ref || args.autoUpdate) {
throw new Error(
'--ref and --auto-update are not applicable for local extensions.',
);
}
try {
await stat(source);
installMetadata = {
source,
type: 'local',
};
} catch {
throw new Error('Install source not found.');
}
}
const installMetadata = await inferInstallMetadata(source, {
ref: args.ref,
autoUpdate: args.autoUpdate,
allowPreRelease: args.allowPreRelease,
});
const requestConsent = args.consent
? () => Promise.resolve(true)
@@ -180,7 +180,9 @@ describe('extensions link command', () => {
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => void)(argv);
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
argv,
);
expect(
mockExtensionManager.prototype.installOrUpdateExtension,
@@ -0,0 +1,231 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { settingsCommand } from './settings.js';
import yargs from 'yargs';
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
import type { getExtensionAndManager } from './utils.js';
import type {
updateSetting,
getScopedEnvContents,
} from '../../config/extensions/extensionSettings.js';
import {
promptForSetting,
ExtensionSettingScope,
} from '../../config/extensions/extensionSettings.js';
import type { exitCli } from '../utils.js';
import type { ExtensionManager } from '../../config/extension-manager.js';
const mockGetExtensionAndManager: Mock<typeof getExtensionAndManager> =
vi.hoisted(() => vi.fn());
const mockUpdateSetting: Mock<typeof updateSetting> = vi.hoisted(() => vi.fn());
const mockGetScopedEnvContents: Mock<typeof getScopedEnvContents> = vi.hoisted(
() => vi.fn(),
);
const mockExitCli: Mock<typeof exitCli> = vi.hoisted(() => vi.fn());
vi.mock('./utils.js', () => ({
getExtensionAndManager: mockGetExtensionAndManager,
}));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
updateSetting: mockUpdateSetting,
promptForSetting: vi.fn(),
ExtensionSettingScope: {
USER: 'user',
WORKSPACE: 'workspace',
},
getScopedEnvContents: mockGetScopedEnvContents,
}));
vi.mock('@google/gemini-cli-core', () => ({
debugLogger: {
log: vi.fn(),
error: vi.fn(),
},
}));
vi.mock('../utils.js', () => ({
exitCli: mockExitCli,
}));
describe('settings command', () => {
let debugLogSpy: Mock;
let debugErrorSpy: Mock;
beforeEach(() => {
debugLogSpy = debugLogger.log as Mock;
debugErrorSpy = debugLogger.error as Mock;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('set command', () => {
it('should log error and exit if extension is not found', async () => {
mockGetExtensionAndManager.mockResolvedValue({
extension: null,
extensionManager: null,
});
await yargs([])
.command(settingsCommand)
.parseAsync('settings set foo bar');
expect(mockExitCli).toHaveBeenCalled();
});
it('should log error and exit if extension config is not found', async () => {
const mockExtensionManager = {
loadExtensionConfig: vi.fn().mockResolvedValue(null),
} as unknown as ExtensionManager;
mockGetExtensionAndManager.mockResolvedValue({
extension: { path: '/path/to/ext' } as unknown as GeminiCLIExtension,
extensionManager: mockExtensionManager,
});
await yargs([])
.command(settingsCommand)
.parseAsync('settings set foo bar');
expect(debugErrorSpy).toHaveBeenCalledWith(
'Could not find configuration for extension "foo".',
);
expect(mockExitCli).toHaveBeenCalled();
});
it('should call updateSetting with correct arguments', async () => {
const mockExtensionManager = {
loadExtensionConfig: vi.fn().mockResolvedValue({}),
} as unknown as ExtensionManager;
const extension = { path: '/path/to/ext', id: 'ext-id' };
mockGetExtensionAndManager.mockResolvedValue({
extension: extension as unknown as GeminiCLIExtension,
extensionManager: mockExtensionManager,
});
await yargs([])
.command(settingsCommand)
.parseAsync('settings set foo bar --scope workspace');
expect(mockUpdateSetting).toHaveBeenCalledWith(
{},
'ext-id',
'bar',
promptForSetting,
ExtensionSettingScope.WORKSPACE,
);
expect(mockExitCli).toHaveBeenCalled();
});
});
describe('list command', () => {
it('should log error and exit if extension is not found', async () => {
mockGetExtensionAndManager.mockResolvedValue({
extension: null,
extensionManager: null,
});
await yargs([]).command(settingsCommand).parseAsync('settings list foo');
expect(mockExitCli).toHaveBeenCalled();
});
it('should log message and exit if extension has no settings', async () => {
const mockExtensionManager = {
loadExtensionConfig: vi.fn().mockResolvedValue({ settings: [] }),
} as unknown as ExtensionManager;
mockGetExtensionAndManager.mockResolvedValue({
extension: { path: '/path/to/ext' } as unknown as GeminiCLIExtension,
extensionManager: mockExtensionManager,
});
await yargs([]).command(settingsCommand).parseAsync('settings list foo');
expect(debugLogSpy).toHaveBeenCalledWith(
'Extension "foo" has no settings to configure.',
);
expect(mockExitCli).toHaveBeenCalled();
});
it('should list settings correctly', async () => {
const mockExtensionManager = {
loadExtensionConfig: vi.fn().mockResolvedValue({
settings: [
{
name: 'Setting 1',
envVar: 'SETTING_1',
description: 'Desc 1',
sensitive: false,
},
{
name: 'Setting 2',
envVar: 'SETTING_2',
description: 'Desc 2',
sensitive: true,
},
{
name: 'Setting 3',
envVar: 'SETTING_3',
description: 'Desc 3',
sensitive: false,
},
],
}),
} as unknown as ExtensionManager;
const extension = { path: '/path/to/ext', id: 'ext-id' };
mockGetExtensionAndManager.mockResolvedValue({
extension: extension as unknown as GeminiCLIExtension,
extensionManager: mockExtensionManager,
});
mockGetScopedEnvContents.mockImplementation((_config, _id, scope) => {
if (scope === ExtensionSettingScope.USER) {
return Promise.resolve({
SETTING_1: 'val1',
SETTING_2: 'val2',
});
}
if (scope === ExtensionSettingScope.WORKSPACE) {
return Promise.resolve({
SETTING_3: 'val3',
});
}
return Promise.resolve({});
});
await yargs([]).command(settingsCommand).parseAsync('settings list foo');
expect(debugLogSpy).toHaveBeenCalledWith('Settings for "foo":');
// Setting 1 (User)
expect(debugLogSpy).toHaveBeenCalledWith('\n- Setting 1 (SETTING_1)');
expect(debugLogSpy).toHaveBeenCalledWith(' Description: Desc 1');
expect(debugLogSpy).toHaveBeenCalledWith(' Value: val1 (user)');
// Setting 2 (Sensitive)
expect(debugLogSpy).toHaveBeenCalledWith('\n- Setting 2 (SETTING_2)');
expect(debugLogSpy).toHaveBeenCalledWith(' Description: Desc 2');
expect(debugLogSpy).toHaveBeenCalledWith(
' Value: [value stored in keychain] (user)',
);
// Setting 3 (Workspace)
expect(debugLogSpy).toHaveBeenCalledWith('\n- Setting 3 (SETTING_3)');
expect(debugLogSpy).toHaveBeenCalledWith(' Description: Desc 3');
expect(debugLogSpy).toHaveBeenCalledWith(' Value: val3 (workspace)');
expect(mockExitCli).toHaveBeenCalled();
});
});
});
@@ -47,15 +47,17 @@ const setCommand: CommandModule<object, SetArgs> = {
const { name, setting, scope } = args;
const { extension, extensionManager } = await getExtensionAndManager(name);
if (!extension || !extensionManager) {
await exitCli();
return;
}
const extensionConfig = extensionManager.loadExtensionConfig(
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (!extensionConfig) {
debugLogger.error(
`Could not find configuration for extension "${name}".`,
);
await exitCli();
return;
}
await updateSetting(
@@ -87,9 +89,10 @@ const listCommand: CommandModule<object, ListArgs> = {
const { name } = args;
const { extension, extensionManager } = await getExtensionAndManager(name);
if (!extension || !extensionManager) {
await exitCli();
return;
}
const extensionConfig = extensionManager.loadExtensionConfig(
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
@@ -98,6 +101,7 @@ const listCommand: CommandModule<object, ListArgs> = {
extensionConfig.settings.length === 0
) {
debugLogger.log(`Extension "${name}" has no settings to configure.`);
await exitCli();
return;
}
@@ -287,7 +287,9 @@ describe('extensions uninstall command', () => {
[key: string]: unknown;
}
const argv: TestArgv = { names: ['my-extension'], _: [], $0: '' };
await (command.handler as unknown as (args: TestArgv) => void)(argv);
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
argv,
);
expect(mockUninstallExtension).toHaveBeenCalledWith(
'my-extension',
@@ -5,7 +5,15 @@
*/
import * as fs from 'node:fs';
import { describe, it, expect, vi, type MockInstance } from 'vitest';
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type MockInstance,
} from 'vitest';
import { handleValidate, validateCommand } from './validate.js';
import yargs from 'yargs';
import { createExtension } from '../../test-utils/createExtension.js';
@@ -41,7 +41,7 @@ async function validateExtension(args: ValidateArgs) {
});
const absoluteInputPath = path.resolve(args.path);
const extensionConfig: ExtensionConfig =
extensionManager.loadExtensionConfig(absoluteInputPath);
await extensionManager.loadExtensionConfig(absoluteInputPath);
const warnings: string[] = [];
const errors: string[] = [];
@@ -512,7 +512,7 @@ describe('migrate command', () => {
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'Note: Set tools.enableHooks to true in your settings to enable the hook system.',
'Note: Set hooks.enabled to true in your settings to enable the hook system.',
);
});
});
+1 -1
View File
@@ -243,7 +243,7 @@ export async function handleMigrateFromClaude() {
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
);
debugLogger.log(
'Note: Set tools.enableHooks to true in your settings to enable the hook system.',
'Note: Set hooks.enabled to true in your settings to enable the hook system.',
);
} catch (error) {
debugLogger.error(`Error saving migrated hooks: ${getErrorMessage(error)}`);
+9 -1
View File
@@ -4,7 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, type Mock, type MockInstance } from 'vitest';
import {
describe,
it,
expect,
vi,
beforeEach,
type Mock,
type MockInstance,
} from 'vitest';
import yargs, { type Argv } from 'yargs';
import { addCommand } from './add.js';
import { loadSettings, SettingScope } from '../../config/settings.js';
+14 -1
View File
@@ -59,10 +59,23 @@ async function testMCPConnection(
version: '0.0.1',
});
const settings = loadSettings();
const sanitizationConfig = {
enableEnvironmentVariableRedaction: true,
allowedEnvironmentVariables: [],
blockedEnvironmentVariables:
settings.merged.advanced?.excludedEnvVars || [],
};
let transport;
try {
// Use the same transport creation logic as core
transport = await createTransport(serverName, config, false);
transport = await createTransport(
serverName,
config,
false,
sanitizationConfig,
);
} catch (_error) {
await client.close();
return MCPServerStatus.DISCONNECTED;
+56
View File
@@ -0,0 +1,56 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { skillsCommand } from './skills.js';
vi.mock('./skills/list.js', () => ({ listCommand: { command: 'list' } }));
vi.mock('./skills/enable.js', () => ({
enableCommand: { command: 'enable <name>' },
}));
vi.mock('./skills/disable.js', () => ({
disableCommand: { command: 'disable <name>' },
}));
vi.mock('../gemini.js', () => ({
initializeOutputListenersAndFlush: vi.fn(),
}));
describe('skillsCommand', () => {
it('should have correct command and aliases', () => {
expect(skillsCommand.command).toBe('skills <command>');
expect(skillsCommand.aliases).toEqual(['skill']);
expect(skillsCommand.describe).toBe('Manage agent skills.');
});
it('should register all subcommands in builder', () => {
const mockYargs = {
middleware: vi.fn().mockReturnThis(),
command: vi.fn().mockReturnThis(),
demandCommand: vi.fn().mockReturnThis(),
version: vi.fn().mockReturnThis(),
};
// @ts-expect-error - Mocking yargs
skillsCommand.builder(mockYargs);
expect(mockYargs.middleware).toHaveBeenCalled();
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'list' });
expect(mockYargs.command).toHaveBeenCalledWith({
command: 'enable <name>',
});
expect(mockYargs.command).toHaveBeenCalledWith({
command: 'disable <name>',
});
expect(mockYargs.demandCommand).toHaveBeenCalledWith(1, expect.any(String));
expect(mockYargs.version).toHaveBeenCalledWith(false);
});
it('should have a handler that does nothing', () => {
// @ts-expect-error - Handler doesn't take arguments in this case
expect(skillsCommand.handler()).toBeUndefined();
});
});
+29
View File
@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import { listCommand } from './skills/list.js';
import { enableCommand } from './skills/enable.js';
import { disableCommand } from './skills/disable.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
export const skillsCommand: CommandModule = {
command: 'skills <command>',
aliases: ['skill'],
describe: 'Manage agent skills.',
builder: (yargs) =>
yargs
.middleware(() => initializeOutputListenersAndFlush())
.command(listCommand)
.command(enableCommand)
.command(disableCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {
// This handler is not called when a subcommand is provided.
// Yargs will show the help menu.
},
};
@@ -0,0 +1,115 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { handleDisable, disableCommand } from './disable.js';
import {
loadSettings,
SettingScope,
type LoadedSettings,
type LoadableSettingScope,
} from '../../config/settings.js';
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger,
};
});
vi.mock('../../config/settings.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../config/settings.js')>();
return {
...actual,
loadSettings: vi.fn(),
};
});
vi.mock('../utils.js', () => ({
exitCli: vi.fn(),
}));
describe('skills disable command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleDisable', () => {
it('should disable an enabled skill in user scope', async () => {
const mockSettings = {
forScope: vi.fn().mockReturnValue({
settings: { skills: { disabled: [] } },
}),
setValue: vi.fn(),
};
mockLoadSettings.mockReturnValue(
mockSettings as unknown as LoadedSettings,
);
await handleDisable({
name: 'skill1',
scope: SettingScope.User as LoadableSettingScope,
});
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'skills.disabled',
['skill1'],
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Skill "skill1" successfully disabled in scope "User".',
);
});
it('should log a message if the skill is already disabled', async () => {
const mockSettings = {
forScope: vi.fn().mockReturnValue({
settings: { skills: { disabled: ['skill1'] } },
}),
setValue: vi.fn(),
};
mockLoadSettings.mockReturnValue(
mockSettings as unknown as LoadedSettings,
);
await handleDisable({
name: 'skill1',
scope: SettingScope.User as LoadableSettingScope,
});
expect(mockSettings.setValue).not.toHaveBeenCalled();
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Skill "skill1" is already disabled in scope "User".',
);
});
});
describe('disableCommand', () => {
it('should have correct command and describe', () => {
expect(disableCommand.command).toBe('disable <name>');
expect(disableCommand.describe).toBe('Disables an agent skill.');
});
});
});
@@ -0,0 +1,65 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import {
loadSettings,
SettingScope,
type LoadableSettingScope,
} from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
interface DisableArgs {
name: string;
scope: LoadableSettingScope;
}
export async function handleDisable(args: DisableArgs) {
const { name, scope } = args;
const workspaceDir = process.cwd();
const settings = loadSettings(workspaceDir);
const currentDisabled =
settings.forScope(scope).settings.skills?.disabled || [];
if (currentDisabled.includes(name)) {
debugLogger.log(`Skill "${name}" is already disabled in scope "${scope}".`);
return;
}
const newDisabled = [...currentDisabled, name];
settings.setValue(scope, 'skills.disabled', newDisabled);
debugLogger.log(`Skill "${name}" successfully disabled in scope "${scope}".`);
}
export const disableCommand: CommandModule = {
command: 'disable <name>',
describe: 'Disables an agent skill.',
builder: (yargs) =>
yargs
.positional('name', {
describe: 'The name of the skill to disable.',
type: 'string',
demandOption: true,
})
.option('scope', {
alias: 's',
describe: 'The scope to disable the skill in (user or project).',
type: 'string',
default: 'user',
choices: ['user', 'project'],
}),
handler: async (argv) => {
const scope: LoadableSettingScope =
argv['scope'] === 'project' ? SettingScope.Workspace : SettingScope.User;
await handleDisable({
name: argv['name'] as string,
scope,
});
await exitCli();
},
};
@@ -0,0 +1,115 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { handleEnable, enableCommand } from './enable.js';
import {
loadSettings,
SettingScope,
type LoadedSettings,
type LoadableSettingScope,
} from '../../config/settings.js';
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger,
};
});
vi.mock('../../config/settings.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../config/settings.js')>();
return {
...actual,
loadSettings: vi.fn(),
};
});
vi.mock('../utils.js', () => ({
exitCli: vi.fn(),
}));
describe('skills enable command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleEnable', () => {
it('should enable a disabled skill in user scope', async () => {
const mockSettings = {
forScope: vi.fn().mockReturnValue({
settings: { skills: { disabled: ['skill1'] } },
}),
setValue: vi.fn(),
};
mockLoadSettings.mockReturnValue(
mockSettings as unknown as LoadedSettings,
);
await handleEnable({
name: 'skill1',
scope: SettingScope.User as LoadableSettingScope,
});
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'skills.disabled',
[],
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Skill "skill1" successfully enabled in scope "User".',
);
});
it('should log a message if the skill is already enabled', async () => {
const mockSettings = {
forScope: vi.fn().mockReturnValue({
settings: { skills: { disabled: [] } },
}),
setValue: vi.fn(),
};
mockLoadSettings.mockReturnValue(
mockSettings as unknown as LoadedSettings,
);
await handleEnable({
name: 'skill1',
scope: SettingScope.User as LoadableSettingScope,
});
expect(mockSettings.setValue).not.toHaveBeenCalled();
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Skill "skill1" is already enabled in scope "User".',
);
});
});
describe('enableCommand', () => {
it('should have correct command and describe', () => {
expect(enableCommand.command).toBe('enable <name>');
expect(enableCommand.describe).toBe('Enables an agent skill.');
});
});
});
@@ -0,0 +1,65 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import {
loadSettings,
SettingScope,
type LoadableSettingScope,
} from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
interface EnableArgs {
name: string;
scope: LoadableSettingScope;
}
export async function handleEnable(args: EnableArgs) {
const { name, scope } = args;
const workspaceDir = process.cwd();
const settings = loadSettings(workspaceDir);
const currentDisabled =
settings.forScope(scope).settings.skills?.disabled || [];
const newDisabled = currentDisabled.filter((d) => d !== name);
if (currentDisabled.length === newDisabled.length) {
debugLogger.log(`Skill "${name}" is already enabled in scope "${scope}".`);
return;
}
settings.setValue(scope, 'skills.disabled', newDisabled);
debugLogger.log(`Skill "${name}" successfully enabled in scope "${scope}".`);
}
export const enableCommand: CommandModule = {
command: 'enable <name>',
describe: 'Enables an agent skill.',
builder: (yargs) =>
yargs
.positional('name', {
describe: 'The name of the skill to enable.',
type: 'string',
demandOption: true,
})
.option('scope', {
alias: 's',
describe: 'The scope to enable the skill in (user or project).',
type: 'string',
default: 'user',
choices: ['user', 'project'],
}),
handler: async (argv) => {
const scope: LoadableSettingScope =
argv['scope'] === 'project' ? SettingScope.Workspace : SettingScope.User;
await handleEnable({
name: argv['name'] as string,
scope,
});
await exitCli();
},
};
@@ -0,0 +1,138 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { handleList, listCommand } from './list.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { loadCliConfig } from '../../config/config.js';
import type { Config } from '@google/gemini-cli-core';
import chalk from 'chalk';
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/settings.js');
vi.mock('../../config/config.js');
vi.mock('../utils.js', () => ({
exitCli: vi.fn(),
}));
describe('skills list command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockLoadCliConfig = vi.mocked(loadCliConfig);
beforeEach(async () => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleList', () => {
it('should log a message if no skills are discovered', async () => {
const mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue([]),
}),
};
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'No skills discovered.',
);
});
it('should list all discovered skills', async () => {
const skills = [
{
name: 'skill1',
description: 'desc1',
disabled: false,
location: '/path/to/skill1',
},
{
name: 'skill2',
description: 'desc2',
disabled: true,
location: '/path/to/skill2',
},
];
const mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue(skills),
}),
};
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
chalk.bold('Discovered Agent Skills:'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('skill1'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining(chalk.green('[Enabled]')),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('skill2'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining(chalk.red('[Disabled]')),
);
});
it('should throw an error when listing fails', async () => {
mockLoadCliConfig.mockRejectedValue(new Error('List failed'));
await expect(handleList()).rejects.toThrow('List failed');
});
});
describe('listCommand', () => {
const command = listCommand;
it('should have correct command and describe', () => {
expect(command.command).toBe('list');
expect(command.describe).toBe('Lists discovered agent skills.');
});
});
});
+61
View File
@@ -0,0 +1,61 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import { debugLogger } from '@google/gemini-cli-core';
import { loadSettings } from '../../config/settings.js';
import { loadCliConfig, type CliArgs } from '../../config/config.js';
import { exitCli } from '../utils.js';
import chalk from 'chalk';
export async function handleList() {
const workspaceDir = process.cwd();
const settings = loadSettings(workspaceDir);
const config = await loadCliConfig(
settings.merged,
'skills-list-session',
{
debug: false,
} as Partial<CliArgs> as CliArgs,
{ cwd: workspaceDir },
);
// Initialize to trigger extension loading and skill discovery
await config.initialize();
const skillManager = config.getSkillManager();
const skills = skillManager.getAllSkills();
if (skills.length === 0) {
debugLogger.log('No skills discovered.');
return;
}
debugLogger.log(chalk.bold('Discovered Agent Skills:'));
debugLogger.log('');
for (const skill of skills) {
const status = skill.disabled
? chalk.red('[Disabled]')
: chalk.green('[Enabled]');
debugLogger.log(`${chalk.bold(skill.name)} ${status}`);
debugLogger.log(` Description: ${skill.description}`);
debugLogger.log(` Location: ${skill.location}`);
debugLogger.log('');
}
}
export const listCommand: CommandModule = {
command: 'list',
describe: 'Lists discovered agent skills.',
builder: (yargs) => yargs,
handler: async () => {
await handleList();
await exitCli();
},
};
+232 -14
View File
@@ -16,6 +16,7 @@ import {
WEB_FETCH_TOOL_NAME,
type ExtensionLoader,
debugLogger,
ApprovalMode,
} from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import type { Settings } from './settings.js';
@@ -25,13 +26,11 @@ import { ExtensionManager } from './extension-manager.js';
import { RESUME_LATEST } from '../utils/sessionUtils.js';
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi
.fn()
.mockReturnValue({ isTrusted: true, source: 'file' }), // Default to trusted
isWorkspaceTrusted: vi.fn(() => ({ isTrusted: true, source: 'file' })), // Default to trusted
}));
vi.mock('./sandboxConfig.js', () => ({
loadSandboxConfig: vi.fn().mockResolvedValue(undefined),
loadSandboxConfig: vi.fn(async () => undefined),
}));
vi.mock('fs', async (importOriginal) => {
@@ -125,6 +124,12 @@ vi.mock('@google/gemini-cli-core', async () => {
respectGitIgnore: true,
respectGeminiIgnore: true,
},
createPolicyEngineConfig: vi.fn(async () => ({
rules: [],
checkers: [],
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
})),
};
});
@@ -631,14 +636,7 @@ describe('loadCliConfig', () => {
expect(config.getFileFilteringRespectGeminiIgnore()).toBe(
DEFAULT_FILE_FILTERING_OPTIONS.respectGeminiIgnore,
);
});
it('should default enableMessageBusIntegration to true when unconfigured', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config['enableMessageBusIntegration']).toBe(true);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
});
@@ -1071,7 +1069,7 @@ describe('Approval mode tool exclusion logic', () => {
};
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Cannot start in YOLO mode when it is disabled by settings',
'Cannot start in YOLO mode since it is disabled by your admin',
);
});
@@ -1283,7 +1281,7 @@ describe('loadCliConfig model selection', () => {
argv,
);
expect(config.getModel()).toBe('auto');
expect(config.getModel()).toBe('auto-gemini-2.5');
});
it('always prefers model from argv', async () => {
@@ -2325,3 +2323,223 @@ describe('Telemetry configuration via environment variables', () => {
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
});
});
describe('PolicyEngine nonInteractive wiring', () => {
const originalIsTTY = process.stdin.isTTY;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
});
afterEach(() => {
process.stdin.isTTY = originalIsTTY;
vi.restoreAllMocks();
});
it('should set nonInteractive to true in one-shot mode', async () => {
process.stdin.isTTY = true;
process.argv = ['node', 'script.js', 'echo hello']; // Positional query makes it one-shot
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig({}, 'test-session', argv);
expect(config.isInteractive()).toBe(false);
expect(
(config.getPolicyEngine() as unknown as { nonInteractive: boolean })
.nonInteractive,
).toBe(true);
});
it('should set nonInteractive to false in interactive mode', async () => {
process.stdin.isTTY = true;
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig({}, 'test-session', argv);
expect(config.isInteractive()).toBe(true);
expect(
(config.getPolicyEngine() as unknown as { nonInteractive: boolean })
.nonInteractive,
).toBe(false);
});
});
describe('Policy Engine Integration in loadCliConfig', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('should pass merged allowed tools from CLI and settings to createPolicyEngineConfig', async () => {
process.argv = ['node', 'script.js', '--allowed-tools', 'cli-tool'];
const settings: Settings = { tools: { allowed: ['settings-tool'] } };
const argv = await parseArguments({} as Settings);
await loadCliConfig(settings, 'test-session', argv);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
allowed: expect.arrayContaining(['cli-tool']),
}),
}),
expect.anything(),
);
});
it('should pass merged exclude tools from CLI logic and settings to createPolicyEngineConfig', async () => {
process.stdin.isTTY = false; // Non-interactive to trigger default excludes
process.argv = ['node', 'script.js', '-p', 'test'];
const settings: Settings = { tools: { exclude: ['settings-exclude'] } };
const argv = await parseArguments({} as Settings);
await loadCliConfig(settings, 'test-session', argv);
// In non-interactive mode, ShellTool, etc. are excluded
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
exclude: expect.arrayContaining([SHELL_TOOL_NAME]),
}),
}),
expect.anything(),
);
});
});
describe('loadCliConfig secureModeEnabled', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: undefined,
});
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('should throw an error if YOLO mode is attempted when secureModeEnabled is true', async () => {
process.argv = ['node', 'script.js', '--yolo'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
admin: {
secureModeEnabled: true,
},
};
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Cannot start in YOLO mode since it is disabled by your admin',
);
});
it('should throw an error if approval-mode=yolo is attempted when secureModeEnabled is true', async () => {
process.argv = ['node', 'script.js', '--approval-mode=yolo'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
admin: {
secureModeEnabled: true,
},
};
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Cannot start in YOLO mode since it is disabled by your admin',
);
});
it('should set disableYoloMode to true when secureModeEnabled is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
admin: {
secureModeEnabled: true,
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.isYoloModeDisabled()).toBe(true);
});
});
describe('loadCliConfig mcpEnabled', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
const mcpSettings = {
mcp: {
serverCommand: 'mcp-server',
allowed: ['serverA'],
excluded: ['serverB'],
},
mcpServers: { serverA: { url: 'http://a' } },
};
it('should enable MCP by default', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { ...mcpSettings };
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpEnabled()).toBe(true);
expect(config.getMcpServerCommand()).toBe('mcp-server');
expect(config.getMcpServers()).toEqual({ serverA: { url: 'http://a' } });
expect(config.getAllowedMcpServers()).toEqual(['serverA']);
expect(config.getBlockedMcpServers()).toEqual(['serverB']);
});
it('should disable MCP when mcpEnabled is false', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...mcpSettings,
admin: {
mcp: {
enabled: false,
},
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpEnabled()).toBe(false);
expect(config.getMcpServerCommand()).toBeUndefined();
expect(config.getMcpServers()).toEqual({});
expect(config.getAllowedMcpServers()).toEqual([]);
expect(config.getBlockedMcpServers()).toEqual([]);
});
it('should enable MCP when mcpEnabled is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...mcpSettings,
admin: {
mcp: {
enabled: true,
},
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpEnabled()).toBe(true);
expect(config.getMcpServerCommand()).toBe('mcp-server');
expect(config.getMcpServers()).toEqual({ serverA: { url: 'http://a' } });
expect(config.getAllowedMcpServers()).toEqual(['serverA']);
expect(config.getBlockedMcpServers()).toEqual(['serverB']);
});
});
+114 -41
View File
@@ -8,8 +8,8 @@ import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import process from 'node:process';
import { mcpCommand } from '../commands/mcp.js';
import type { OutputFormat } from '@google/gemini-cli-core';
import { extensionsCommand } from '../commands/extensions.js';
import { skillsCommand } from '../commands/skills.js';
import { hooksCommand } from '../commands/hooks.js';
import {
Config,
@@ -32,8 +32,13 @@ import {
loadServerHierarchicalMemory,
WEB_FETCH_TOOL_NAME,
getVersion,
PREVIEW_GEMINI_MODEL_AUTO,
type HookDefinition,
type HookEventName,
type OutputFormat,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
import { saveModelChange, loadSettings } from './settings.js';
import { loadSandboxConfig } from './sandboxConfig.js';
import { resolvePath } from '../utils/resolvePath.js';
@@ -48,6 +53,7 @@ import { requestConsentNonInteractive } from './extensions/consent.js';
import { promptForSetting } from './extensions/extensionSettings.js';
import type { EventEmitter } from 'node:stream';
import { runExitCleanup } from '../utils/cleanup.js';
import { getEnableHooks } from './settingsSchema.js';
export interface CliArgs {
query: string | undefined;
@@ -69,7 +75,6 @@ export interface CliArgs {
deleteSession: string | undefined;
includeDirectories: string[] | undefined;
screenReader: boolean | undefined;
useSmartEdit: boolean | undefined;
useWriteTodos: boolean | undefined;
outputFormat: string | undefined;
fakeResponses: string | undefined;
@@ -282,8 +287,12 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
yargsInstance.command(extensionsCommand);
}
if (settings?.experimental?.skills ?? false) {
yargsInstance.command(skillsCommand);
}
// Register hooks command if hooks are enabled
if (settings?.tools?.enableHooks) {
if (getEnableHooks(settings)) {
yargsInstance.command(hooksCommand);
}
@@ -378,14 +387,24 @@ export function isDebugMode(argv: CliArgs): boolean {
);
}
export interface LoadCliConfigOptions {
cwd?: string;
projectHooks?: { [K in HookEventName]?: HookDefinition[] } & {
disabled?: string[];
};
}
export async function loadCliConfig(
settings: Settings,
sessionId: string,
argv: CliArgs,
cwd: string = process.cwd(),
options: LoadCliConfigOptions = {},
): Promise<Config> {
const { cwd = process.cwd(), projectHooks } = options;
const debugMode = isDebugMode(argv);
const loadedSettings = loadSettings(cwd);
if (argv.sandbox) {
process.env['GEMINI_SANDBOX'] = 'true';
}
@@ -395,7 +414,7 @@ export async function loadCliConfig(
const ideMode = settings.ide?.enabled ?? false;
const folderTrust = settings.security?.folderTrust?.enabled ?? false;
const trustedFolder = isWorkspaceTrusted(settings)?.isTrusted ?? true;
const trustedFolder = isWorkspaceTrusted(settings)?.isTrusted ?? false;
// Set the context filename in the server's memoryTool module BEFORE loading memory
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
@@ -434,9 +453,15 @@ export async function loadCliConfig(
});
await extensionManager.loadExtensions();
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
const { memoryContent, fileCount, filePaths } =
await loadServerHierarchicalMemory(
const experimentalJitContext = settings.experimental?.jitContext ?? false;
let memoryContent = '';
let fileCount = 0;
let filePaths: string[] = [];
if (!experimentalJitContext) {
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
const result = await loadServerHierarchicalMemory(
cwd,
[],
debugMode,
@@ -447,6 +472,10 @@ export async function loadCliConfig(
memoryFileFiltering,
settings.context?.discoveryMaxDirs,
);
memoryContent = result.memoryContent;
fileCount = result.fileCount;
filePaths = result.filePaths;
}
const question = argv.promptInteractive || argv.prompt || '';
@@ -476,11 +505,19 @@ export async function loadCliConfig(
}
// Override approval mode if disableYoloMode is set.
if (settings.security?.disableYoloMode) {
if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) {
if (approvalMode === ApprovalMode.YOLO) {
debugLogger.error('YOLO mode is disabled by the "disableYolo" setting.');
if (settings.admin?.secureModeEnabled) {
debugLogger.error(
'YOLO mode is disabled by "secureModeEnabled" setting.',
);
} else {
debugLogger.error(
'YOLO mode is disabled by the "disableYolo" setting.',
);
}
throw new FatalConfigError(
'Cannot start in YOLO mode when it is disabled by settings',
'Cannot start in YOLO mode since it is disabled by your admin',
);
}
approvalMode = ApprovalMode.DEFAULT;
@@ -513,23 +550,16 @@ export async function loadCliConfig(
throw err;
}
const policyEngineConfig = await createPolicyEngineConfig(
settings,
approvalMode,
);
const enableMessageBusIntegration =
settings.tools?.enableMessageBusIntegration ?? true;
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
// Interactive mode: explicit -i flag or (TTY + no args + no -p flag)
const hasQuery = !!argv.query;
const interactive =
!!argv.promptInteractive ||
!!argv.experimentalAcp ||
(process.stdin.isTTY && !hasQuery && !argv.prompt);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
// In non-interactive mode, exclude tools that require a prompt.
const extraExcludes: string[] = [];
if (!interactive) {
@@ -569,7 +599,29 @@ export async function loadCliConfig(
extraExcludes.length > 0 ? extraExcludes : undefined,
);
const defaultModel = DEFAULT_GEMINI_MODEL_AUTO;
// Create a settings object that includes CLI overrides for policy generation
const effectiveSettings: Settings = {
...settings,
tools: {
...settings.tools,
allowed: allowedTools,
exclude: excludeTools,
},
mcp: {
...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
},
};
const policyEngineConfig = await createPolicyEngineConfig(
effectiveSettings,
approvalMode,
);
policyEngineConfig.nonInteractive = !interactive;
const defaultModel = settings.general?.previewFeatures
? PREVIEW_GEMINI_MODEL_AUTO
: DEFAULT_GEMINI_MODEL_AUTO;
const resolvedModel: string =
argv.model ||
process.env['GEMINI_MODEL'] ||
@@ -584,6 +636,8 @@ export async function loadCliConfig(
const ptyInfo = await getPty();
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
return new Config({
sessionId,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
@@ -602,24 +656,34 @@ export async function loadCliConfig(
excludeTools,
toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand,
mcpServerCommand: settings.mcp?.serverCommand,
mcpServers: settings.mcpServers,
allowedMcpServers: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
blockedMcpServers: argv.allowedMcpServerNames
? [] // explicitly allowed servers overrides everything
: settings.mcp?.excluded,
mcpServerCommand: mcpEnabled ? settings.mcp?.serverCommand : undefined,
mcpServers: mcpEnabled ? settings.mcpServers : {},
mcpEnabled,
allowedMcpServers: mcpEnabled
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
: undefined,
blockedMcpServers: mcpEnabled
? argv.allowedMcpServerNames
? undefined
: settings.mcp?.excluded
: undefined,
blockedEnvironmentVariables:
settings.security?.environmentVariableRedaction?.blocked,
enableEnvironmentVariableRedaction:
settings.security?.environmentVariableRedaction?.enabled,
userMemory: memoryContent,
geminiMdFileCount: fileCount,
geminiMdFilePaths: filePaths,
approvalMode,
disableYoloMode: settings.security?.disableYoloMode,
disableYoloMode:
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
showMemoryUsage: settings.ui?.showMemoryUsage || false,
accessibility: {
...settings.ui?.accessibility,
screenReader,
},
telemetry: telemetrySettings,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled,
fileFiltering,
checkpointing: settings.general?.checkpointing?.enabled,
proxy:
@@ -631,7 +695,7 @@ export async function loadCliConfig(
fileDiscoveryService: fileService,
bugCommand: settings.advanced?.bugCommand,
model: resolvedModel,
maxSessionTurns: settings.model?.maxSessionTurns ?? -1,
maxSessionTurns: settings.model?.maxSessionTurns,
experimentalZedIntegration: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
listSessions: argv.listSessions || false,
@@ -640,8 +704,8 @@ export async function loadCliConfig(
extensionLoader: extensionManager,
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
enableModelAvailabilityService:
settings.experimental?.isModelAvailabilityServiceEnabled,
skillsSupport: settings.experimental?.skills,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
@@ -651,31 +715,40 @@ export async function loadCliConfig(
interactive,
trustedFolder,
useRipgrep: settings.tools?.useRipgrep,
enableInteractiveShell:
settings.tools?.shell?.enableInteractiveShell ?? true,
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
enableShellOutputEfficiency:
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion ?? false,
enablePromptCompletion: settings.general?.enablePromptCompletion,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
truncateToolOutputLines: settings.tools?.truncateToolOutputLines,
enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation,
eventEmitter: appEvents,
useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
output: {
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
},
enableMessageBusIntegration,
codebaseInvestigatorSettings:
settings.experimental?.codebaseInvestigatorSettings,
introspectionAgentSettings:
settings.experimental?.introspectionAgentSettings,
fakeResponses: argv.fakeResponses,
recordResponses: argv.recordResponses,
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
retryFetchErrors: settings.general?.retryFetchErrors,
ptyInfo: ptyInfo?.name,
modelConfigServiceConfig: settings.modelConfigs,
// TODO: loading of hooks based on workspace trust
enableHooks: settings.tools?.enableHooks ?? false,
enableHooks: getEnableHooks(settings),
hooks: settings.hooks || {},
projectHooks: projectHooks || {},
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
onReload: async () => {
const refreshedSettings = loadSettings(cwd);
return {
disabledSkills: refreshedSettings.merged.skills?.disabled,
};
},
});
}
@@ -0,0 +1,138 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { ExtensionManager } from './extension-manager.js';
import { loadSettings } from './settings.js';
import { createExtension } from '../test-utils/createExtension.js';
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
import { coreEvents } from '@google/gemini-cli-core';
const mockHomedir = vi.hoisted(() => vi.fn());
vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof import('node:os')>();
return {
...mockedOs,
homedir: mockHomedir,
};
});
describe('ExtensionManager skills validation', () => {
let tempHomeDir: string;
let tempWorkspaceDir: string;
let userExtensionsDir: string;
let extensionManager: ExtensionManager;
beforeEach(() => {
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-skills-test-home-'),
);
tempWorkspaceDir = fs.mkdtempSync(
path.join(tempHomeDir, 'gemini-cli-skills-test-workspace-'),
);
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
fs.mkdirSync(userExtensionsDir, { recursive: true });
mockHomedir.mockReturnValue(tempHomeDir);
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: vi.fn().mockResolvedValue(''),
settings: loadSettings(tempWorkspaceDir).merged,
});
vi.spyOn(coreEvents, 'emitFeedback');
});
afterEach(() => {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it('should emit a warning during install if skills directory is not empty but no skills are loaded', async () => {
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'skills-ext',
version: '1.0.0',
});
const skillsDir = path.join(sourceExtDir, 'skills');
fs.mkdirSync(skillsDir);
fs.writeFileSync(path.join(skillsDir, 'not-a-skill.txt'), 'hello');
await extensionManager.loadExtensions();
const extension = await extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
});
expect(extension.name).toBe('skills-ext');
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'warning',
expect.stringContaining('Failed to load skills from'),
);
});
it('should emit a warning during load if skills directory is not empty but no skills are loaded', async () => {
const extDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'load-skills-ext',
version: '1.0.0',
});
const skillsDir = path.join(extDir, 'skills');
fs.mkdirSync(skillsDir);
fs.writeFileSync(path.join(skillsDir, 'not-a-skill.txt'), 'hello');
await extensionManager.loadExtensions();
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'warning',
expect.stringContaining('Failed to load skills from'),
);
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'warning',
expect.stringContaining(
'The directory is not empty but no valid skills were discovered',
),
);
});
it('should succeed if skills are correctly loaded', async () => {
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'good-skills-ext',
version: '1.0.0',
});
const skillsDir = path.join(sourceExtDir, 'skills');
const skillSubdir = path.join(skillsDir, 'test-skill');
fs.mkdirSync(skillSubdir, { recursive: true });
fs.writeFileSync(
path.join(skillSubdir, 'SKILL.md'),
'---\nname: test-skill\ndescription: test desc\n---\nbody',
);
await extensionManager.loadExtensions();
const extension = await extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
});
expect(extension.skills).toHaveLength(1);
expect(extension.skills![0].name).toBe('test-skill');
// It might be called for other reasons during startup, but shouldn't be called for our skills loading success
// Actually, it shouldn't be called with our warning message
expect(coreEvents.emitFeedback).not.toHaveBeenCalledWith(
'warning',
expect.stringContaining('Failed to load skills from'),
);
});
});
+120 -31
View File
@@ -7,6 +7,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { stat } from 'node:fs/promises';
import chalk from 'chalk';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
import { type Settings, SettingScope } from './settings.js';
@@ -37,12 +38,15 @@ import {
logExtensionInstallEvent,
logExtensionUninstall,
logExtensionUpdateEvent,
loadSkillsFromDir,
type ExtensionEvents,
type MCPServerConfig,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
type HookDefinition,
type HookEventName,
type ResolvedExtensionSetting,
coreEvents,
} from '@google/gemini-cli-core';
import { maybeRequestConsentOrFail } from './extensions/consent.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
@@ -56,9 +60,11 @@ import {
import {
getEnvContents,
maybePromptForSettings,
getMissingSettings,
type ExtensionSetting,
} from './extensions/extensionSettings.js';
import type { EventEmitter } from 'node:stream';
import { getEnableHooks } from './settingsSchema.js';
interface ExtensionManagerParams {
enabledExtensionOverrides?: string[];
@@ -198,7 +204,9 @@ export class ExtensionManager extends ExtensionLoader {
installMetadata.type === 'git') ||
// Otherwise ask the user if they would like to try a git clone.
(await this.requestConsent(
`Error downloading github release for ${installMetadata.source} with the following error: ${result.errorMessage}.\n\nWould you like to attempt to install via "git clone" instead?`,
`Error downloading github release for ${installMetadata.source} with the following error: ${result.errorMessage}.
Would you like to attempt to install via "git clone" instead?`,
))
) {
await cloneFromGit(installMetadata, tempDir);
@@ -220,26 +228,7 @@ export class ExtensionManager extends ExtensionLoader {
}
try {
newExtensionConfig = this.loadExtensionConfig(localSourcePath);
if (isUpdate && installMetadata.autoUpdate) {
const oldSettings = new Set(
previousExtensionConfig.settings?.map((s) => s.name) || [],
);
const newSettings = new Set(
newExtensionConfig.settings?.map((s) => s.name) || [],
);
const settingsAreEqual =
oldSettings.size === newSettings.size &&
[...oldSettings].every((value) => newSettings.has(value));
if (!settingsAreEqual && installMetadata.autoUpdate) {
throw new Error(
`Extension "${newExtensionConfig.name}" has settings changes and cannot be auto-updated. Please update manually.`,
);
}
}
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
const newExtensionName = newExtensionConfig.name;
const previous = this.getExtensions().find(
@@ -258,10 +247,17 @@ export class ExtensionManager extends ExtensionLoader {
const newHasHooks = fs.existsSync(
path.join(localSourcePath, 'hooks', 'hooks.json'),
);
let previousHasHooks = false;
if (isUpdate && previous && previous.hooks) {
previousHasHooks = Object.keys(previous.hooks).length > 0;
}
const previousHasHooks = !!(
isUpdate &&
previous &&
previous.hooks &&
Object.keys(previous.hooks).length > 0
);
const newSkills = await loadSkillsFromDir(
path.join(localSourcePath, 'skills'),
);
const previousSkills = previous?.skills ?? [];
await maybeRequestConsentOrFail(
newExtensionConfig,
@@ -269,6 +265,8 @@ export class ExtensionManager extends ExtensionLoader {
newHasHooks,
previousExtensionConfig,
previousHasHooks,
newSkills,
previousSkills,
);
const extensionId = getExtensionId(newExtensionConfig, installMetadata);
const destinationPath = new ExtensionStorage(
@@ -302,6 +300,20 @@ export class ExtensionManager extends ExtensionLoader {
}
}
const missingSettings = await getMissingSettings(
newExtensionConfig,
extensionId,
);
if (missingSettings.length > 0) {
const message = `Extension "${newExtensionConfig.name}" has missing settings: ${missingSettings
.map((s) => s.name)
.join(
', ',
)}. Please run "gemini extensions settings ${newExtensionConfig.name} <setting-name>" to configure them.`;
debugLogger.warn(message);
coreEvents.emitFeedback('warning', message);
}
if (
installMetadata.type === 'local' ||
installMetadata.type === 'git' ||
@@ -364,7 +376,7 @@ export class ExtensionManager extends ExtensionLoader {
// to get the name and version for logging.
if (!newExtensionConfig && localSourcePath) {
try {
newExtensionConfig = this.loadExtensionConfig(localSourcePath);
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
} catch {
// Ignore error, this is just for logging.
}
@@ -491,7 +503,7 @@ export class ExtensionManager extends ExtensionLoader {
}
try {
let config = this.loadExtensionConfig(effectiveExtensionPath);
let config = await this.loadExtensionConfig(effectiveExtensionPath);
if (
this.getExtensions().find((extension) => extension.name === config.name)
) {
@@ -506,6 +518,24 @@ export class ExtensionManager extends ExtensionLoader {
);
config = resolveEnvVarsInObject(config, customEnv);
const resolvedSettings: ResolvedExtensionSetting[] = [];
if (config.settings) {
for (const setting of config.settings) {
const value = customEnv[setting.envVar];
resolvedSettings.push({
name: setting.name,
envVar: setting.envVar,
value:
value === undefined
? '[not set]'
: setting.sensitive
? '***'
: value,
sensitive: setting.sensitive ?? false,
});
}
}
if (config.mcpServers) {
config.mcpServers = Object.fromEntries(
Object.entries(config.mcpServers).map(([key, value]) => [
@@ -522,14 +552,18 @@ export class ExtensionManager extends ExtensionLoader {
.filter((contextFilePath) => fs.existsSync(contextFilePath));
let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined;
if (this.settings.tools?.enableHooks) {
if (getEnableHooks(this.settings)) {
hooks = await this.loadExtensionHooks(effectiveExtensionPath, {
extensionPath: effectiveExtensionPath,
workspacePath: this.workspaceDir,
});
}
const extension = {
const skills = await loadSkillsFromDir(
path.join(effectiveExtensionPath, 'skills'),
);
const extension: GeminiCLIExtension = {
name: config.name,
version: config.version,
path: effectiveExtensionPath,
@@ -543,6 +577,9 @@ export class ExtensionManager extends ExtensionLoader {
this.workspaceDir,
),
id: getExtensionId(config, installMetadata),
settings: config.settings,
resolvedSettings,
skills,
};
this.loadedExtensions = [...this.loadedExtensions, extension];
@@ -571,13 +608,13 @@ export class ExtensionManager extends ExtensionLoader {
return this.maybeStopExtension(extension);
}
loadExtensionConfig(extensionDir: string): ExtensionConfig {
async loadExtensionConfig(extensionDir: string): Promise<ExtensionConfig> {
const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
if (!fs.existsSync(configFilePath)) {
throw new Error(`Configuration file not found at ${configFilePath}`);
}
try {
const configContent = fs.readFileSync(configFilePath, 'utf-8');
const configContent = await fs.promises.readFile(configFilePath, 'utf-8');
const rawConfig = JSON.parse(configContent) as ExtensionConfig;
if (!rawConfig.name || !rawConfig.version) {
throw new Error(
@@ -697,6 +734,19 @@ export class ExtensionManager extends ExtensionLoader {
output += `\n ${tool}`;
});
}
if (extension.skills && extension.skills.length > 0) {
output += `\n Agent skills:`;
extension.skills.forEach((skill) => {
output += `\n ${skill.name}: ${skill.description}`;
});
}
const resolvedSettings = extension.resolvedSettings;
if (resolvedSettings && resolvedSettings.length > 0) {
output += `\n Settings:`;
resolvedSettings.forEach((setting) => {
output += `\n ${setting.name}: ${setting.value}`;
});
}
return output;
}
@@ -797,6 +847,45 @@ function validateName(name: string) {
}
}
export async function inferInstallMetadata(
source: string,
args: {
ref?: string;
autoUpdate?: boolean;
allowPreRelease?: boolean;
} = {},
): Promise<ExtensionInstallMetadata> {
if (
source.startsWith('http://') ||
source.startsWith('https://') ||
source.startsWith('git@') ||
source.startsWith('sso://')
) {
return {
source,
type: 'git',
ref: args.ref,
autoUpdate: args.autoUpdate,
allowPreRelease: args.allowPreRelease,
};
} else {
if (args.ref || args.autoUpdate) {
throw new Error(
'--ref and --auto-update are not applicable for local extensions.',
);
}
try {
await stat(source);
return {
source,
type: 'local',
};
} catch {
throw new Error('Install source not found.');
}
}
}
export function getExtensionId(
config: ExtensionConfig,
installMetadata?: ExtensionInstallMetadata,
+27 -22
View File
@@ -750,8 +750,8 @@ describe('extension tests', () => {
);
const settings = loadSettings(tempWorkspaceDir).merged;
if (!settings.tools) settings.tools = {};
settings.tools.enableHooks = true;
if (!settings.hooks) settings.hooks = {};
settings.hooks.enabled = true;
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
@@ -771,7 +771,7 @@ describe('extension tests', () => {
);
});
it('should not load hooks if enableHooks is false', async () => {
it('should not load hooks if hooks.enabled is false', async () => {
const extDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'hook-extension-disabled',
@@ -786,8 +786,8 @@ describe('extension tests', () => {
);
const settings = loadSettings(tempWorkspaceDir).merged;
if (!settings.tools) settings.tools = {};
settings.tools.enableHooks = false;
if (!settings.hooks) settings.hooks = {};
settings.hooks.enabled = false;
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
@@ -1424,9 +1424,10 @@ This extension will run the following MCP servers:
],
});
const previousExtensionConfig = extensionManager.loadExtensionConfig(
path.join(userExtensionsDir, 'my-local-extension'),
);
const previousExtensionConfig =
await extensionManager.loadExtensionConfig(
path.join(userExtensionsDir, 'my-local-extension'),
);
mockPromptForSettings.mockResolvedValueOnce('new-setting-value');
// 3. Call installOrUpdateExtension to perform the update.
@@ -1446,7 +1447,7 @@ This extension will run the following MCP servers:
expect(envContent).toContain('NEW_SETTING=new-setting-value');
});
it('should fail auto-update if settings have changed', async () => {
it('should auto-update if settings have changed', async () => {
// 1. Install initial version with autoUpdate: true
const oldSourceExtDir = createExtension({
extensionsDir: tempHomeDir,
@@ -1468,7 +1469,7 @@ This extension will run the following MCP servers:
});
// 2. Create new version with different settings
const newSourceExtDir = createExtension({
const extensionDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-auto-update-ext',
version: '1.1.0',
@@ -1481,19 +1482,23 @@ This extension will run the following MCP servers:
],
});
const previousExtensionConfig = extensionManager.loadExtensionConfig(
path.join(userExtensionsDir, 'my-auto-update-ext'),
);
const previousExtensionConfig =
await extensionManager.loadExtensionConfig(
path.join(userExtensionsDir, 'my-auto-update-ext'),
);
// 3. Attempt to update and assert it fails
await expect(
extensionManager.installOrUpdateExtension(
{ source: newSourceExtDir, type: 'local', autoUpdate: true },
previousExtensionConfig,
),
).rejects.toThrow(
'Extension "my-auto-update-ext" has settings changes and cannot be auto-updated. Please update manually.',
const updatedExtension = await extensionManager.installOrUpdateExtension(
{
source: extensionDir,
type: 'local',
autoUpdate: true,
},
previousExtensionConfig,
);
expect(updatedExtension.version).toBe('1.1.0');
expect(extensionManager.getExtensions()[0].version).toBe('1.1.0');
});
it('should throw an error for invalid extension names', async () => {
@@ -1967,7 +1972,7 @@ This extension will run the following MCP servers:
expect(activeExtensions).toHaveLength(0);
await extensionManager.enableExtension('ext1', SettingScope.User);
activeExtensions = await getActiveExtensions();
activeExtensions = getActiveExtensions();
expect(activeExtensions).toHaveLength(1);
expect(activeExtensions[0].name).toBe('ext1');
});
@@ -1984,7 +1989,7 @@ This extension will run the following MCP servers:
expect(activeExtensions).toHaveLength(0);
await extensionManager.enableExtension('ext1', SettingScope.Workspace);
activeExtensions = await getActiveExtensions();
activeExtensions = getActiveExtensions();
expect(activeExtensions).toHaveLength(1);
expect(activeExtensions[0].name).toBe('ext1');
});
@@ -5,15 +5,20 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import chalk from 'chalk';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import {
requestConsentNonInteractive,
requestConsentInteractive,
maybeRequestConsentOrFail,
INSTALL_WARNING_MESSAGE,
SKILLS_WARNING_MESSAGE,
} from './consent.js';
import type { ConfirmationRequest } from '../../ui/types.js';
import type { ExtensionConfig } from '../extension.js';
import { debugLogger } from '@google/gemini-cli-core';
import { debugLogger, type SkillDefinition } from '@google/gemini-cli-core';
const mockReadline = vi.hoisted(() => ({
createInterface: vi.fn().mockReturnValue({
@@ -22,12 +27,26 @@ const mockReadline = vi.hoisted(() => ({
}),
}));
const mockReaddir = vi.hoisted(() => vi.fn());
const originalReaddir = vi.hoisted(() => ({
current: null as typeof fs.readdir | null,
}));
// Mocking readline for non-interactive prompts
vi.mock('node:readline', () => ({
default: mockReadline,
createInterface: mockReadline.createInterface,
}));
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>();
originalReaddir.current = actual.readdir;
return {
...actual,
readdir: mockReaddir,
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
@@ -40,11 +59,22 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
});
describe('consent', () => {
beforeEach(() => {
let tempDir: string;
beforeEach(async () => {
vi.clearAllMocks();
if (originalReaddir.current) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockReaddir.mockImplementation(originalReaddir.current as any);
}
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'consent-test-'));
});
afterEach(() => {
afterEach(async () => {
vi.restoreAllMocks();
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
describe('requestConsentNonInteractive', () => {
@@ -250,6 +280,105 @@ describe('consent', () => {
);
expect(requestConsent).toHaveBeenCalledTimes(1);
});
it('should request consent if skills change', async () => {
const skill1Dir = path.join(tempDir, 'skill1');
const skill2Dir = path.join(tempDir, 'skill2');
await fs.mkdir(skill1Dir, { recursive: true });
await fs.mkdir(skill2Dir, { recursive: true });
await fs.writeFile(path.join(skill1Dir, 'SKILL.md'), 'body1');
await fs.writeFile(path.join(skill1Dir, 'extra.txt'), 'extra');
await fs.writeFile(path.join(skill2Dir, 'SKILL.md'), 'body2');
const skill1: SkillDefinition = {
name: 'skill1',
description: 'desc1',
location: path.join(skill1Dir, 'SKILL.md'),
body: 'body1',
};
const skill2: SkillDefinition = {
name: 'skill2',
description: 'desc2',
location: path.join(skill2Dir, 'SKILL.md'),
body: 'body2',
};
const config: ExtensionConfig = {
...baseConfig,
mcpServers: {
server1: { command: 'npm', args: ['start'] },
server2: { httpUrl: 'https://remote.com' },
},
contextFileName: 'my-context.md',
excludeTools: ['tool1', 'tool2'],
};
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
config,
requestConsent,
false,
undefined,
false,
[skill1, skill2],
);
const expectedConsentString = [
'Installing extension "test-ext".',
INSTALL_WARNING_MESSAGE,
'This extension will run the following MCP servers:',
' * server1 (local): npm start',
' * server2 (remote): https://remote.com',
'This extension will append info to your gemini.md context using my-context.md',
'This extension will exclude the following core tools: tool1,tool2',
'',
chalk.bold('Agent Skills:'),
SKILLS_WARNING_MESSAGE,
'This extension will install the following agent skills:',
` * ${chalk.bold('skill1')}: desc1`,
` (Location: ${skill1.location}) (2 items in directory)`,
` * ${chalk.bold('skill2')}: desc2`,
` (Location: ${skill2.location}) (1 items in directory)`,
'',
].join('\n');
expect(requestConsent).toHaveBeenCalledWith(expectedConsentString);
});
it('should show a warning if the skill directory cannot be read', async () => {
const lockedDir = path.join(tempDir, 'locked');
await fs.mkdir(lockedDir, { recursive: true });
const skill: SkillDefinition = {
name: 'locked-skill',
description: 'A skill in a locked dir',
location: path.join(lockedDir, 'SKILL.md'),
body: 'body',
};
// Mock readdir to simulate a permission error.
// We do this instead of using fs.mkdir(..., { mode: 0o000 }) because
// directory permissions work differently on Windows and 0o000 doesn't
// effectively block access there, leading to test failures in Windows CI.
mockReaddir.mockRejectedValueOnce(
new Error('EACCES: permission denied, scandir'),
);
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
baseConfig,
requestConsent,
false,
undefined,
false,
[skill],
);
expect(requestConsent).toHaveBeenCalledWith(
expect.stringContaining(
` (Location: ${skill.location}) ${chalk.red('⚠️ (Could not count items in directory)')}`,
),
);
});
});
});
});
+43 -9
View File
@@ -4,14 +4,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { debugLogger } from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { debugLogger, type SkillDefinition } from '@google/gemini-cli-core';
import chalk from 'chalk';
import type { ConfirmationRequest } from '../../ui/types.js';
import { escapeAnsiCtrlCodes } from '../../ui/utils/textUtils.js';
import type { ExtensionConfig } from '../extension.js';
export const INSTALL_WARNING_MESSAGE =
'**The extension you are about to install may have been created by a third-party developer and sourced from a public repository. Google does not vet, endorse, or guarantee the functionality or security of extensions. Please carefully inspect any extension and its source code before installing to understand the permissions it requires and the actions it may perform.**';
export const INSTALL_WARNING_MESSAGE = chalk.yellow(
'The extension you are about to install may have been created by a third-party developer and sourced from a public repository. Google does not vet, endorse, or guarantee the functionality or security of extensions. Please carefully inspect any extension and its source code before installing to understand the permissions it requires and the actions it may perform.',
);
export const SKILLS_WARNING_MESSAGE = chalk.yellow(
"Agent skills inject specialized instructions and domain-specific knowledge into the agent's system prompt. This can change how the agent interprets your requests and interacts with your environment. Review the skill definitions at the location(s) provided below to ensure they meet your security standards.",
);
/**
* Requests consent from the user to perform an action, by reading a Y/n
@@ -38,7 +46,7 @@ export async function requestConsentNonInteractive(
* This should not be called from non-interactive mode as it will not work.
*
* @param consentDescription The description of the thing they will be consenting to.
* @param setExtensionUpdateConfirmationRequest A function to actually add a prompt to the UI.
* @param addExtensionUpdateConfirmationRequest A function to actually add a prompt to the UI.
* @returns boolean, whether they consented or not.
*/
export async function requestConsentInteractive(
@@ -82,7 +90,7 @@ async function promptForConsentNonInteractive(
* This should not be called from non-interactive mode as it will break the CLI.
*
* @param prompt A markdown prompt to ask the user
* @param setExtensionUpdateConfirmationRequest Function to update the UI state with the confirmation request.
* @param addExtensionUpdateConfirmationRequest Function to update the UI state with the confirmation request.
* @returns Whether or not the user answers yes.
*/
async function promptForConsentInteractive(
@@ -103,10 +111,11 @@ async function promptForConsentInteractive(
* Builds a consent string for installing an extension based on it's
* extensionConfig.
*/
function extensionConsentString(
async function extensionConsentString(
extensionConfig: ExtensionConfig,
hasHooks: boolean,
): string {
skills: SkillDefinition[] = [],
): Promise<string> {
const sanitizedConfig = escapeAnsiCtrlCodes(extensionConfig);
const output: string[] = [];
const mcpServerEntries = Object.entries(sanitizedConfig.mcpServers || {});
@@ -138,6 +147,24 @@ function extensionConsentString(
'⚠️ This extension contains Hooks which can automatically execute commands.',
);
}
if (skills.length > 0) {
output.push(`\n${chalk.bold('Agent Skills:')}`);
output.push(SKILLS_WARNING_MESSAGE);
output.push('This extension will install the following agent skills:');
for (const skill of skills) {
output.push(` * ${chalk.bold(skill.name)}: ${skill.description}`);
const skillDir = path.dirname(skill.location);
let fileCountStr = '';
try {
const skillDirItems = await fs.readdir(skillDir);
fileCountStr = ` (${skillDirItems.length} items in directory)`;
} catch {
fileCountStr = ` ${chalk.red('⚠️ (Could not count items in directory)')}`;
}
output.push(` (Location: ${skill.location})${fileCountStr}`);
}
output.push('');
}
return output.join('\n');
}
@@ -156,12 +183,19 @@ export async function maybeRequestConsentOrFail(
hasHooks: boolean,
previousExtensionConfig?: ExtensionConfig,
previousHasHooks?: boolean,
skills: SkillDefinition[] = [],
previousSkills: SkillDefinition[] = [],
) {
const extensionConsent = extensionConsentString(extensionConfig, hasHooks);
const extensionConsent = await extensionConsentString(
extensionConfig,
hasHooks,
skills,
);
if (previousExtensionConfig) {
const previousExtensionConsent = extensionConsentString(
const previousExtensionConsent = await extensionConsentString(
previousExtensionConfig,
previousHasHooks ?? false,
previousSkills,
);
if (previousExtensionConsent === extensionConsent) {
return;
@@ -298,3 +298,24 @@ async function clearSettings(
}
return;
}
export async function getMissingSettings(
extensionConfig: ExtensionConfig,
extensionId: string,
): Promise<ExtensionSetting[]> {
const { settings } = extensionConfig;
if (!settings || settings.length === 0) {
return [];
}
const existingSettings = await getEnvContents(extensionConfig, extensionId);
const missingSettings: ExtensionSetting[] = [];
for (const setting of settings) {
if (existingSettings[setting.envVar] === undefined) {
missingSettings.push(setting);
}
}
return missingSettings;
}
@@ -0,0 +1,308 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as path from 'node:path';
import * as os from 'node:os';
import * as fs from 'node:fs';
import { getMissingSettings } from './extensionSettings.js';
import type { ExtensionConfig } from '../extension.js';
import { ExtensionStorage } from './storage.js';
import {
KeychainTokenStorage,
debugLogger,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
coreEvents,
} from '@google/gemini-cli-core';
import { EXTENSION_SETTINGS_FILENAME } from './variables.js';
import { ExtensionManager } from '../extension-manager.js';
vi.mock('node:fs', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actual = await importOriginal<any>();
return {
...actual,
default: {
...actual.default,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
existsSync: vi.fn((...args: any[]) => actual.existsSync(...args)),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
existsSync: vi.fn((...args: any[]) => actual.existsSync(...args)),
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
KeychainTokenStorage: vi.fn(),
debugLogger: {
warn: vi.fn(),
error: vi.fn(),
log: vi.fn(),
},
coreEvents: {
emitFeedback: vi.fn(), // Mock emitFeedback
on: vi.fn(),
off: vi.fn(),
},
};
});
// Mock os.homedir because ExtensionStorage uses it
vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof os>();
return {
...mockedOs,
homedir: vi.fn(),
};
});
describe('extensionUpdates', () => {
let tempHomeDir: string;
let tempWorkspaceDir: string;
let extensionDir: string;
let mockKeychainData: Record<string, Record<string, string>>;
beforeEach(() => {
vi.clearAllMocks();
mockKeychainData = {};
// Mock Keychain
vi.mocked(KeychainTokenStorage).mockImplementation(
(serviceName: string) => {
if (!mockKeychainData[serviceName]) {
mockKeychainData[serviceName] = {};
}
const keychainData = mockKeychainData[serviceName];
return {
getSecret: vi
.fn()
.mockImplementation(
async (key: string) => keychainData[key] || null,
),
setSecret: vi
.fn()
.mockImplementation(async (key: string, value: string) => {
keychainData[key] = value;
}),
deleteSecret: vi.fn().mockImplementation(async (key: string) => {
delete keychainData[key];
}),
listSecrets: vi
.fn()
.mockImplementation(async () => Object.keys(keychainData)),
isAvailable: vi.fn().mockResolvedValue(true),
} as unknown as KeychainTokenStorage;
},
);
// Setup Temp Dirs
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);
tempWorkspaceDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-workspace-'),
);
extensionDir = path.join(tempHomeDir, '.gemini', 'extensions', 'test-ext');
// Mock ExtensionStorage to rely on our temp extension dir
vi.spyOn(ExtensionStorage.prototype, 'getExtensionDir').mockReturnValue(
extensionDir,
);
// Mock getEnvFilePath is checking extensionDir/variables.env? No, it used ExtensionStorage logic.
// getEnvFilePath in extensionSettings.ts:
// if workspace, process.cwd()/.env (we need to mock process.cwd or move tempWorkspaceDir there)
// if user, ExtensionStorage(name).getEnvFilePath() -> joins extensionDir + '.env'
fs.mkdirSync(extensionDir, { recursive: true });
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
});
afterEach(() => {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
describe('getMissingSettings', () => {
it('should return empty list if all settings are present', async () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [
{ name: 's1', description: 'd1', envVar: 'VAR1' },
{ name: 's2', description: 'd2', envVar: 'VAR2', sensitive: true },
],
};
const extensionId = '12345';
// Setup User Env
const userEnvPath = path.join(extensionDir, EXTENSION_SETTINGS_FILENAME);
fs.writeFileSync(userEnvPath, 'VAR1=val1');
// Setup Keychain
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext ${extensionId}`,
);
await userKeychain.setSecret('VAR2', 'val2');
const missing = await getMissingSettings(config, extensionId);
expect(missing).toEqual([]);
});
it('should identify missing non-sensitive settings', async () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
};
const extensionId = '12345';
const missing = await getMissingSettings(config, extensionId);
expect(missing).toHaveLength(1);
expect(missing[0].name).toBe('s1');
});
it('should identify missing sensitive settings', async () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [
{ name: 's2', description: 'd2', envVar: 'VAR2', sensitive: true },
],
};
const extensionId = '12345';
const missing = await getMissingSettings(config, extensionId);
expect(missing).toHaveLength(1);
expect(missing[0].name).toBe('s2');
});
it('should respect settings present in workspace', async () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
};
const extensionId = '12345';
// Setup Workspace Env
const workspaceEnvPath = path.join(
tempWorkspaceDir,
EXTENSION_SETTINGS_FILENAME,
);
fs.writeFileSync(workspaceEnvPath, 'VAR1=val1');
const missing = await getMissingSettings(config, extensionId);
expect(missing).toEqual([]);
});
});
describe('ExtensionManager integration', () => {
it('should warn about missing settings after update', async () => {
// Mock ExtensionManager methods to avoid FS/Network usage
const newConfig: ExtensionConfig = {
name: 'test-ext',
version: '1.1.0',
settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
};
const previousConfig: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [],
};
const installMetadata: ExtensionInstallMetadata = {
source: extensionDir,
type: 'local',
autoUpdate: true,
};
const manager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settings: { telemetry: {} } as any,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null, // Simulate non-interactive
});
// Mock methods called by installOrUpdateExtension
vi.spyOn(manager, 'loadExtensionConfig').mockResolvedValue(newConfig);
vi.spyOn(manager, 'getExtensions').mockReturnValue([
{
name: 'test-ext',
version: '1.0.0',
installMetadata,
path: extensionDir,
// Mocks for other required props
contextFiles: [],
mcpServers: {},
hooks: undefined,
isActive: true,
id: 'test-id',
settings: [],
resolvedSettings: [],
skills: [],
} as unknown as GeminiCLIExtension,
]);
vi.spyOn(manager, 'uninstallExtension').mockResolvedValue(undefined);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.spyOn(manager as any, 'loadExtension').mockResolvedValue(
{} as unknown as GeminiCLIExtension,
);
vi.spyOn(manager, 'enableExtension').mockResolvedValue(undefined);
// Mock fs.promises for the operations inside installOrUpdateExtension
vi.spyOn(fs.promises, 'mkdir').mockResolvedValue(undefined);
vi.spyOn(fs.promises, 'writeFile').mockResolvedValue(undefined);
vi.spyOn(fs.promises, 'rm').mockResolvedValue(undefined);
vi.mocked(fs.existsSync).mockReturnValue(false); // No hooks
// Mock copyExtension? It's imported.
// We can rely on ignoring the failure if we mock enough.
// Actually copyExtension is called. We need to mock it if it does real IO.
// But we can just let it fail or mock fs.cp if it uses it.
// Let's assume the other mocks cover the critical path to the warning.
// Warning happens BEFORE copyExtension?
// No, warning is after copyExtension usually.
// But in my code:
// const missingSettings = await getMissingSettings(...)
// if (missingSettings.length > 0) debugLogger.warn(...)
// ...
// copyExtension(...)
// Wait, let's check extension-manager.ts order.
// Line 303: getMissingSettings
// Line 317: if (installMetadata.type === 'local' ...) copyExtension
// So getMissingSettings is called BEFORE copyExtension.
try {
await manager.installOrUpdateExtension(installMetadata, previousConfig);
} catch (_) {
// Ignore errors from copyExtension or others, we just want to verify the warning
}
expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'Extension "test-ext" has missing settings: s1',
),
);
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'warning',
expect.stringContaining(
'Please run "gemini extensions settings test-ext <setting-name>"',
),
);
});
});
});
@@ -221,9 +221,11 @@ describe('github.ts', () => {
});
it('should return NOT_UPDATABLE for non-git/non-release extensions', async () => {
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
version: '1.0.0',
} as unknown as ExtensionConfig);
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
Promise.resolve({
version: '1.0.0',
} as unknown as ExtensionConfig),
);
const linkExt = {
installMetadata: { type: 'link' },
+1 -1
View File
@@ -175,7 +175,7 @@ export async function checkForExtensionUpdate(
if (installMetadata?.type === 'local') {
let latestConfig: ExtensionConfig | undefined;
try {
latestConfig = extensionManager.loadExtensionConfig(
latestConfig = await extensionManager.loadExtensionConfig(
installMetadata.source,
);
} catch (e) {
@@ -145,10 +145,12 @@ describe('Extension Update Logic', () => {
});
it('should successfully update extension and set state to UPDATED_NEEDS_RESTART by default', async () => {
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
name: 'test-extension',
version: '1.0.0',
});
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
Promise.resolve({
name: 'test-extension',
version: '1.0.0',
}),
);
vi.mocked(
mockExtensionManager.installOrUpdateExtension,
).mockResolvedValue({
@@ -183,10 +185,12 @@ describe('Extension Update Logic', () => {
});
it('should set state to UPDATED if enableExtensionReloading is true', async () => {
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
name: 'test-extension',
version: '1.0.0',
});
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
Promise.resolve({
name: 'test-extension',
version: '1.0.0',
}),
);
vi.mocked(
mockExtensionManager.installOrUpdateExtension,
).mockResolvedValue({
@@ -212,10 +216,12 @@ describe('Extension Update Logic', () => {
});
it('should rollback and set state to ERROR if installation fails', async () => {
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
name: 'test-extension',
version: '1.0.0',
});
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
Promise.resolve({
name: 'test-extension',
version: '1.0.0',
}),
);
vi.mocked(
mockExtensionManager.installOrUpdateExtension,
).mockRejectedValue(new Error('Install failed'));
@@ -257,10 +263,12 @@ describe('Extension Update Logic', () => {
['ext3', { status: ExtensionUpdateState.UPDATE_AVAILABLE }],
]);
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
name: 'ext',
version: '1.0.0',
});
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
Promise.resolve({
name: 'ext',
version: '1.0.0',
}),
);
vi.mocked(
mockExtensionManager.installOrUpdateExtension,
).mockResolvedValue({ ...mockExtension, version: '1.1.0' });
+1 -1
View File
@@ -59,7 +59,7 @@ export async function updateExtension(
const tempDir = await ExtensionStorage.createTmpDir();
try {
const previousExtensionConfig = extensionManager.loadExtensionConfig(
const previousExtensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
let updatedExtension: GeminiCLIExtension;
+4 -1
View File
@@ -192,7 +192,10 @@ export const defaultKeyBindings: KeyBindingConfig = {
{ key: 'x', ctrl: true },
{ sequence: '\x18', ctrl: true },
],
[Command.PASTE_CLIPBOARD]: [{ key: 'v', ctrl: true }],
[Command.PASTE_CLIPBOARD]: [
{ key: 'v', ctrl: true },
{ key: 'v', command: true },
],
// App level bindings
[Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }],
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
ApprovalMode,
PolicyDecision,
@@ -13,6 +13,21 @@ import {
import { createPolicyEngineConfig } from './policy.js';
import type { Settings } from './settings.js';
// Mock Storage to ensure tests are hermetic and don't read from user's home directory
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
const Storage = actual.Storage;
// Monkey-patch static methods
Storage.getUserPoliciesDir = () => '/non-existent/user/policies';
Storage.getSystemPoliciesDir = () => '/non-existent/system/policies';
return {
...actual,
Storage,
};
});
describe('Policy Engine Integration Tests', () => {
describe('Policy configuration produces valid PolicyEngine config', () => {
it('should create a working PolicyEngine from basic settings', async () => {
@@ -219,6 +219,60 @@ describe('settings-validation', () => {
}
});
it('should validate mcpServers with type field for all transport types', () => {
const validSettings = {
mcpServers: {
'sse-server': {
url: 'https://example.com/sse',
type: 'sse',
headers: { 'X-API-Key': 'key' },
},
'http-server': {
url: 'https://example.com/mcp',
type: 'http',
},
'stdio-server': {
command: '/usr/bin/mcp-server',
type: 'stdio',
},
},
};
const result = validateSettings(validSettings);
expect(result.success).toBe(true);
});
it('should reject invalid type values in mcpServers', () => {
const invalidSettings = {
mcpServers: {
'bad-server': {
url: 'https://example.com/mcp',
type: 'invalid-type',
},
},
};
const result = validateSettings(invalidSettings);
expect(result.success).toBe(false);
});
it('should validate mcpServers without type field', () => {
const validSettings = {
mcpServers: {
'stdio-server': {
command: '/usr/bin/mcp-server',
args: ['--port', '8080'],
},
'url-server': {
url: 'https://example.com/mcp',
},
},
};
const result = validateSettings(validSettings);
expect(result.success).toBe(true);
});
it('should validate complex nested customThemes configuration', () => {
const invalidSettings = {
ui: {
@@ -269,9 +323,7 @@ describe('settings-validation', () => {
expect(formatted).toContain('/path/to/settings.json');
expect(formatted).toContain('model.name');
expect(formatted).toContain('Expected: string, but received: object');
expect(formatted).toContain(
'Please fix the configuration and try again.',
);
expect(formatted).toContain('Please fix the configuration.');
expect(formatted).toContain(
'https://github.com/google-gemini/gemini-cli',
);
@@ -322,7 +322,7 @@ export function formatValidationError(
lines.push('');
}
lines.push('Please fix the configuration and try again.');
lines.push('Please fix the configuration.');
lines.push(
'See: https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md',
);
File diff suppressed because it is too large Load Diff
+61 -319
View File
@@ -10,7 +10,6 @@ import { homedir, platform } from 'node:os';
import * as dotenv from 'dotenv';
import process from 'node:process';
import {
debugLogger,
FatalConfigError,
GEMINI_DIR,
getErrorMessage,
@@ -30,14 +29,12 @@ import {
getSettingsSchema,
} from './settingsSchema.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
import { customDeepMerge, type MergeableObject } from '../utils/deepMerge.js';
import { customDeepMerge } from '../utils/deepMerge.js';
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
import type { ExtensionManager } from './extension-manager.js';
import {
validateSettings,
formatValidationError,
} from './settings-validation.js';
import { SettingPaths } from './settingPaths.js';
function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
let current: SettingDefinition | undefined = undefined;
@@ -66,79 +63,6 @@ export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
const MIGRATE_V2_OVERWRITE = true;
const MIGRATION_MAP: Record<string, string> = {
accessibility: 'ui.accessibility',
allowedTools: 'tools.allowed',
allowMCPServers: 'mcp.allowed',
autoAccept: 'tools.autoAccept',
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
bugCommand: 'advanced.bugCommand',
chatCompression: 'model.compressionThreshold',
checkpointing: 'general.checkpointing',
coreTools: 'tools.core',
contextFileName: 'context.fileName',
customThemes: 'ui.customThemes',
customWittyPhrases: 'ui.customWittyPhrases',
debugKeystrokeLogging: 'general.debugKeystrokeLogging',
disableAutoUpdate: 'general.disableAutoUpdate',
disableUpdateNag: 'general.disableUpdateNag',
dnsResolutionOrder: 'advanced.dnsResolutionOrder',
enableMessageBusIntegration: 'tools.enableMessageBusIntegration',
enableHooks: 'tools.enableHooks',
enablePromptCompletion: 'general.enablePromptCompletion',
enforcedAuthType: 'security.auth.enforcedType',
excludeTools: 'tools.exclude',
excludeMCPServers: 'mcp.excluded',
excludedProjectEnvVars: 'advanced.excludedEnvVars',
extensionManagement: 'experimental.extensionManagement',
extensions: 'extensions',
fileFiltering: 'context.fileFiltering',
folderTrustFeature: 'security.folderTrust.featureEnabled',
folderTrust: 'security.folderTrust.enabled',
hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge',
hideWindowTitle: 'ui.hideWindowTitle',
showStatusInTitle: 'ui.showStatusInTitle',
hideTips: 'ui.hideTips',
hideBanner: 'ui.hideBanner',
hideFooter: 'ui.hideFooter',
hideCWD: 'ui.footer.hideCWD',
hideSandboxStatus: 'ui.footer.hideSandboxStatus',
hideModelInfo: 'ui.footer.hideModelInfo',
hideContextSummary: 'ui.hideContextSummary',
showMemoryUsage: 'ui.showMemoryUsage',
showLineNumbers: 'ui.showLineNumbers',
showCitations: 'ui.showCitations',
ideMode: 'ide.enabled',
includeDirectories: 'context.includeDirectories',
loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories',
maxSessionTurns: 'model.maxSessionTurns',
mcpServers: 'mcpServers',
mcpServerCommand: 'mcp.serverCommand',
memoryImportFormat: 'context.importFormat',
memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs',
model: 'model.name',
preferredEditor: SettingPaths.General.PreferredEditor,
retryFetchErrors: 'general.retryFetchErrors',
sandbox: 'tools.sandbox',
selectedAuthType: 'security.auth.selectedType',
enableInteractiveShell: 'tools.shell.enableInteractiveShell',
shellPager: 'tools.shell.pager',
shellShowColor: 'tools.shell.showColor',
shellInactivityTimeout: 'tools.shell.inactivityTimeout',
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
summarizeToolOutput: 'model.summarizeToolOutput',
telemetry: 'telemetry',
theme: 'ui.theme',
toolDiscoveryCommand: 'tools.discoveryCommand',
toolCallCommand: 'tools.callCommand',
usageStatisticsEnabled: 'privacy.usageStatisticsEnabled',
useExternalAuth: 'security.auth.useExternal',
useRipgrep: 'tools.useRipgrep',
vimMode: 'general.vimMode',
};
export function getSystemSettingsPath(): string {
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
@@ -233,6 +157,7 @@ export interface SessionRetentionSettings {
export interface SettingsError {
message: string;
path: string;
severity: 'error' | 'warning';
}
export interface SettingsFile {
@@ -267,160 +192,22 @@ function setNestedProperty(
current[lastKey] = value;
}
export function needsMigration(settings: Record<string, unknown>): boolean {
// A file needs migration if it contains any top-level key that is moved to a
// nested location in V2.
const hasV1Keys = Object.entries(MIGRATION_MAP).some(([v1Key, v2Path]) => {
if (v1Key === v2Path || !(v1Key in settings)) {
return false;
}
// If a key exists that is a V1 key and a V2 container (like 'model'),
// we need to check the type. If it's an object, it's a V2 container and not
// a V1 key that needs migration.
if (
KNOWN_V2_CONTAINERS.has(v1Key) &&
typeof settings[v1Key] === 'object' &&
settings[v1Key] !== null
) {
return false;
}
return true;
});
return hasV1Keys;
}
function migrateSettingsToV2(
flatSettings: Record<string, unknown>,
): Record<string, unknown> | null {
if (!needsMigration(flatSettings)) {
return null;
}
const v2Settings: Record<string, unknown> = {};
const flatKeys = new Set(Object.keys(flatSettings));
for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
if (flatKeys.has(oldKey)) {
// If the key exists and is a V2 container (like 'model'), and the value is an object,
// it is likely already migrated or partially migrated. We should not move it
// to the mapped V2 path (e.g. 'model' -> 'model.name').
// Instead, let it fall through to the "Carry over" section to be merged.
if (
KNOWN_V2_CONTAINERS.has(oldKey) &&
typeof flatSettings[oldKey] === 'object' &&
flatSettings[oldKey] !== null &&
!Array.isArray(flatSettings[oldKey])
) {
continue;
export function getDefaultsFromSchema(
schema: SettingsSchema = getSettingsSchema(),
): Settings {
const defaults: Record<string, unknown> = {};
for (const key in schema) {
const definition = schema[key];
if (definition.properties) {
const childDefaults = getDefaultsFromSchema(definition.properties);
if (Object.keys(childDefaults).length > 0) {
defaults[key] = childDefaults;
}
setNestedProperty(v2Settings, newPath, flatSettings[oldKey]);
flatKeys.delete(oldKey);
} else if (definition.default !== undefined) {
defaults[key] = definition.default;
}
}
// Preserve mcpServers at the top level
if (flatSettings['mcpServers']) {
v2Settings['mcpServers'] = flatSettings['mcpServers'];
flatKeys.delete('mcpServers');
}
// Carry over any unrecognized keys
for (const remainingKey of flatKeys) {
const existingValue = v2Settings[remainingKey];
const newValue = flatSettings[remainingKey];
if (
typeof existingValue === 'object' &&
existingValue !== null &&
!Array.isArray(existingValue) &&
typeof newValue === 'object' &&
newValue !== null &&
!Array.isArray(newValue)
) {
const pathAwareGetStrategy = (path: string[]) =>
getMergeStrategyForPath([remainingKey, ...path]);
v2Settings[remainingKey] = customDeepMerge(
pathAwareGetStrategy,
{},
existingValue as MergeableObject,
newValue as MergeableObject,
);
} else {
v2Settings[remainingKey] = newValue;
}
}
return v2Settings;
}
function getNestedProperty(
obj: Record<string, unknown>,
path: string,
): unknown {
const keys = path.split('.');
let current: unknown = obj;
for (const key of keys) {
if (typeof current !== 'object' || current === null || !(key in current)) {
return undefined;
}
current = (current as Record<string, unknown>)[key];
}
return current;
}
const REVERSE_MIGRATION_MAP: Record<string, string> = Object.fromEntries(
Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]),
);
// Dynamically determine the top-level keys from the V2 settings structure.
const KNOWN_V2_CONTAINERS = new Set(
Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]),
);
export function migrateSettingsToV1(
v2Settings: Record<string, unknown>,
): Record<string, unknown> {
const v1Settings: Record<string, unknown> = {};
const v2Keys = new Set(Object.keys(v2Settings));
for (const [newPath, oldKey] of Object.entries(REVERSE_MIGRATION_MAP)) {
const value = getNestedProperty(v2Settings, newPath);
if (value !== undefined) {
v1Settings[oldKey] = value;
v2Keys.delete(newPath.split('.')[0]);
}
}
// Preserve mcpServers at the top level
if (v2Settings['mcpServers']) {
v1Settings['mcpServers'] = v2Settings['mcpServers'];
v2Keys.delete('mcpServers');
}
// Carry over any unrecognized keys
for (const remainingKey of v2Keys) {
const value = v2Settings[remainingKey];
if (value === undefined) {
continue;
}
// Don't carry over empty objects that were just containers for migrated settings.
if (
KNOWN_V2_CONTAINERS.has(remainingKey) &&
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.keys(value).length === 0
) {
continue;
}
v1Settings[remainingKey] = value;
}
return v1Settings;
return defaults as Settings;
}
function mergeSettings(
@@ -431,16 +218,18 @@ function mergeSettings(
isTrusted: boolean,
): Settings {
const safeWorkspace = isTrusted ? workspace : ({} as Settings);
const schemaDefaults = getDefaultsFromSchema();
// Settings are merged with the following precedence (last one wins for
// single values):
// 1. System Defaults
// 2. User Settings
// 3. Workspace Settings
// 4. System Settings (as overrides)
// 1. Schema Defaults (Built-in)
// 2. System Defaults
// 3. User Settings
// 4. Workspace Settings
// 5. System Settings (as overrides)
return customDeepMerge(
getMergeStrategyForPath,
{}, // Start with an empty object
schemaDefaults,
systemDefaults,
user,
safeWorkspace,
@@ -455,14 +244,14 @@ export class LoadedSettings {
user: SettingsFile,
workspace: SettingsFile,
isTrusted: boolean,
migratedInMemoryScopes: Set<SettingScope>,
errors: SettingsError[] = [],
) {
this.system = system;
this.systemDefaults = systemDefaults;
this.user = user;
this.workspace = workspace;
this.isTrusted = isTrusted;
this.migratedInMemoryScopes = migratedInMemoryScopes;
this.errors = errors;
this._merged = this.computeMergedSettings();
}
@@ -471,7 +260,7 @@ export class LoadedSettings {
readonly user: SettingsFile;
readonly workspace: SettingsFile;
readonly isTrusted: boolean;
readonly migratedInMemoryScopes: Set<SettingScope>;
readonly errors: SettingsError[];
private _merged: Settings;
@@ -510,6 +299,7 @@ export class LoadedSettings {
setNestedProperty(settingsFile.originalSettings, key, value);
this._merged = this.computeMergedSettings();
saveSettings(settingsFile);
coreEvents.emitSettingsChanged();
}
}
@@ -620,7 +410,6 @@ export function loadSettings(
const settingsErrors: SettingsError[] = [];
const systemSettingsPath = getSystemSettingsPath();
const systemDefaultsPath = getSystemDefaultsPath();
const migratedInMemoryScopes = new Set<SettingScope>();
// Resolve paths to their canonical representation to handle symlinks
const resolvedWorkspaceDir = path.resolve(workspaceDir);
@@ -641,10 +430,7 @@ export function loadSettings(
workspaceDir,
).getWorkspaceSettingsPath();
const loadAndMigrate = (
filePath: string,
scope: SettingScope,
): { settings: Settings; rawJson?: string } => {
const load = (filePath: string): { settings: Settings; rawJson?: string } => {
try {
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
@@ -658,77 +444,49 @@ export function loadSettings(
settingsErrors.push({
message: 'Settings file is not a valid JSON object.',
path: filePath,
severity: 'error',
});
return { settings: {} };
}
let settingsObject = rawSettings as Record<string, unknown>;
if (needsMigration(settingsObject)) {
const migratedSettings = migrateSettingsToV2(settingsObject);
if (migratedSettings) {
if (MIGRATE_V2_OVERWRITE) {
try {
fs.renameSync(filePath, `${filePath}.orig`);
fs.writeFileSync(
filePath,
JSON.stringify(migratedSettings, null, 2),
'utf-8',
);
} catch (e) {
coreEvents.emitFeedback(
'error',
'Failed to migrate settings file.',
e,
);
}
} else {
migratedInMemoryScopes.add(scope);
}
settingsObject = migratedSettings;
}
}
const settingsObject = rawSettings as Record<string, unknown>;
// Validate settings structure with Zod after migration
// Validate settings structure with Zod
const validationResult = validateSettings(settingsObject);
if (!validationResult.success && validationResult.error) {
const errorMessage = formatValidationError(
validationResult.error,
filePath,
);
throw new FatalConfigError(errorMessage);
settingsErrors.push({
message: errorMessage,
path: filePath,
severity: 'warning',
});
}
return { settings: settingsObject as Settings, rawJson: content };
}
} catch (error: unknown) {
// Preserve FatalConfigError with formatted validation messages
if (error instanceof FatalConfigError) {
throw error;
}
settingsErrors.push({
message: getErrorMessage(error),
path: filePath,
severity: 'error',
});
}
return { settings: {} };
};
const systemResult = loadAndMigrate(systemSettingsPath, SettingScope.System);
const systemDefaultsResult = loadAndMigrate(
systemDefaultsPath,
SettingScope.SystemDefaults,
);
const userResult = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User);
const systemResult = load(systemSettingsPath);
const systemDefaultsResult = load(systemDefaultsPath);
const userResult = load(USER_SETTINGS_PATH);
let workspaceResult: { settings: Settings; rawJson?: string } = {
settings: {} as Settings,
rawJson: undefined,
};
if (realWorkspaceDir !== realHomeDir) {
workspaceResult = loadAndMigrate(
workspaceSettingsPath,
SettingScope.Workspace,
);
workspaceResult = load(workspaceSettingsPath);
}
const systemOriginalSettings = structuredClone(systemResult.settings);
@@ -779,10 +537,10 @@ export function loadSettings(
// the settings to avoid a cycle
loadEnvironment(tempMergedSettings);
// Create LoadedSettings first
if (settingsErrors.length > 0) {
const errorMessages = settingsErrors.map(
// Check for any fatal errors before proceeding
const fatalErrors = settingsErrors.filter((e) => e.severity === 'error');
if (fatalErrors.length > 0) {
const errorMessages = fatalErrors.map(
(error) => `Error in ${error.path}: ${error.message}`,
);
throw new FatalConfigError(
@@ -816,36 +574,10 @@ export function loadSettings(
rawJson: workspaceResult.rawJson,
},
isTrusted,
migratedInMemoryScopes,
settingsErrors,
);
}
export function migrateDeprecatedSettings(
loadedSettings: LoadedSettings,
extensionManager: ExtensionManager,
): void {
const processScope = (scope: LoadableSettingScope) => {
const settings = loadedSettings.forScope(scope).settings;
if (settings.extensions?.disabled) {
debugLogger.log(
`Migrating deprecated extensions.disabled settings from ${scope} settings...`,
);
for (const extension of settings.extensions.disabled ?? []) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
extensionManager.disableExtension(extension, scope);
}
const newExtensionsValue = { ...settings.extensions };
newExtensionsValue.disabled = undefined;
loadedSettings.setValue(scope, 'extensions', newExtensionsValue);
}
};
processScope(SettingScope.User);
processScope(SettingScope.Workspace);
}
export function saveSettings(settingsFile: SettingsFile): void {
try {
// Ensure the directory exists
@@ -854,12 +586,7 @@ export function saveSettings(settingsFile: SettingsFile): void {
fs.mkdirSync(dirPath, { recursive: true });
}
let settingsToSave = settingsFile.originalSettings;
if (!MIGRATE_V2_OVERWRITE) {
settingsToSave = migrateSettingsToV1(
settingsToSave as Record<string, unknown>,
) as Settings;
}
const settingsToSave = settingsFile.originalSettings;
// Use the format-preserving update function
updateSettingsFilePreservingFormat(
@@ -874,3 +601,18 @@ export function saveSettings(settingsFile: SettingsFile): void {
);
}
}
export function saveModelChange(
loadedSettings: LoadedSettings,
model: string,
): void {
try {
loadedSettings.setValue(SettingScope.User, 'model.name', model);
} catch (error) {
coreEvents.emitFeedback(
'error',
'There was an error saving your preferred model.',
error,
);
}
}
+20 -12
View File
@@ -352,22 +352,30 @@ describe('SettingsSchema', () => {
expect(setting.default).toBe(false);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(false);
expect(setting.description).toBe('Enable local and remote subagents.');
expect(setting.description).toBe(
'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents',
);
});
it('should have isModelAvailabilityServiceEnabled setting in schema', () => {
const setting =
getSettingsSchema().experimental.properties
.isModelAvailabilityServiceEnabled;
it('should have hooks.notifications setting in schema', () => {
const setting = getSettingsSchema().hooks.properties.notifications;
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental');
expect(setting.default).toBe(false);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(false);
expect(setting.description).toBe(
'Enable model routing using new availability service.',
);
expect(setting.category).toBe('Advanced');
expect(setting.default).toBe(true);
expect(setting.showInDialog).toBe(true);
});
it('should have name and description in hook definitions', () => {
const hookDef = SETTINGS_SCHEMA_DEFINITIONS['HookDefinitionArray'];
expect(hookDef).toBeDefined();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hookItemProperties = (hookDef as any).items.properties.hooks.items
.properties;
expect(hookItemProperties.name).toBeDefined();
expect(hookItemProperties.name.type).toBe('string');
expect(hookItemProperties.description).toBeDefined();
expect(hookItemProperties.description.type).toBe('string');
});
});
+238 -41
View File
@@ -19,7 +19,7 @@ import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
DEFAULT_MODEL_CONFIGS,
GEMINI_MODEL_ALIAS_PRO,
GEMINI_MODEL_ALIAS_AUTO,
} from '@google/gemini-cli-core';
import type { CustomTheme } from '../ui/themes/theme.js';
import type { SessionRetentionSettings } from './settings.js';
@@ -953,6 +953,16 @@ const SETTINGS_SCHEMA = {
'The maximum time in seconds allowed without output from the shell command. Defaults to 5 minutes.',
showInDialog: false,
},
enableShellOutputEfficiency: {
type: 'boolean',
label: 'Enable Shell Output Efficiency',
category: 'Tools',
requiresRestart: false,
default: true,
description:
'Enable shell output efficiency optimizations for better performance.',
showInDialog: false,
},
},
},
autoAccept: {
@@ -1063,26 +1073,14 @@ const SETTINGS_SCHEMA = {
description: 'The number of lines to keep when truncating tool output.',
showInDialog: true,
},
enableMessageBusIntegration: {
type: 'boolean',
label: 'Enable Message Bus Integration',
category: 'Tools',
requiresRestart: true,
default: true,
description: oneLine`
Enable policy-based tool confirmation via message bus integration.
When enabled, tools automatically respect policy engine decisions (ALLOW/DENY/ASK_USER) without requiring individual tool implementations.
`,
showInDialog: true,
},
enableHooks: {
type: 'boolean',
label: 'Enable Hooks System',
label: 'Enable Hooks System (Experimental)',
category: 'Advanced',
requiresRestart: true,
default: false,
default: true,
description:
'Enable the hooks system for intercepting and customizing Gemini CLI behavior. When enabled, hooks configured in settings will execute at appropriate lifecycle events (BeforeTool, AfterTool, BeforeModel, etc.). Requires MessageBus integration.',
'Enables the hooks system experiment. When disabled, the hooks system is completely deactivated regardless of other settings.',
showInDialog: false,
},
},
@@ -1128,15 +1126,6 @@ const SETTINGS_SCHEMA = {
},
},
},
useSmartEdit: {
type: 'boolean',
label: 'Use Smart Edit',
category: 'Advanced',
requiresRestart: false,
default: true,
description: 'Enable the smart-edit tool instead of the replace tool.',
showInDialog: false,
},
useWriteTodos: {
type: 'boolean',
label: 'Use WriteTodos',
@@ -1164,6 +1153,16 @@ const SETTINGS_SCHEMA = {
description: 'Disable YOLO mode, even if enabled by a flag.',
showInDialog: true,
},
enablePermanentToolApproval: {
type: 'boolean',
label: 'Allow Permanent Tool Approval',
category: 'Security',
requiresRestart: false,
default: false,
description:
'Enable the "Allow for all future sessions" option in tool confirmation dialogs.',
showInDialog: true,
},
blockGitExtensions: {
type: 'boolean',
label: 'Blocks extensions from Git',
@@ -1193,6 +1192,48 @@ const SETTINGS_SCHEMA = {
},
},
},
environmentVariableRedaction: {
type: 'object',
label: 'Environment Variable Redaction',
category: 'Security',
requiresRestart: false,
default: {},
description: 'Settings for environment variable redaction.',
showInDialog: false,
properties: {
allowed: {
type: 'array',
label: 'Allowed Environment Variables',
category: 'Security',
requiresRestart: true,
default: [] as string[],
description:
'Environment variables to always allow (bypass redaction).',
showInDialog: false,
items: { type: 'string' },
},
blocked: {
type: 'array',
label: 'Blocked Environment Variables',
category: 'Security',
requiresRestart: true,
default: [] as string[],
description: 'Environment variables to always redact.',
showInDialog: false,
items: { type: 'string' },
},
enabled: {
type: 'boolean',
label: 'Enable Environment Variable Redaction',
category: 'Security',
requiresRestart: true,
default: false,
description:
'Enable redaction of environment variables that may contain secrets.',
showInDialog: true,
},
},
},
auth: {
type: 'object',
label: 'Authentication',
@@ -1301,7 +1342,8 @@ const SETTINGS_SCHEMA = {
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable local and remote subagents.',
description:
'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents',
showInDialog: false,
},
extensionManagement: {
@@ -1323,15 +1365,6 @@ const SETTINGS_SCHEMA = {
'Enables extension loading/unloading within the CLI session.',
showInDialog: false,
},
isModelAvailabilityServiceEnabled: {
type: 'boolean',
label: 'Enable Model Availability Service',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable model routing using new availability service.',
showInDialog: false,
},
jitContext: {
type: 'boolean',
label: 'JIT Context Loading',
@@ -1341,6 +1374,15 @@ const SETTINGS_SCHEMA = {
description: 'Enable Just-In-Time (JIT) context loading.',
showInDialog: false,
},
skills: {
type: 'boolean',
label: 'Agent Skills',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable Agent Skills (experimental).',
showInDialog: true,
},
codebaseInvestigatorSettings: {
type: 'object',
label: 'Codebase Investigator Settings',
@@ -1394,13 +1436,43 @@ const SETTINGS_SCHEMA = {
label: 'Model',
category: 'Experimental',
requiresRestart: true,
default: GEMINI_MODEL_ALIAS_PRO,
default: GEMINI_MODEL_ALIAS_AUTO,
description:
'The model to use for the Codebase Investigator agent.',
showInDialog: false,
},
},
},
useOSC52Paste: {
type: 'boolean',
label: 'Use OSC 52 Paste',
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions).',
showInDialog: true,
},
introspectionAgentSettings: {
type: 'object',
label: 'Introspection Agent Settings',
category: 'Experimental',
requiresRestart: true,
default: {},
description: 'Configuration for Introspection Agent.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Introspection Agent',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable the Introspection Agent.',
showInDialog: true,
},
},
},
},
},
@@ -1439,6 +1511,29 @@ const SETTINGS_SCHEMA = {
},
},
skills: {
type: 'object',
label: 'Skills',
category: 'Advanced',
requiresRestart: true,
default: {},
description: 'Settings for agent skills.',
showInDialog: false,
properties: {
disabled: {
type: 'array',
label: 'Disabled Skills',
category: 'Advanced',
requiresRestart: true,
default: [] as string[],
description: 'List of disabled skills.',
showInDialog: false,
items: { type: 'string' },
mergeStrategy: MergeStrategy.UNION,
},
},
},
hooks: {
type: 'object',
label: 'Hooks',
@@ -1449,6 +1544,16 @@ const SETTINGS_SCHEMA = {
'Hook configurations for intercepting and customizing agent behavior.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Hooks',
category: 'Advanced',
requiresRestart: false,
default: false,
description:
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
showInDialog: false,
},
disabled: {
type: 'array',
label: 'Disabled Hooks',
@@ -1464,6 +1569,15 @@ const SETTINGS_SCHEMA = {
},
mergeStrategy: MergeStrategy.UNION,
},
notifications: {
type: 'boolean',
label: 'Hook Notifications',
category: 'Advanced',
requiresRestart: false,
default: true,
description: 'Show visual indicators when hooks are executing.',
showInDialog: true,
},
BeforeTool: {
type: 'array',
label: 'Before Tool Hooks',
@@ -1604,6 +1718,74 @@ const SETTINGS_SCHEMA = {
mergeStrategy: MergeStrategy.CONCAT,
},
},
admin: {
type: 'object',
label: 'Admin',
category: 'Admin',
requiresRestart: false,
default: {},
description: 'Settings configured remotely by enterprise admins.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
properties: {
secureModeEnabled: {
type: 'boolean',
label: 'Secure Mode Enabled',
category: 'Admin',
requiresRestart: false,
default: false,
description: 'If true, disallows yolo mode from being used.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
},
extensions: {
type: 'object',
label: 'Extensions Settings',
category: 'Admin',
requiresRestart: false,
default: {},
description: 'Extensions-specific admin settings.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
properties: {
enabled: {
type: 'boolean',
label: 'Extensions Enabled',
category: 'Admin',
requiresRestart: false,
default: true,
description:
'If false, disallows extensions from being installed or used.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
},
},
},
mcp: {
type: 'object',
label: 'MCP Settings',
category: 'Admin',
requiresRestart: false,
default: {},
description: 'MCP-specific admin settings.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
properties: {
enabled: {
type: 'boolean',
label: 'MCP Enabled',
category: 'Admin',
requiresRestart: false,
default: true,
description: 'If false, disallows MCP servers from being used.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
},
},
},
},
},
} as const satisfies SettingsSchema;
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
@@ -1640,7 +1822,8 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
},
url: {
type: 'string',
description: 'SSE transport URL.',
description:
'URL for SSE or HTTP transport. Use with "type" field to specify transport type.',
},
httpUrl: {
type: 'string',
@@ -1655,6 +1838,12 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
type: 'string',
description: 'TCP address for websocket transport.',
},
type: {
type: 'string',
description:
'Transport type. Use "stdio" for local command, "sse" for Server-Sent Events, or "http" for Streamable HTTP.',
enum: ['stdio', 'sse', 'http'],
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds for MCP requests.',
@@ -1899,6 +2088,10 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
type: 'object',
description: 'Individual hook configuration.',
properties: {
name: {
type: 'string',
description: 'Unique identifier for the hook.',
},
type: {
type: 'string',
description:
@@ -1909,6 +2102,10 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
description:
'Shell command to execute. Receives JSON input via stdin and returns JSON output via stdout.',
},
description: {
type: 'string',
description: 'A description of the hook.',
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds for hook execution.',
@@ -1939,8 +2136,8 @@ type InferSettings<T extends SettingsSchema> = {
export type Settings = InferSettings<SettingsSchemaType>;
export interface FooterSettings {
hideCWD?: boolean;
hideSandboxStatus?: boolean;
hideModelInfo?: boolean;
export function getEnableHooks(settings: Settings): boolean {
return (
(settings.tools?.enableHooks ?? true) && (settings.hooks?.enabled ?? false)
);
}
@@ -150,7 +150,7 @@ describe('Settings Repro', () => {
showColor: true,
enableInteractiveShell: true,
},
enableMessageBusIntegration: true,
truncateToolOutputLines: 100,
},
experimental: {
useModelRouter: false,

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