From 79f5ccea400737f96aa961826b6dacd8141996a3 Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Mon, 13 Apr 2026 10:48:54 -0400 Subject: [PATCH 1/6] fix(core): expose GEMINI_PLANS_DIR to hook environment --- docs/cli/plan-mode.md | 101 +++++++++++---------- docs/hooks/index.md | 13 +-- package-lock.json | 34 ++++++- packages/core/src/hooks/hookRunner.test.ts | 4 + packages/core/src/hooks/hookRunner.ts | 1 + 5 files changed, 96 insertions(+), 57 deletions(-) diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index f5532a07ca8..2877c523fa2 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -11,51 +11,51 @@ implementation. With Plan Mode, you can: Plan Mode is enabled by default. You can manage this setting using the `/settings` command. -## How to enter Plan Mode +## How to enter plan mode -Plan Mode integrates seamlessly into your workflow, letting you switch between +Plan mode integrates seamlessly into your workflow, letting you switch between planning and execution as needed. -You can either configure Gemini CLI to start in Plan Mode by default or enter -Plan Mode manually during a session. +You can either configure Gemini CLI to start in plan mode by default or enter +plan mode manually during a session. -### Launch in Plan Mode +### Launch in plan mode -To start Gemini CLI directly in Plan Mode by default: +To start Gemini CLI directly in plan mode by default: 1. Use the `/settings` command. -2. Set **Default Approval Mode** to `Plan`. +2. Set **Default approval mode** to `Plan`. -To launch Gemini CLI in Plan Mode once: +To launch Gemini CLI in plan mode once: 1. Use `gemini --approval-mode=plan` when launching Gemini CLI. -### Enter Plan Mode manually +### Enter plan mode manually -To start Plan Mode while using Gemini CLI: +To start plan mode while using Gemini CLI: - **Keyboard shortcut:** Press `Shift+Tab` to cycle through approval modes - (`Default` -> `Auto-Edit` -> `Plan`). Plan Mode is automatically removed from + (`Default` -> `Auto-Edit` -> `Plan`). Plan mode is automatically removed from the rotation when Gemini CLI is actively processing or showing confirmation dialogs. - **Command:** Type `/plan [goal]` in the input box. The `[goal]` is optional; - for example, `/plan implement authentication` will switch to Plan Mode and + for example, `/plan implement authentication` will switch to plan mode and immediately submit the prompt to the model. -- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI +- **Natural language:** Ask Gemini CLI to "start a plan for...". Gemini CLI calls the [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode) tool to switch modes. This tool is not available when Gemini CLI is in [YOLO mode](../reference/configuration.md#command-line-arguments). -## How to use Plan Mode +## How to use plan mode -Plan Mode lets you collaborate with Gemini CLI to design a solution before +Plan mode lets you collaborate with Gemini CLI to design a solution before Gemini CLI takes action. 1. **Provide a goal:** Start by describing what you want to achieve. Gemini CLI - will then enter Plan Mode (if it's not already) to research the task. + will then enter plan mode (if it's not already) to research the task. 2. **Discuss and agree on strategy:** As Gemini CLI analyzes your codebase, it will discuss its findings and proposed strategy with you to ensure alignment. It may ask you questions or present different implementation @@ -101,19 +101,19 @@ describing complex changes in natural language. your comments, and adjusts the implementation strategy. It then presents the refined plan for your final approval. -## How to exit Plan Mode +## How to exit plan mode -You can exit Plan Mode at any time, whether you have finalized a plan or want to +You can exit plan mode at any time, whether you have finalized a plan or want to switch back to another mode. - **Approve a plan:** When Gemini CLI presents a finalized plan, approving it - automatically exits Plan Mode and starts the implementation. + automatically exits plan mode and starts the implementation. - **Keyboard shortcut:** Press `Shift+Tab` to cycle to the desired mode. - **Natural language:** Ask Gemini CLI to "exit plan mode" or "stop planning." -## Tool Restrictions +## Tool restrictions -Plan Mode enforces strict safety policies to prevent accidental changes. +Plan mode enforces strict safety policies to prevent accidental changes. These are the only allowed tools: @@ -142,7 +142,7 @@ These are the only allowed tools: ## Customization and best practices -Plan Mode is secure by default, but you can adapt it to fit your specific +Plan mode is secure by default, but you can adapt it to fit your specific workflows. You can customize how Gemini CLI plans by using skills, adjusting safety policies, changing where plans are stored, or adding hooks. @@ -150,7 +150,7 @@ safety policies, changing where plans are stored, or adding hooks. You can use [Agent Skills](../cli/skills.md) to customize how Gemini CLI approaches planning for specific types of tasks. When a skill is activated -during Plan Mode, its specialized instructions and procedural workflows will +during plan mode, its specialized instructions and procedural workflows will guide the research, design, and planning phases. For example: @@ -162,13 +162,13 @@ For example: - A **"Frontend Design"** skill could guide Gemini CLI to use specific UI components and accessibility standards in its proposal. -To use a skill in Plan Mode, you can explicitly ask Gemini CLI to "use the +To use a skill in plan mode, you can explicitly ask Gemini CLI to "use the `` skill to plan..." or Gemini CLI may autonomously activate it based on the task description. ### Custom policies -Plan Mode's default tool restrictions are managed by the +Plan mode's default tool restrictions are managed by the [policy engine](../reference/policy-engine.md) and defined in the built-in [`plan.toml`] file. The built-in policy (Tier 1) enforces the read-only state, but you can customize these rules by creating your own policies in your @@ -179,18 +179,18 @@ but you can customize these rules by creating your own policies in your As described in the [policy engine documentation](../reference/policy-engine.md#approval-modes), any rule that does not explicitly specify `modes` is considered "always active" and -will apply to Plan Mode as well. +will apply to plan mode as well. -To maintain the integrity of Plan Mode as a safe research environment, +To maintain the integrity of plan mode as a safe research environment, persistent tool approvals are context-aware. Approvals granted in modes like -Default or Auto-Edit do not apply to Plan Mode, ensuring that tools trusted for +Default or Auto-Edit do not apply to plan mode, ensuring that tools trusted for implementation don't automatically execute while you're researching. However, -approvals granted while in Plan Mode are treated as intentional choices for +approvals granted while in plan mode are treated as intentional choices for global trust and apply to all modes. -If you want to manually restrict a rule to other modes but _not_ to Plan Mode, +If you want to manually restrict a rule to other modes but _not_ to plan mode, you must explicitly specify the target modes. For example, to allow `npm test` -in default and Auto-Edit modes but not in Plan Mode: +in default and Auto-Edit modes but not in plan mode: ```toml [[rule]] @@ -198,13 +198,13 @@ toolName = "run_shell_command" commandPrefix = "npm test" decision = "allow" priority = 100 -# By omitting "plan", this rule will not be active in Plan Mode. +# By omitting "plan", this rule will not be active in plan mode. modes = ["default", "autoEdit"] ``` #### Example: Automatically approve read-only MCP tools -By default, read-only MCP tools require user confirmation in Plan Mode. You can +By default, read-only MCP tools require user confirmation in plan mode. You can use `toolAnnotations` and the `mcpName` wildcard to customize this behavior for your specific environment. @@ -223,10 +223,10 @@ modes = ["plan"] For more information on how the policy engine works, see the [policy engine](../reference/policy-engine.md) docs. -#### Example: Allow git commands in Plan Mode +#### Example: Allow git commands in plan mode -This rule lets you check the repository status and see changes while in Plan -Mode. +This rule lets you check the repository status and see changes while in plan +mode. `~/.gemini/policies/git-research.toml` @@ -239,12 +239,12 @@ priority = 100 modes = ["plan"] ``` -#### Example: Enable custom subagents in Plan Mode +#### Example: Enable custom subagents in plan mode Built-in research [subagents](../core/subagents.md) like [`codebase_investigator`](../core/subagents.md#codebase-investigator) and -[`cli_help`](../core/subagents.md#cli-help-agent) are enabled by default in Plan -Mode. You can enable additional +[`cli_help`](../core/subagents.md#cli-help-agent) are enabled by default in plan +mode. You can enable additional [custom subagents](../core/subagents.md#creating-custom-subagents) by adding a rule to your policy. @@ -279,7 +279,7 @@ example, to store plans in a `.gemini/plans` directory within your project: } ``` -To maintain the safety of Plan Mode, user-configured paths for the plans +To maintain the safety of plan mode, user-configured paths for the plans directory are restricted to the project root. This ensures that custom planning locations defined within a project's workspace cannot be used to escape and overwrite sensitive files elsewhere. Any user-configured directory must reside @@ -302,17 +302,19 @@ modes = ["plan"] argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\"" ``` -### Using hooks with Plan Mode +### Using hooks with plan mode You can use the [hook system](../hooks/writing-hooks.md) to automate parts of the planning workflow or enforce additional checks when Gemini CLI transitions -into or out of Plan Mode. +into or out of plan mode. Hooks such as `BeforeTool` or `AfterTool` can be configured to intercept the `enter_plan_mode` and `exit_plan_mode` tool calls. -> [!WARNING] When hooks are triggered by **tool executions**, they do **not** -> run when you manually toggle Plan Mode using the `/plan` command or the + +> [!WARNING] +> When hooks are triggered by **tool executions**, they do **not** +> run when you manually toggle plan mode using the `/plan` command or the > `Shift+Tab` keyboard shortcut. If you need hooks to execute on mode changes, > ensure the transition is initiated by the agent (for example, by asking "start > a plan for..."). @@ -321,18 +323,21 @@ Hooks such as `BeforeTool` or `AfterTool` can be configured to intercept the If your organizational policy requires a record of all execution plans, you can use an `AfterTool` hook to securely copy the plan artifact to Google Cloud -Storage whenever Gemini CLI exits Plan Mode to start the implementation. +Storage whenever Gemini CLI exits plan mode to start the implementation. **`.gemini/hooks/archive-plan.sh`:** ```bash #!/usr/bin/env bash -# Extract the plan path from the tool input JSON -plan_path=$(jq -r '.tool_input.plan_path // empty') +# Extract the plan filename from the tool input JSON +plan_filename=$(jq -r '.tool_input.plan_filename // empty') + +# Construct the absolute path using the GEMINI_PLANS_DIR environment variable +plan_path="$GEMINI_PLANS_DIR/$plan_filename" if [ -f "$plan_path" ]; then # Generate a unique filename using a timestamp - filename="$(date +%s)_$(basename "$plan_path")" + filename="$(date +%s)_$plan_filename" # Upload the plan to GCS in the background so it doesn't block the CLI gsutil cp "$plan_path" "gs://my-audit-bucket/gemini-plans/$filename" > /dev/null 2>&1 & diff --git a/docs/hooks/index.md b/docs/hooks/index.md index 0d6ae6d447e..aec709e9672 100644 --- a/docs/hooks/index.md +++ b/docs/hooks/index.md @@ -57,13 +57,13 @@ Understanding these core principles is essential for building robust hooks. Hooks communicate via `stdin` (Input) and `stdout` (Output). -1. **Silence is Mandatory**: Your script **must not** print any plain text to +1. **Silence is mandatory**: Your script **must not** print any plain text to `stdout` other than the final JSON object. **Even a single `echo` or `print` call before the JSON will break parsing.** -2. **Pollution = Failure**: If `stdout` contains non-JSON text, parsing will +2. **Pollution = failure**: If `stdout` contains non-JSON text, parsing will fail. The CLI will default to "Allow" and treat the entire output as a `systemMessage`. -3. **Debug via Stderr**: Use `stderr` for **all** logging and debugging (for +3. **Debug via stderr**: Use `stderr` for **all** logging and debugging (for example, `echo "debug" >&2`). Gemini CLI captures `stderr` but never attempts to parse it as JSON. @@ -75,7 +75,7 @@ execution: | Exit Code | Label | Behavioral Impact | | --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **0** | **Success** | The `stdout` is parsed as JSON. **Preferred code** for all logic, including intentional blocks (for example, `{"decision": "deny"}`). | -| **2** | **System Block** | **Critical Block**. The target action (tool, turn, or stop) is aborted. `stderr` is used as the rejection reason. High severity; used for security stops or script failures. | +| **2** | **System block** | **Critical block**. The target action (tool, turn, or stop) is aborted. `stderr` is used as the rejection reason. High severity; used for security stops or script failures. | | **Other** | **Warning** | Non-fatal failure. A warning is shown, but the interaction proceeds using original parameters. | #### Matchers @@ -84,8 +84,8 @@ You can filter which specific tools or triggers fire your hook using the `matcher` field. - **Tool events** (`BeforeTool`, `AfterTool`): Matchers are **Regular - Expressions**. (for example, `"write_.*"`). -- **Lifecycle events**: Matchers are **Exact Strings**. (for example, + expressions**. (for example, `"write_.*"`). +- **Lifecycle events**: Matchers are **Exact strings**. (for example, `"startup"`). - **Wildcards**: `"*"` or `""` (empty string) matches all occurrences. @@ -138,6 +138,7 @@ multiple layers in the following order of precedence (highest to lowest): Hooks are executed with a sanitized environment. - `GEMINI_PROJECT_DIR`: The absolute path to the project root. +- `GEMINI_PLANS_DIR`: The absolute path to the plans directory. - `GEMINI_SESSION_ID`: The unique ID for the current session. - `GEMINI_CWD`: The current working directory. - `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility. diff --git a/package-lock.json b/package-lock.json index 17b8bc26ccc..45ac0ffd050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -447,7 +447,8 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)" + "license": "(Apache-2.0 AND BSD-3-Clause)", + "peer": true }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.1", @@ -1450,6 +1451,7 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" @@ -2156,6 +2158,7 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2336,6 +2339,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2385,6 +2389,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2759,6 +2764,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2792,6 +2798,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" @@ -2846,6 +2853,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", @@ -4082,6 +4090,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4356,6 +4365,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5229,6 +5239,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7369,7 +7380,8 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7953,6 +7965,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8470,6 +8483,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9782,6 +9796,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -10060,6 +10075,7 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.9.tgz", "integrity": "sha512-RL9sSiLQZECnjbmBwjIHOp8yVGdWF7C/uifg7ISv/e+F3nLNsfl7FdUFQs8iZARFMJAYxMFpxW6OW+HSt9drwQ==", "license": "MIT", + "peer": true, "dependencies": { "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.3", @@ -13833,6 +13849,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13843,6 +13860,7 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -15992,6 +16010,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16214,7 +16233,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -16222,6 +16242,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16387,6 +16408,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16609,6 +16631,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16722,6 +16745,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16734,6 +16758,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -17381,6 +17406,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17824,6 +17850,7 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -17927,6 +17954,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/core/src/hooks/hookRunner.test.ts b/packages/core/src/hooks/hookRunner.test.ts index 9cee6575fef..76cd06959c8 100644 --- a/packages/core/src/hooks/hookRunner.test.ts +++ b/packages/core/src/hooks/hookRunner.test.ts @@ -76,6 +76,9 @@ describe('HookRunner', () => { sanitizationConfig: { enableEnvironmentVariableRedaction: true, }, + storage: { + getPlansDir: vi.fn().mockReturnValue('/test/project/plans'), + }, } as unknown as Config; hookRunner = new HookRunner(mockConfig); @@ -370,6 +373,7 @@ describe('HookRunner', () => { shell: false, env: expect.objectContaining({ GEMINI_PROJECT_DIR: '/test/project', + GEMINI_PLANS_DIR: '/test/project/plans', CLAUDE_PROJECT_DIR: '/test/project', }), }), diff --git a/packages/core/src/hooks/hookRunner.ts b/packages/core/src/hooks/hookRunner.ts index 812deafcbe3..f69c2157aa6 100644 --- a/packages/core/src/hooks/hookRunner.ts +++ b/packages/core/src/hooks/hookRunner.ts @@ -348,6 +348,7 @@ export class HookRunner { const env = { ...sanitizeEnvironment(process.env, this.config.sanitizationConfig), GEMINI_PROJECT_DIR: input.cwd, + GEMINI_PLANS_DIR: this.config.storage.getPlansDir(), CLAUDE_PROJECT_DIR: input.cwd, // For compatibility ...hookConfig.env, }; From 2484923f36106d64c536f994381efcf6dd3d732d Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Mon, 13 Apr 2026 11:10:56 -0400 Subject: [PATCH 2/6] update docs --- docs/cli/plan-mode.md | 94 +++++++++++++++++++++---------------------- docs/hooks/index.md | 13 +++--- 2 files changed, 52 insertions(+), 55 deletions(-) diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 2877c523fa2..1f9f6020639 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -11,51 +11,51 @@ implementation. With Plan Mode, you can: Plan Mode is enabled by default. You can manage this setting using the `/settings` command. -## How to enter plan mode +## How to enter Plan Mode -Plan mode integrates seamlessly into your workflow, letting you switch between +Plan Mode integrates seamlessly into your workflow, letting you switch between planning and execution as needed. -You can either configure Gemini CLI to start in plan mode by default or enter -plan mode manually during a session. +You can either configure Gemini CLI to start in Plan Mode by default or enter +Plan Mode manually during a session. -### Launch in plan mode +### Launch in Plan Mode -To start Gemini CLI directly in plan mode by default: +To start Gemini CLI directly in Plan Mode by default: 1. Use the `/settings` command. -2. Set **Default approval mode** to `Plan`. +2. Set **Default Approval Mode** to `Plan`. -To launch Gemini CLI in plan mode once: +To launch Gemini CLI in Plan Mode once: 1. Use `gemini --approval-mode=plan` when launching Gemini CLI. -### Enter plan mode manually +### Enter Plan Mode manually -To start plan mode while using Gemini CLI: +To start Plan Mode while using Gemini CLI: - **Keyboard shortcut:** Press `Shift+Tab` to cycle through approval modes - (`Default` -> `Auto-Edit` -> `Plan`). Plan mode is automatically removed from + (`Default` -> `Auto-Edit` -> `Plan`). Plan Mode is automatically removed from the rotation when Gemini CLI is actively processing or showing confirmation dialogs. - **Command:** Type `/plan [goal]` in the input box. The `[goal]` is optional; - for example, `/plan implement authentication` will switch to plan mode and + for example, `/plan implement authentication` will switch to Plan Mode and immediately submit the prompt to the model. -- **Natural language:** Ask Gemini CLI to "start a plan for...". Gemini CLI +- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI calls the [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode) tool to switch modes. This tool is not available when Gemini CLI is in [YOLO mode](../reference/configuration.md#command-line-arguments). -## How to use plan mode +## How to use Plan Mode -Plan mode lets you collaborate with Gemini CLI to design a solution before +Plan Mode lets you collaborate with Gemini CLI to design a solution before Gemini CLI takes action. 1. **Provide a goal:** Start by describing what you want to achieve. Gemini CLI - will then enter plan mode (if it's not already) to research the task. + will then enter Plan Mode (if it's not already) to research the task. 2. **Discuss and agree on strategy:** As Gemini CLI analyzes your codebase, it will discuss its findings and proposed strategy with you to ensure alignment. It may ask you questions or present different implementation @@ -101,19 +101,19 @@ describing complex changes in natural language. your comments, and adjusts the implementation strategy. It then presents the refined plan for your final approval. -## How to exit plan mode +## How to exit Plan Mode -You can exit plan mode at any time, whether you have finalized a plan or want to +You can exit Plan Mode at any time, whether you have finalized a plan or want to switch back to another mode. - **Approve a plan:** When Gemini CLI presents a finalized plan, approving it - automatically exits plan mode and starts the implementation. + automatically exits Plan Mode and starts the implementation. - **Keyboard shortcut:** Press `Shift+Tab` to cycle to the desired mode. - **Natural language:** Ask Gemini CLI to "exit plan mode" or "stop planning." -## Tool restrictions +## Tool Restrictions -Plan mode enforces strict safety policies to prevent accidental changes. +Plan Mode enforces strict safety policies to prevent accidental changes. These are the only allowed tools: @@ -142,7 +142,7 @@ These are the only allowed tools: ## Customization and best practices -Plan mode is secure by default, but you can adapt it to fit your specific +Plan Mode is secure by default, but you can adapt it to fit your specific workflows. You can customize how Gemini CLI plans by using skills, adjusting safety policies, changing where plans are stored, or adding hooks. @@ -150,7 +150,7 @@ safety policies, changing where plans are stored, or adding hooks. You can use [Agent Skills](../cli/skills.md) to customize how Gemini CLI approaches planning for specific types of tasks. When a skill is activated -during plan mode, its specialized instructions and procedural workflows will +during Plan Mode, its specialized instructions and procedural workflows will guide the research, design, and planning phases. For example: @@ -162,13 +162,13 @@ For example: - A **"Frontend Design"** skill could guide Gemini CLI to use specific UI components and accessibility standards in its proposal. -To use a skill in plan mode, you can explicitly ask Gemini CLI to "use the +To use a skill in Plan Mode, you can explicitly ask Gemini CLI to "use the `` skill to plan..." or Gemini CLI may autonomously activate it based on the task description. ### Custom policies -Plan mode's default tool restrictions are managed by the +Plan Mode's default tool restrictions are managed by the [policy engine](../reference/policy-engine.md) and defined in the built-in [`plan.toml`] file. The built-in policy (Tier 1) enforces the read-only state, but you can customize these rules by creating your own policies in your @@ -179,18 +179,18 @@ but you can customize these rules by creating your own policies in your As described in the [policy engine documentation](../reference/policy-engine.md#approval-modes), any rule that does not explicitly specify `modes` is considered "always active" and -will apply to plan mode as well. +will apply to Plan Mode as well. -To maintain the integrity of plan mode as a safe research environment, +To maintain the integrity of Plan Mode as a safe research environment, persistent tool approvals are context-aware. Approvals granted in modes like -Default or Auto-Edit do not apply to plan mode, ensuring that tools trusted for +Default or Auto-Edit do not apply to Plan Mode, ensuring that tools trusted for implementation don't automatically execute while you're researching. However, -approvals granted while in plan mode are treated as intentional choices for +approvals granted while in Plan Mode are treated as intentional choices for global trust and apply to all modes. -If you want to manually restrict a rule to other modes but _not_ to plan mode, +If you want to manually restrict a rule to other modes but _not_ to Plan Mode, you must explicitly specify the target modes. For example, to allow `npm test` -in default and Auto-Edit modes but not in plan mode: +in default and Auto-Edit modes but not in Plan Mode: ```toml [[rule]] @@ -198,13 +198,13 @@ toolName = "run_shell_command" commandPrefix = "npm test" decision = "allow" priority = 100 -# By omitting "plan", this rule will not be active in plan mode. +# By omitting "plan", this rule will not be active in Plan Mode. modes = ["default", "autoEdit"] ``` #### Example: Automatically approve read-only MCP tools -By default, read-only MCP tools require user confirmation in plan mode. You can +By default, read-only MCP tools require user confirmation in Plan Mode. You can use `toolAnnotations` and the `mcpName` wildcard to customize this behavior for your specific environment. @@ -223,10 +223,10 @@ modes = ["plan"] For more information on how the policy engine works, see the [policy engine](../reference/policy-engine.md) docs. -#### Example: Allow git commands in plan mode +#### Example: Allow git commands in Plan Mode -This rule lets you check the repository status and see changes while in plan -mode. +This rule lets you check the repository status and see changes while in Plan +Mode. `~/.gemini/policies/git-research.toml` @@ -239,12 +239,12 @@ priority = 100 modes = ["plan"] ``` -#### Example: Enable custom subagents in plan mode +#### Example: Enable custom subagents in Plan Mode Built-in research [subagents](../core/subagents.md) like [`codebase_investigator`](../core/subagents.md#codebase-investigator) and -[`cli_help`](../core/subagents.md#cli-help-agent) are enabled by default in plan -mode. You can enable additional +[`cli_help`](../core/subagents.md#cli-help-agent) are enabled by default in Plan +Mode. You can enable additional [custom subagents](../core/subagents.md#creating-custom-subagents) by adding a rule to your policy. @@ -279,7 +279,7 @@ example, to store plans in a `.gemini/plans` directory within your project: } ``` -To maintain the safety of plan mode, user-configured paths for the plans +To maintain the safety of Plan Mode, user-configured paths for the plans directory are restricted to the project root. This ensures that custom planning locations defined within a project's workspace cannot be used to escape and overwrite sensitive files elsewhere. Any user-configured directory must reside @@ -302,19 +302,17 @@ modes = ["plan"] argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\"" ``` -### Using hooks with plan mode +### Using hooks with Plan Mode You can use the [hook system](../hooks/writing-hooks.md) to automate parts of the planning workflow or enforce additional checks when Gemini CLI transitions -into or out of plan mode. +into or out of Plan Mode. Hooks such as `BeforeTool` or `AfterTool` can be configured to intercept the `enter_plan_mode` and `exit_plan_mode` tool calls. - -> [!WARNING] -> When hooks are triggered by **tool executions**, they do **not** -> run when you manually toggle plan mode using the `/plan` command or the +> [!WARNING] When hooks are triggered by **tool executions**, they do **not** +> run when you manually toggle Plan Mode using the `/plan` command or the > `Shift+Tab` keyboard shortcut. If you need hooks to execute on mode changes, > ensure the transition is initiated by the agent (for example, by asking "start > a plan for..."). @@ -323,7 +321,7 @@ Hooks such as `BeforeTool` or `AfterTool` can be configured to intercept the If your organizational policy requires a record of all execution plans, you can use an `AfterTool` hook to securely copy the plan artifact to Google Cloud -Storage whenever Gemini CLI exits plan mode to start the implementation. +Storage whenever Gemini CLI exits Plan Mode to start the implementation. **`.gemini/hooks/archive-plan.sh`:** @@ -337,7 +335,7 @@ plan_path="$GEMINI_PLANS_DIR/$plan_filename" if [ -f "$plan_path" ]; then # Generate a unique filename using a timestamp - filename="$(date +%s)_$plan_filename" + filename="$(date +%s)_$(basename "$plan_path")" # Upload the plan to GCS in the background so it doesn't block the CLI gsutil cp "$plan_path" "gs://my-audit-bucket/gemini-plans/$filename" > /dev/null 2>&1 & diff --git a/docs/hooks/index.md b/docs/hooks/index.md index aec709e9672..0d6ae6d447e 100644 --- a/docs/hooks/index.md +++ b/docs/hooks/index.md @@ -57,13 +57,13 @@ Understanding these core principles is essential for building robust hooks. Hooks communicate via `stdin` (Input) and `stdout` (Output). -1. **Silence is mandatory**: Your script **must not** print any plain text to +1. **Silence is Mandatory**: Your script **must not** print any plain text to `stdout` other than the final JSON object. **Even a single `echo` or `print` call before the JSON will break parsing.** -2. **Pollution = failure**: If `stdout` contains non-JSON text, parsing will +2. **Pollution = Failure**: If `stdout` contains non-JSON text, parsing will fail. The CLI will default to "Allow" and treat the entire output as a `systemMessage`. -3. **Debug via stderr**: Use `stderr` for **all** logging and debugging (for +3. **Debug via Stderr**: Use `stderr` for **all** logging and debugging (for example, `echo "debug" >&2`). Gemini CLI captures `stderr` but never attempts to parse it as JSON. @@ -75,7 +75,7 @@ execution: | Exit Code | Label | Behavioral Impact | | --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **0** | **Success** | The `stdout` is parsed as JSON. **Preferred code** for all logic, including intentional blocks (for example, `{"decision": "deny"}`). | -| **2** | **System block** | **Critical block**. The target action (tool, turn, or stop) is aborted. `stderr` is used as the rejection reason. High severity; used for security stops or script failures. | +| **2** | **System Block** | **Critical Block**. The target action (tool, turn, or stop) is aborted. `stderr` is used as the rejection reason. High severity; used for security stops or script failures. | | **Other** | **Warning** | Non-fatal failure. A warning is shown, but the interaction proceeds using original parameters. | #### Matchers @@ -84,8 +84,8 @@ You can filter which specific tools or triggers fire your hook using the `matcher` field. - **Tool events** (`BeforeTool`, `AfterTool`): Matchers are **Regular - expressions**. (for example, `"write_.*"`). -- **Lifecycle events**: Matchers are **Exact strings**. (for example, + Expressions**. (for example, `"write_.*"`). +- **Lifecycle events**: Matchers are **Exact Strings**. (for example, `"startup"`). - **Wildcards**: `"*"` or `""` (empty string) matches all occurrences. @@ -138,7 +138,6 @@ multiple layers in the following order of precedence (highest to lowest): Hooks are executed with a sanitized environment. - `GEMINI_PROJECT_DIR`: The absolute path to the project root. -- `GEMINI_PLANS_DIR`: The absolute path to the plans directory. - `GEMINI_SESSION_ID`: The unique ID for the current session. - `GEMINI_CWD`: The current working directory. - `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility. From fa8d168eceb07bb6c2df1244a000ef6916766aa7 Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Mon, 13 Apr 2026 11:11:55 -0400 Subject: [PATCH 3/6] revert --- package-lock.json | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45ac0ffd050..17b8bc26ccc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -447,8 +447,7 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.1", @@ -1451,7 +1450,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" @@ -2158,7 +2156,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2339,7 +2336,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -2389,7 +2385,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2764,7 +2759,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2798,7 +2792,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" @@ -2853,7 +2846,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", @@ -4090,7 +4082,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4365,7 +4356,6 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5239,7 +5229,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7380,8 +7369,7 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7965,7 +7953,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8483,7 +8470,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9796,7 +9782,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -10075,7 +10060,6 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.9.tgz", "integrity": "sha512-RL9sSiLQZECnjbmBwjIHOp8yVGdWF7C/uifg7ISv/e+F3nLNsfl7FdUFQs8iZARFMJAYxMFpxW6OW+HSt9drwQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.3", @@ -13849,7 +13833,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13860,7 +13843,6 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -16010,7 +15992,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16233,8 +16214,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -16242,7 +16222,6 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16408,7 +16387,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16631,7 +16609,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16745,7 +16722,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16758,7 +16734,6 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -17406,7 +17381,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17850,7 +17824,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -17954,7 +17927,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, From d78a65ac30b10892b31b24a37b5b788aba3267c2 Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Mon, 13 Apr 2026 11:12:23 -0400 Subject: [PATCH 4/6] update docs --- docs/hooks/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hooks/index.md b/docs/hooks/index.md index 0d6ae6d447e..0125a28eb23 100644 --- a/docs/hooks/index.md +++ b/docs/hooks/index.md @@ -138,6 +138,7 @@ multiple layers in the following order of precedence (highest to lowest): Hooks are executed with a sanitized environment. - `GEMINI_PROJECT_DIR`: The absolute path to the project root. +- `GEMINI_PLANS_DIR`: The absolute path to the plans directory. - `GEMINI_SESSION_ID`: The unique ID for the current session. - `GEMINI_CWD`: The current working directory. - `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility. From a8f0d53290c8b17fb6c1e95e1bdf99de8d3f1f18 Mon Sep 17 00:00:00 2001 From: Adib234 <30782825+Adib234@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:12:40 -0400 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/cli/plan-mode.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 1f9f6020639..4d9c45ce17a 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -329,6 +329,7 @@ Storage whenever Gemini CLI exits Plan Mode to start the implementation. #!/usr/bin/env bash # Extract the plan filename from the tool input JSON plan_filename=$(jq -r '.tool_input.plan_filename // empty') +plan_filename=$(basename -- "$plan_filename") # Construct the absolute path using the GEMINI_PLANS_DIR environment variable plan_path="$GEMINI_PLANS_DIR/$plan_filename" From 2baa3a6ec542b3b91f4fab5d98ae90a057fc0add Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Mon, 13 Apr 2026 11:56:27 -0400 Subject: [PATCH 6/6] fix(core): secure GEMINI_PLANS_DIR expansion and add missing env vars --- packages/core/src/hooks/hookRunner.test.ts | 38 ++++++++++++++++++++++ packages/core/src/hooks/hookRunner.ts | 11 +++++++ 2 files changed, 49 insertions(+) diff --git a/packages/core/src/hooks/hookRunner.test.ts b/packages/core/src/hooks/hookRunner.test.ts index 76cd06959c8..5b155a8516d 100644 --- a/packages/core/src/hooks/hookRunner.test.ts +++ b/packages/core/src/hooks/hookRunner.test.ts @@ -374,12 +374,50 @@ describe('HookRunner', () => { env: expect.objectContaining({ GEMINI_PROJECT_DIR: '/test/project', GEMINI_PLANS_DIR: '/test/project/plans', + GEMINI_CWD: '/test/project', + GEMINI_SESSION_ID: 'test-session', CLAUDE_PROJECT_DIR: '/test/project', }), }), ); }); + it('should expand and escape GEMINI_PLANS_DIR in commands', async () => { + const configWithEnvVar: HookConfig = { + type: HookType.Command, + command: 'ls $GEMINI_PLANS_DIR', + }; + + // Change plans dir to one with spaces + vi.mocked(mockConfig.storage.getPlansDir).mockReturnValue( + '/test/project/plans with spaces', + ); + + mockSpawn.mockProcessOn.mockImplementation( + (event: string, callback: (code: number) => void) => { + if (event === 'close') { + setImmediate(() => callback(0)); + } + }, + ); + + await hookRunner.executeHook( + configWithEnvVar, + HookEventName.BeforeTool, + mockInput, + ); + + expect(spawn).toHaveBeenCalledWith( + expect.stringMatching(/bash|powershell/), + expect.arrayContaining([ + expect.stringMatching( + /ls ['"]\/test\/project\/plans with spaces['"]/, + ), + ]), + expect.any(Object), + ); + }); + it('should not allow command injection via GEMINI_PROJECT_DIR', async () => { const maliciousCwd = '/test/project; echo "pwned" > /tmp/pwned'; const mockMaliciousInput: HookInput = { diff --git a/packages/core/src/hooks/hookRunner.ts b/packages/core/src/hooks/hookRunner.ts index f69c2157aa6..4c199ebfc74 100644 --- a/packages/core/src/hooks/hookRunner.ts +++ b/packages/core/src/hooks/hookRunner.ts @@ -349,6 +349,8 @@ export class HookRunner { ...sanitizeEnvironment(process.env, this.config.sanitizationConfig), GEMINI_PROJECT_DIR: input.cwd, GEMINI_PLANS_DIR: this.config.storage.getPlansDir(), + GEMINI_CWD: input.cwd, + GEMINI_SESSION_ID: input.session_id, CLAUDE_PROJECT_DIR: input.cwd, // For compatibility ...hookConfig.env, }; @@ -515,8 +517,17 @@ export class HookRunner { ): string { debugLogger.debug(`Expanding hook command: ${command} (cwd: ${input.cwd})`); const escapedCwd = escapeShellArg(input.cwd, shellType); + const escapedPlansDir = escapeShellArg( + this.config.storage.getPlansDir(), + shellType, + ); + const escapedSessionId = escapeShellArg(input.session_id, shellType); + return command .replace(/\$GEMINI_PROJECT_DIR/g, () => escapedCwd) + .replace(/\$GEMINI_CWD/g, () => escapedCwd) + .replace(/\$GEMINI_PLANS_DIR/g, () => escapedPlansDir) + .replace(/\$GEMINI_SESSION_ID/g, () => escapedSessionId) .replace(/\$CLAUDE_PROJECT_DIR/g, () => escapedCwd); // For compatibility }