google-gemini/gemini-cli
github.com/google-gemini/gemini-cli ↗ · 2974 files · 25.0 MB
Gemini CLI Gemini CLI is an open-source AI agent that brings the power of Gemini directly into your terminal.
Overview
Gemini CLI is Google's open-source terminal agent for the Gemini models: an npm-workspaces monorepo whose main artifact is the `gemini` binary (packages/cli on Ink/React for the TUI, packages/core for the agent loop, tools, policy engine, MCP client, and telemetry). It runs a two-process startup (a lightweight parent that respawns a heavy child with tuned V8 flags), supports interactive and non-interactive/headless modes, an ACP stdio bridge for editors, an A2A HTTP server, an SDK package, and a VS Code companion extension. Its distinctive features are a TOML-driven policy engine gating every tool call, a hook system that can intercept agent/model/tool lifecycle events, and git-checkpointing plus JSONL session recording for rewind/resume.
System map
The repo is an npm workspaces monorepo (Node >=20, ESM, TypeScript 5.8) with the runtime split between packages/cli (Ink/React TUI, arg parsing, settings, extensions, sandboxing, ACP) and packages/core (agentic loop, tool registry, scheduler, policy engine, hooks, MCP client, telemetry). Secondary packages are a2a-server (Express A2A endpoint), sdk (programmatic sessions), vscode-ide-companion (IDE bridge), devtools (Vite React activity viewer), and test-utils. The shipped binary is produced by esbuild into bundle/gemini.js via the root `bundle` script. Runtime state lives under ~/.gemini (or GEMINI_CLI_HOME) via the Storage class; external integrations include the Gemini API / Code Assist / Vertex AI backends, arbitrary MCP servers, IDE companions over a local server, and git for worktrees/checkpointing.
- The published `gemini` bin is bundle/gemini.js, built by `npm run bundle` which runs esbuild.config.js after generating git-commit info and bundling devtools; per-package `npm run build` uses scripts/build_package.js.
- The true process entrypoint is packages/cli/index.ts run(): unless GEMINI_CLI_NO_RELAUNCH or SANDBOX is set it stays a lightweight parent that spawns `process.execPath` with IPC for admin-settings messages and loops on RELAUNCH_EXIT_CODE; only the child dynamically imports src/gemini.js main().
- packages/cli/src/gemini.tsx main() is the heavy child orchestrator: patchStdio, unhandled-rejection/uncaught-exception handlers, loadSettings, optional worktree setup (process.chdir), parseArguments (yargs), resolveSessionId, loadCliConfig, refreshAuth, optional sandbox relaunch, then interactive UI or runNonInteractive.
- packages/core exposes its surface through src/index.ts; the Config class (config.ts, ~4200 lines) is the central god-object implementing McpContext and AgentLoopContext and owning the ToolRegistry, GeminiClient, McpClientManager, PolicyEngine, MessageBus, Storage, and HookSystem.
- Config._initialize() wires the runtime in order: storage, workspace context dirs, PromptRegistry/ResourceRegistry, AgentRegistry, createToolRegistry(), McpClientManager (started non-blockingly unless non-interactive/ACP), skills discovery, HookSystem, MemoryContextManager, and geminiClient.initialize().
- The agentic core is GeminiClient.sendMessageStream → Turn.run → GeminiChat.sendMessageStream, yielding typed ServerGeminiStreamEvent values (Content, Thought, ToolCallRequest, Finished, LoopDetected, InvalidStream, AgentExecutionStopped/Blocked, ...).
- Model access is abstracted behind the ContentGenerator interface; createContentGenerator() picks FakeContentGenerator (test replay), Code Assist (OAuth/ADC via createCodeAssistContentGenerator wrapped in ModelMappingContentGenerator+LoggingContentGenerator), or @google/genai GoogleGenAI for API-key/Vertex/Gateway auth, with proxy agents, custom headers, and Vertex REP endpoints for us/eu.
- Cross-layer signaling uses two buses: coreEvents, a process-wide EventEmitter with a CoreEvent enum (UserFeedback, Output, ConsoleLog, ModelChanged, ConsentRequest, ...), and a MessageBus (confirmation-bus) that carries tool-confirmation and policy-update messages to the UI.
- Persistent state is rooted at ~/.gemini (GEMINI_DIR, overridable via GEMINI_CLI_HOME or tmpdir on some paths) managed by the Storage class; per-project data is namespaced by a hash of the project root.
- packages/a2a-server is a separate Express app exposing an A2A-protocol agent card ('Gemini SDLC Agent') with bearer/basic auth schemes and pluggable TaskStore (InMemory/GCS/NoOp), reusing core via @google/gemini-cli-core.
- packages/sdk re-exports agent/session/tool/skills/fs helpers as a programmatic API, and packages/vscode-ide-companion runs an ide-server plus diff-manager/open-files-manager that the core connects to via IdeClient.
- OS sandboxing is a first-class runtime boundary: the CLI can relaunch itself inside Docker/Podman images or macOS Seatbelt profiles (BUILTIN_SEATBELT_PROFILES/*.sb files) before doing expensive init, and per-command sandboxing is mediated by sandboxManager.prepareCommand in tools.
Feature catalog
User-facing surface is broad: a yargs-driven CLI with subcommands (mcp, extensions, skills, hooks, gemma) plus ~45 built-in slash commands, custom TOML commands, and MCP-prompt commands. The agent can call ~20 built-in tools (file IO, shell, grep/glob/ripgrep, web fetch/search, todos, plan mode, tracker tasks, skills, subagents incl. a browser agent). Auth supports Google OAuth, Gemini API key, Vertex AI, compute ADC, and gateway modes. Features include session resume/checkpointing/rewind, GEMINI.md hierarchical memory, hooks, policies, extensions, skills, ACP editor mode, A2A server, voice mode, IDE integration, worktrees, and multiple output formats (text/json/stream-json).
- CLI flags are defined with yargs in parseArguments: --model, --prompt/-p (headless), --prompt-interactive/-i, --sandbox, --yolo, --approval-mode, --policy, --admin-policy, --acp, --allowed-tools, --allowed-mcp-server-names, --extensions, --resume, --session-id, --session-file, --list-sessions, --delete-session, --include-directories, --output-format, --fake-responses, --record-responses, --raw-output, plus positional query.
- Five yargs subcommands are registered alongside the default '$0 [query..]' launcher: mcpCommand, extensionsCommand, skillsCommand, hooksCommand, gemmaCommand.
- BuiltinCommandLoader registers ~45 slash commands (/about /agents /auth /bug /chat /clear /compress /copy /directory /docs /editor /extensions /help /hooks /ide /init /mcp /memory /model /permissions /plan /policies /privacy /quit /restore /resume /stats /theme /tools /skills /settings /tasks /vim /rewind ...), many gated by config flags such as isAgentsEnabled, isPlanEnabled, isSkillsSupportEnabled, getFolderTrust, or nightly/dev builds.
- Config.createToolRegistry registers the built-in tool set conditionally via maybeRegister (honoring the coreTools allowlist): UpdateTopic, LS, ReadFile, RipGrep-or-Grep fallback, Glob, ActivateSkill, Edit, WriteFile, WebFetch, ReadMcpResource, ListMcpResources, Shell, background-process tools, WebSearch, AskUser, WriteTodos, Enter/ExitPlanMode, Tracker* tools, and AgentTool.
- Six AuthType values are supported (oauth-personal LOGIN_WITH_GOOGLE, gemini-api-key, vertex-ai, legacy cloud-shell, compute-default-credentials, gateway); getAuthTypeFromEnv auto-detects via GOOGLE_GENAI_USE_GCA, GOOGLE_GENAI_USE_VERTEXAI, GOOGLE_GEMINI_BASE_URL, GEMINI_API_KEY, CLOUD_SHELL/GEMINI_CLI_USE_COMPUTE_ADC.
- Sessions are recorded as JSONL files named session-<ts>-<id>.jsonl under the project's temp 'chats' dir; they can be resumed via --resume/--session-id, imported via --session-file (filtering to user/gemini messages), listed via --list-sessions, and deleted via --delete-session.
- Custom slash commands are user-defined TOML files (required 'prompt', optional 'description' per TomlCommandDefSchema) scanned from user, project, and extension command dirs by FileCommandLoader, with !{...} shell injection and @file injection prompt processors that can require confirmation.
- MCP servers from settings/extensions are connected by McpClientManager during Config._initialize; MCP prompts become slash commands via McpPromptLoader and MCP tools enter the ToolRegistry as DiscoveredMCPTool instances.
- An extension system (ExtensionManager) installs/loads extensions from git clones or GitHub releases, hydrates variables, manages consent and per-extension settings/env files, contributes MCP servers, commands, agents, skills, hooks, themes, and TOML policies, and enforces admin allowlists and integrity checks.
- A hook system (HookSystem/HookRegistry/HookRunner/HookAggregator/HookPlanner) fires lifecycle events — SessionStart/End, Before/AfterAgent, Before/AfterModel, BeforeToolSelection, Before/AfterTool — whose outputs can inject context, block/stop execution, synthesize model responses, or modify tool/config.
- Subagents are a feature: AgentRegistry loads local agent definitions (built-ins include CodebaseInvestigatorAgent, CliHelpAgent, GeneralistAgent, BrowserAgentDefinition) plus remote A2A agents, exposes them through AgentTool, and supports hot reload with new/updated/deleted summaries.
- ACP mode (--acp) runs the CLI as an Agent Client Protocol stdio server via runAcpClient with session management, RPC dispatch, and a filesystem bridge for editor clients such as Zed.
Data flow
Input enters via argv/stdin, is merged with layered settings.json files (system → user → workspace, plus remote admin controls over IPC), then Config becomes the single state owner for the session. A prompt flows through slash-command and @-file processors into Part[] arrays, into GeminiClient.sendMessageStream → Turn → GeminiChat → ContentGenerator → the Gemini/Code Assist/Vertex HTTP stream. Streamed chunks become typed events; ToolCallRequest events are drained into the Scheduler which runs policy checks, user confirmation over the MessageBus, hooks, then the ToolExecutor; tool results are appended as functionResponse parts and recorded into chat history plus the JSONL session file, looping until a Finished/Stopped event. Output exits via coreEvents.Output/TextOutput/StreamJsonFormatter or the Ink UI.
- Settings data flow: loadSettings() reads layered settings.json (system path /etc/gemini-cli or OS equivalents, user ~/.gemini/settings.json, workspace .gemini/settings.json), interpolates env vars (resolveEnvVarsInObject), merges with customDeepMerge per schema mergeStrategy, and validates against the generated settingsSchema.
- Remote admin/enterprise controls flow over Node IPC: the parent process forwards {type:'admin-settings'} messages to the child, which calls config.setRemoteAdminSettings; the child pushes 'admin-settings-update' back after refreshAuth fetches CCPA admin settings.
- Prompt ingress in headless mode: stdin (readStdin) is prepended to --prompt input, SessionStart hook context is wrapped in <hook_context> tags and prepended, then slash commands (isSlashCommand → handleSlashCommand) or @-commands (handleAtCommand) convert text into Part[] before entering the loop.
- Per turn, GeminiClient.sendMessageStream resets turn state, runs BeforeAgent hooks (can stop/block/inject additionalContext), then delegates to Turn.run which streams GeminiChat responses; GeminiChat wraps the API call in retryWithBackoff and raises InvalidStreamError for empty/malformed/safety-blocked streams.
- Tool-call data flow: resp.functionCalls become ToolCallRequestInfo via Turn.handlePendingFunctionCall → ToolCallRequest events; the consumer collects them and calls Scheduler.schedule, which runs checkPolicy/policy engine, resolveConfirmation over the MessageBus, BeforeTool hooks (evaluateBeforeToolHook), ToolModificationHandler, then ToolExecutor, producing CompletedToolCall with ToolCallResponseInfo.
- Tool responses re-enter the model loop as functionResponse parts appended to currentMessages; recordCompletedToolCalls persists metadata into GeminiChat history and recordToolCallInteractions logs telemetry before the next sendMessageStream iteration, bounded by getMaxSessionTurns().
- Policy decisions are computed by a tiered TOML rule engine: directories ordered Default → Extension → Workspace → User → Admin map to priority tiers 1-5 with fractional offsets for flags like --allowed-tools, coreTools, and trusted/allowed MCP servers; rules yield PolicyDecision allow/ask_user/deny consumed by the scheduler.
- Session persistence is an append-only JSONL transcript: ConversationRecord header line followed by MessageRecord/MetadataUpdateRecord/RewindRecord lines written to ~/.gemini/tmp/<projectHash>/chats/session-*.jsonl by ChatRecordingService, with ENOSPC graceful degradation.
- The system prompt is assembled by getCoreSystemPrompt(config, userMemory) via a PromptProvider, combining environment context (getDirectoryContextString), GEMINI.md memory, and model-family-specific tool declarations resolved per model in tools/definitions.
- Context-window management: token counts are estimated per request (calculateRequestTokenCount, tokenLimit) and ChatCompressionService compresses history (emitting ChatCompressed events); LoopDetectionService watches for repetitive turns and yields LoopDetected; ContextManager/AgentHistoryProvider/ToolOutputMaskingService manage what history the model sees.
- Output egress is centralized through patchStdio + coreEvents: initializeOutputListenersAndFlush drains backlogged Output/ConsoleLog/UserFeedback events to stdout/stderr, forcing stdout payloads to stderr when outputFormat is 'json' so machine output stays parseable; ConsolePatcher reroutes console.* into the same bus for the UI.
- IDE context flows in from the vscode-ide-companion over a local connection managed by IdeClient into ideContextStore; GeminiClient diffs lastSentIdeContext vs current (forceFullIdeContext flag) to inject file/selection context into requests. (inferred)
Behavior traces
Traced three flows end-to-end. (1) Startup: parent spawn → settings → args → session resolution → partial config → auth → optional sandbox relaunch → full config init → interactive Ink UI or headless loop. (2) Headless agentic turn: input normalization → hooks → GeminiClient stream → Turn/GeminiChat → tool requests → Scheduler (policy→confirm→hooks→execute) → responses fed back until Finished. (3) Session persistence/resume: JSONL transcript written per message/tool call and rehydrated via SessionSelector or imported from file.
- Startup trace (parent): `gemini` bin → packages/cli/index.ts run() → getMemoryNodeArgs() reads ~/.gemini/settings.json for advanced.autoConfigureMemory → getSpawnConfig() → spawn child node with same args + IPC channel → forward admin-settings → on child close, exit with code unless code === RELAUNCH_EXIT_CODE, then respawn.
- Startup trace (child): gemini.tsx main() → setupAdminControlsListener → patchStdio → setupUnhandledRejectionHandler/setupSignalHandlers → SlashCommandConflictHandler.start → loadSettings → optional setupWorktree → parseArguments → resolveSessionId → ConsolePatcher.patch → loadCliConfig (partial, skipExtensions) → validateAuthMethod/validateNonInteractiveAuth → partialConfig.refreshAuth → remote admin settings propagation.
- Startup trace (sandbox hop): if SANDBOX env is unset and not a subcommand, loadSandboxConfig decides; when configured, piped stdin is read and injected into argv as --prompt, then relaunchOnExitCode(start_sandbox(...)) re-executes the CLI inside Docker/Podman/Seatbelt and the outer process exits SUCCESS.
- Startup trace (post-sandbox): full loadCliConfig → config.storage.initialize → createPolicyUpdater(policyEngine, messageBus, storage) → registerCleanup(SessionEnd hook + ConsolePatcher) → flag handlers (--list-extensions/--list-sessions/--delete-session exit early) → setRawMode → setupTerminalAndTheme → initializeApp(config, settings) which auths, connects IdeClient, validates theme, counts GEMINI.md files → startInteractiveUI dynamically imports interactiveCli.js (React/Ink lazy-loaded).
- Headless turn trace: runNonInteractive → promptIdContext.run → ConsolePatcher + TextOutput + Ctrl+C readline keypress abort → new Scheduler({context: config, messageBus}) → optional geminiClient.resumeChat → handleSlashCommand/handleAtCommand → loop: geminiClient.sendMessageStream(parts, signal, prompt_id) → for-await events → collect ToolCallRequest → scheduler.schedule → completedToolCalls → append responseParts to history → next iteration until no tool calls / Finished / stopped.
- Model-stream trace inside core: sendMessageStream → fireBeforeAgentHookSafe (may yield AgentExecutionStopped/Blocked or inject <hook_context>) → processTurn loop → Turn.run → chat.sendMessageStream(modelConfigKey, req, prompt_id, signal, role) → retryWithBackoff around apiCall → per-chunk parse: thought parts → Thought events, text → Content, functionCalls → ToolCallRequest, finishReason → Finished(+citations), citations tracked across chunks.
- Tool-execution trace inside Scheduler: schedule() enqueues SchedulerQueueItem → SchedulerStateManager tracks call lifecycle (validating → awaiting approval → executing → success/error) → checkPolicy consults PolicyEngine (deny short-circuits) → resolveConfirmation publishes ToolConfirmationRequest on the MessageBus for UI approval → evaluateBeforeToolHook may block/modify → ToolExecutor runs the ToolInvocation → ToolCallEvent telemetry logged → CompletedToolCall resolved. (inferred)
- Shell tool trace: model calls run_shell_command → ShellToolInvocation → command parsing via initializeShellParsers/getCommandRoots/stripShellWrapper/detectCommandSubstitution → policy check on root commands → confirmation showing command+roots → ShellExecutionService.execute prefers node-pty (getPty) with childProcessFallback → streamed ShellOutputEvents, live output buffer trimmed at 100k chars, background commands wrapped with EXIT-trap PID capture.
- Session resume trace: --resume → SessionSelector(storage).resolveSession picks or lists JSONL transcripts → ResumedSessionData {conversation, filePath} → in headless mode geminiClient.resumeChat(convertSessionToClientHistory(messages)) rehydrates GeminiChat history; --session-file instead imports an external record, filters to user/gemini messages, mints a new sessionId, and writes a fresh session-*.jsonl.
- AfterAgent continuation trace: when hooks are enabled and no pending tools, fireAfterAgentHookSafe runs; a blocking decision yields AgentExecutionBlocked, decrements hookState.activeCalls, then recursively calls sendMessageStream with the hook's reason as the new request and stopHookActive=true — an internal agentic continuation invisible to the UI loop.
- Interactive UI trace: startInteractiveUI → interactiveCli.js → Ink renders AppContainer (~2900 lines) → useGeminiStream hook drives geminiClient.sendMessageStream and owns Scheduler wiring plus slashCommandProcessor for / commands inside the TUI.
Testing signals
Testing is heavy and layered: Vitest 3.2 with colocated *.test.ts(x) unit tests across every package (a large share of scanned TS files are tests), an integration-tests/ workspace running the real built binary (optionally under docker/podman sandbox) with recorded .responses fixtures, evals/ behavioral evals gated behind RUN_EVALS with an llm-judge, plus perf-tests/ and memory-tests/ with baseline updating. CI (ci.yml) enforces lint (eslint+actionlint+shellcheck+yamllint+prettier+sensitive-keywords+action pinning), build, typecheck, per-workspace test:ci, bundle smoke (`node ./bundle/gemini.js --version`), generated-settings-doc freshness, and lockfile checks. Husky+lint-staged run prettier/eslint on commit.
- Unit tests are colocated Vitest files next to sources (e.g. client.test.ts, scheduler.test.ts, extension-manager*.test.ts, App.test.tsx); vitest.config.ts per package; `npm run test` runs --workspaces plus a sea/sea-launch.test.js binary-launch check, and posttest runs a full build.
- integration-tests/ is a separate vitest root (52+ *.test.ts files) with testTimeout 300000, retry 2, 8-16 threads, globalSetup.ts, GEMINI_TEST_TYPE=integration, and *.responses replay fixtures for browser-agent/hooks/context-compression flows; sandbox variants selected via GEMINI_SANDBOX=none|docker|podman.
- Fake/recorded model backends are first-class test hooks: --fake-responses/--fake-responses-non-strict load FakeContentGenerator from a file and --record-responses wraps the real generator in RecordingContentGenerator — this is how the .responses fixtures are produced and replayed.
- evals/ contains ~30 *.eval.ts behavioral evals (answer-vs-act, plan_mode, prompt_injection_mcp, memory_persistence, model_steering, sandbox_recovery, ...) driven by an llm-judge.ts; only 'always_passing' run by default, all evals need RUN_EVALS=1.
- perf-tests/ and memory-tests/ are dedicated vitest roots with baseline files updated via UPDATE_PERF_BASELINES/UPDATE_MEMORY_BASELINES env flags, plus memory-nightly.yml and perf-nightly.yml scheduled workflows.
- CI runs a lint battery beyond eslint: scripts/lint.js --setup/--eslint/--actionlint/--shellcheck/--yamllint/--prettier/--sensitive-keywords/--check-github-actions-pinning, then build, typecheck, settings-doc regeneration check (docs:settings --check), and lockfile integrity.
- CI test matrix splits jobs by workspace: `npm run test:ci --workspace @google/gemini-cli` on one runner and core+a2a-server+vscode-ide-companion+test-utils (with coverage disabled) on another, followed by bundle verification `node ./bundle/gemini.js --version`.
- A deflake harness exists (scripts/deflake.js + deflake.yml workflow + RUN_FLAKY_INTEGRATION flag) specifically to rerun integration suites, signalling acknowledged flaky e2e coverage.
- Test infrastructure deps include msw (HTTP mocking), mock-fs/memfs (fs virtualization), react-devtools-core, and a dedicated @google/gemini-cli-test-utils workspace; ink is replaced with a fork (@jrichman/ink) via npm overrides to make TUI testable.
- eslint-plugin-headers enforces the Google Apache license header on source files, and lint-staged runs prettier+eslint --fix on commit via husky hooks.
- Coverage gaps visible from the file tree: packages/devtools client has few colocated tests, scripts/ has a separate vitest config, and several heavy paths (sandbox.ts 1431 lines, AppContainer.tsx 2869 lines) have limited direct unit coverage relative to size — integration tests carry much of that weight. (inferred)
Risk map
The riskiest surfaces are the ones that execute or fetch code: ShellTool + ShellExecutionService (model-generated commands on the host, mitigated by policy/confirmation/sandbox), the sandbox relaunch path itself (exec/spawn of docker, podman, sandbox-exec), extension installation (git clone / GitHub release downloads, consent-gated), the toolDiscoveryCommand (arbitrary configured command whose stdout is parsed into tools), custom-command shell injection (!{...} processors), and the a2a-server which ships hardcoded demo credentials. Config.ts is a 4200-line god object and the two-process relaunch + ConsolePatcher/stdio interception are fragile coupling hotspots. Credential storage (OAuth via keychain/file fallback, apiKeyCredentialStorage, MCP oauth-token-storage) is sensitive file IO outside the repo.
- a2a-server customUserBuilder authenticates with literal hardcoded values — Bearer 'valid-token' and Basic 'admin:password' — which would accept those credentials on any deployed instance of this Express app.
- ShellTool executes model-generated shell commands; mitigations are layered (shell-utils root-command parsing for policy matching, PolicyDecision gating, user confirmation details of type 'exec', optional sandboxing via sandboxManager, untrusted-context tracking for build/test commands) but the blast radius is the user's shell.
- The sandbox path spawns external binaries (docker/podman/sandbox-exec) and rewrites argv/stdin; sandbox.ts uses exec/execSync/spawnSync heavily and manages temp seatbelt profile files — a failure here can strand processes or silently run unsandboxed if config detection misfires. (inferred)
- ToolRegistry.discoverAndRegisterToolsFromCommand spawns a user-configured toolDiscoveryCommand via spawn and JSON.parses its stdout into FunctionDeclarations; DiscoveredTool invocations then spawn toolCallCommand piping params as JSON on stdin — arbitrary local command execution is a designed-in extension surface.
- Custom TOML commands support !{...} shell injection executed through ShellProcessor, which throws ConfirmationRequiredError to force user approval — a prompt-file → shell-exec path that depends on the confirmation gate not being bypassed.
- Extension installs fetch remote code (cloneFromGit/downloadFromGitHubRelease) and extensions can contribute MCP servers, hooks, policies, and commands — supply-chain risk is gated by maybeRequestConsentOrFail, admin allowlists (applyAdminAllowlist), ExtensionIntegrityManager, and folder trust.
- Config (packages/core/src/config/config.ts, 4205 lines) concentrates ~200 getters plus lifecycle (auth refresh, tool registry, MCP startup, hooks, extensions) — edits here have the largest blast radius and the file already carries patches like minified-class-name normalization in maybeRegister.
- The uncaught-exception handler deliberately swallows specific node-pty errors ('Cannot resize a pty that has already exited', EBADF from node-pty stacks) — pragmatic, but any genuinely new fatal error signature matching those strings will also be suppressed.
- Secrets handling spans multiple stores: OAuth creds via oauth2.ts, GEMINI_API_KEY persisted in apiKeyCredentialStorage (keytar on supported platforms, with a documented Linux Secret-Service hang avoided by early return for OAuth/ADC), MCP tokens via keychain/hybrid/file storage — file-permission and keychain-availability edge cases are the sensitive surface.
- stdio is globally intercepted (patchStdio + ConsolePatcher redirecting console.* into coreEvents) and stdin is toggled into raw mode at multiple layers; ordering bugs here caused real issues — the code comments cite silent keystroke loss when stdin listeners drop to zero.
- MCP servers are spawned/connected from settings and extension manifests with OAuth flows (mcp-oauth-provider) and token storage; allowedMcpServerNames and admin policies constrain them, but a malicious or compromised MCP server can inject tool descriptions, prompts (McpPromptLoader), and resources into the model context — flagged by the prompt_injection_mcp eval. (inferred)
- --raw-output disables ANSI sanitization of model output in TEXT mode; the code itself warns this 'may contain harmful ANSI sequences (e.g. for phishing or command injection)' and requires --accept-raw-output-risk to suppress the warning.
Extension points
Almost every subsystem is a plug-in point: ICommandLoader implementations feed CommandService; ToolRegistry.registerTool plus the discovery-command and MCP paths add tools; AgentRegistry loads local .md/agent definitions and remote A2A agents; the policy engine loads TOML rule dirs per tier; hooks subscribe to named lifecycle events; extensions bundle all of the above via gemini-extension.json; skills are discovered by SkillManager and activated via ActivateSkillTool; ContentGenerator is a factory interface; and the policy engine, themes, and model-config service all accept externally supplied entries.
- Slash commands plug in through the ICommandLoader interface — BuiltinCommandLoader (hardcoded), FileCommandLoader (TOML in ~/.gemini/commands, project, extension dirs), McpPromptLoader (MCP prompts), SkillCommandLoader — merged by CommandService.create with SlashCommandResolver de-duplicating conflicts by renaming losers.
- New core tools are added by implementing AnyDeclarativeTool/BaseDeclarativeTool (build() → ToolInvocation with getDescription/shouldConfirmExecute/execute) and registering in Config.createToolRegistry's maybeRegister list, which automatically respects the coreTools allowlist.
- Tools can be added without recompiling via settings toolDiscoveryCommand (JSON function declarations on stdout) + toolCallCommand (JSON params on stdin), executed through the optional sandboxManager — a command-protocol plugin surface.
- MCP servers are the main third-party extension surface: configured in settings/extensions, connected by McpClientManager, their tools appear as DiscoveredMCPTool (MCP_TOOL_PREFIX), prompts become slash commands, and resources are exposed via ReadMcpResourceTool/ListMcpResourcesTool through registries.
- Extensions are defined by a gemini-extension.json manifest (EXTENSIONS_CONFIG_FILENAME) and can contribute MCP servers, context files, custom commands, agents, skills, hooks, themes, and TOML policies; ExtensionManager handles install/enable/disable/update and variable hydration (${extensionPath} etc.).
- The policy engine accepts user rules from multiple TOML directories per tier (default core policies in packages/core/src/policy/policies/*.toml — yolo.toml, read-only.toml, plan.toml, agents.toml etc. — plus --policy paths, workspace, user, and --admin-policy dirs) and dynamic rules pushed over the MessageBus via UpdatePolicy messages.
- Hooks are user/extension-supplied handlers for HookEventName lifecycle events (SessionStart/End, Before/AfterAgent, Before/AfterModel, BeforeToolSelection, Before/AfterTool) executed by HookRunner and aggregated by HookAggregator, capable of injecting context, modifying model calls, or blocking execution.
- Subagents plug in via AgentDefinition objects loaded by loadAgentsFromDirectory (project/user/extension dirs) or built in code; kind:'remote' agents are A2A-protocol endpoints resolved through A2AClientManager with pluggable auth via A2AAuthProviderFactory.
- Skills are markdown-based packages discovered by SkillManager from user/project/extension dirs (loadSkillsFromDir), gated by admin settings and folder trust, exposed to the model through ActivateSkillTool whose schema is re-registered with the discovered skill enum.
- Model selection is extensible: ModelConfigService resolves per-alias model configs, routing/routingStrategy.ts plus availability policyHelpers decide model per request, handleFallback switches models on quota errors, and ModelMappingContentGenerator remaps names for Code Assist (CCPA_AI_MODEL_MAPPINGS). (inferred)
- ContentGenerator is a decorated factory: Fake/Recording/Logging/ModelMapping wrappers compose around either the Code Assist server client or @google/genai — new providers slot in by extending createContentGenerator's authType switch.
- Tool declarations shown to the model are model-family aware: tools/definitions/coreTools.ts + resolver.ts + model-family-sets produce per-model FunctionDeclarations so tool schemas can be swapped per Gemini version without touching tool implementations.
Contributor handoff
An agent editing this repo should treat packages/core and packages/cli as separate layers (core must not import cli), keep the Config god-object edits minimal, route all tool gating through the PolicyEngine/MessageBus rather than ad-hoc checks, and always run the colocated vitest file plus typecheck before considering a change done. Build is `npm run bundle` (esbuild → bundle/gemini.js); quick dev loop is `npm run start` / `npm start -- -p '...'` after build. Conventions: ESM with .js import specifiers for .ts files, Apache license header enforced by eslint, debugLogger instead of console.*, proper-lockfile/proper JSONL append for session files. Traps: two-process relaunch means env changes need GEMINI_CLI_NO_RELAUNCH=1 to debug the real process; posttest triggers a full build; Ink is a forked package via npm overrides.
- Read-first files for any agentic-loop change: packages/core/src/core/client.ts (GeminiClient), turn.ts (event types Turn/GeminiEventType), geminiChat.ts (stream+retry), scheduler/scheduler.ts (tool execution), config/config.ts (wiring). CLI-side entry: packages/cli/index.ts then src/gemini.tsx main().
- Build/dev commands: `npm run bundle` produces bundle/gemini.js (esbuild, includes generated git commit info and devtools bundle); `npm run start` runs scripts/start.js with NODE_ENV=development; `npm run build` builds packages only.
- Test commands: `npm run test:ci --workspace @google/gemini-cli-core` for core unit tests; `npm run test:integration:sandbox:none` for e2e without sandbox; `npm run typecheck` across workspaces; `npm run preflight` = clean+ci+format+build+lint+typecheck+test:ci (slow, full gate).
- Imports must use ESM `.js` specifiers referencing the compiled names (e.g. './utils/cleanup.js'), the repo is "type":"module" everywhere, and every file needs the SPDX Apache header enforced by eslint-plugin-headers.
- Logging convention: never use console.log/console.error in library paths — ConsolePatcher intercepts them; use debugLogger (debug/error/warn) for diagnostics and coreEvents.emitFeedback for user-facing messages so headless JSON output stays clean.
- Tool-approval changes must go through the PolicyEngine + MessageBus confirmation flow (checkPolicy → resolveConfirmation → MessageBusType.ToolConfirmationRequest); bypassing it (e.g. auto-executing in a new tool) defeats --approval-mode/--yolo/plan-mode semantics and enterprise admin policies.
- Safe edit boundaries: new tools go in packages/core/src/tools/ + registration in config.ts createToolRegistry; new slash commands in packages/cli/src/ui/commands/ + BuiltinCommandLoader; new settings keys in settingsSchema.ts (docs regenerated via npm run docs:settings, which CI verifies with --check).
- Debugging trap: the CLI double-forks — the visible process is a thin parent; to debug or test the real logic run with GEMINI_CLI_NO_RELAUNCH=1 (or `npm run debug` which uses --inspect-brk), otherwise breakpoints attach to the wrong process and stdin/stdio patching differs.
- Trap: `npm test` at the root runs a build afterwards (posttest → npm run build) and the prepare script runs `husky && npm run bundle`, so npm install/test are not side-effect-free; the package-lock is guarded by check:lockfile in CI.
- Layering rule: packages/cli depends on @google/gemini-cli-core (file:../core) and everything cross-layer is exported through packages/core/src/index.ts; a2a-server and sdk also consume core only through that barrel — new core APIs must be re-exported there to be usable.
- Ink is pinned to a fork (@jrichman/ink@6.6.9) via npm overrides with wrap-ansi/cliui pinned too — do not upgrade ink casually; TUI tests rely on the fork's behavior (react-devtools-core in devDeps).
- Session transcript writes must remain append-only JSONL with proper record shapes (MessageRecord/$set/$rewindTo/$partial metadata guards in chatRecordingService); --resume and rewind depend on parsing these exact line types.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 2173 | 93% |
| Markdown | 170 | docs/config |
| JavaScript | 72 | 3% |
| YAML | 71 | docs/config |
| JSON | 55 | docs/config |
| Python | 49 | 2% |
| TOML | 24 | 1% |
| Shell | 10 | 0% |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (2329 files)
Repository structure
| Top-level path | Files |
|---|---|
packages/ | 2329 |
integration-tests/ | 120 |
tools/ | 117 |
docs/ | 101 |
scripts/ | 83 |
.github/ | 73 |
evals/ | 47 |
.gemini/ | 46 |
Docs found: CONTRIBUTING.mdREADME.mddocs/CONTRIBUTING.mddocs/admin/enterprise-controls.mddocs/behavioral-evals.mddocs/changelogs/index.mddocs/changelogs/latest.mddocs/changelogs/preview.mddocs/cli/acp-mode.mddocs/cli/auto-memory.md
Package
package.json — @google/gemini-cli v0.62.0-nightly.20260918.g9450ade79, 58 declared dependencies.
@agentclientprotocol/sdk@modelcontextprotocol/sdk@octokit/rest@types/express@types/marked@types/mime-types@types/minimatch@types/mock-fs@types/prompts@types/proper-lockfile@types/react@types/react-dom
CI & config entrypoints
README.md— Repository readmepackages/devtools/client/src/App.tsx— React rootpackages/devtools/client/src/main.tsx— Vite React entrypoint.github/workflows/perf-nightly.yml— GitHub Actions workflow.github/workflows/release-promote.yml— GitHub Actions workflow.github/workflows/smoke-test.yml— GitHub Actions workflow.github/workflows/gemini-self-assign-issue.yml— GitHub Actions workflow.github/workflows/links.yml— GitHub Actions workflow.github/workflows/release-manual.yml— GitHub Actions workflow.github/workflows/gemini-scheduled-issue-dedup.yml— GitHub Actions workflow
Structural health signals
Average score 6.2/10 across 96 analyzed source files; 68 hotspots surfaced.
packages/core/src/services/shellExecutionService.ts— 1549 lines, score 4.7 (Large file, Deep nesting)packages/cli/src/ui/components/triage/TriageDuplicates.tsx— 1054 lines, score 5 (Large file, Deep nesting)packages/core/src/tools/mcp-client.ts— 1585 lines, score 5.4 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
d5b3e3accb262026-09-21 — fix(cli): emit tool_call update prior to request_permission in ACP mode (#29439)04e39e5dc44f2026-09-21 — fix(core): normalize proxy-agent esbuild interop for environment proxy resolution (#29401)cfbcaa8df13e2026-09-18 — fix(core): update auth error documentation link to valid anchor and add fallback (#26140) (#29377)e5b6680332882026-09-18 — fix(vscode-ide-companion): preserve terminal focus when closing diff tabs (#29378)196c772a97012026-09-18 — fix(core,cli): improve terminal buffer memory management and format Windows diagnostic paths (#29380)