Skip to content

fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions#24047

Merged
DavidAPierce merged 27 commits intomainfrom
plan_mode_loop_fix
Mar 31, 2026
Merged

fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions#24047
DavidAPierce merged 27 commits intomainfrom
plan_mode_loop_fix

Conversation

@DavidAPierce
Copy link
Copy Markdown
Contributor

Summary

This PR resolves a deadlock in Plan Mode where the agent could not create its plan file if the parent directory (e.g., .gemini/plans) did not exist on the host machine. This was caused by sandbox restrictions preventing both directory creation and the binding of non-existent paths.

Details

The fix implements a proactive, path-specific permission strategy and host-side initialization:

  • EnterPlanModeTool: Now pre-creates the plans directory on the host before entering the sandbox.
  • Virtual Command Translation: Implemented translation for __read and __write in Linux (Bubblewrap) and macOS (Seatbelt) sandbox managers. This allows SandboxedFileSystemService to perform these operations using standard system tools (cat, sh) even when run_shell_command is blocked.
  • Parent Directory Binding (Linux): Updated LinuxSandboxManager to allow binding the parent directory of an explicitly allowed but non-existent path.
  • Error Handling: Enhanced SandboxedFileSystemService to correctly identify and propagate platform-specific ENOENT error strings (e.g., Windows "Could not find a part of the path"), allowing write_file to handle "new file" scenarios correctly.
  • Granular Policies: Ensured that specific file paths and write permissions are passed to the sandbox during file operations.

Related Issues

Fixes #23958

How to Validate

  1. Run the new unit tests:
    npm test -w @google/gemini-cli-core -- src/services/sandboxedFileSystemService.test.ts src/sandbox/linux/LinuxSandboxManager.test.ts src/sandbox/macos/MacOsSandboxManager.test.ts src/tools/enter-plan-mode.test.ts
  2. Manually verify by entering Plan Mode and saving a plan when the .gemini/plans directory does not exist.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

This PR resolves a deadlock in Plan Mode where the agent could not create its plan file if the parent directory (e.g., .gemini/plans) did not exist on the host machine. This was caused by sandbox restrictions preventing both directory creation and binding of non-existent paths.

Key changes:
- updated EnterPlanModeTool to pre-create the plans directory on the host.
- Implemented virtual command translation (__read/__write) for Linux and macOS sandboxes.
- Enhanced SandboxedFileSystemService to recognize platform-specific ENOENT error strings (Linux/Windows).
- Updated LinuxSandboxManager to allow binding the parent directory of explicitly allowed but non-existent paths.
- Ensured operation-specific policies are passed to the sandbox during file operations.

Fixes #23958
@DavidAPierce DavidAPierce requested review from a team as code owners March 27, 2026 19:31
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a deadlock in Plan Mode where the agent was unable to create plan files due to sandbox restrictions when the target directory did not exist. By implementing proactive host-side directory creation and translating virtual file system commands into native shell operations, the changes ensure that the sandbox environment is correctly prepared for file operations. Additionally, the update improves error propagation to handle missing file scenarios gracefully across different platforms.

Highlights

  • Proactive Directory Initialization: Updated EnterPlanModeTool to pre-create the plans directory on the host, ensuring it exists before sandbox binding occurs.
  • Virtual Command Translation: Implemented translation for __read and __write commands in both Linux and macOS sandbox managers to use native system tools like cat and sh, bypassing sandbox restrictions.
  • Sandbox Binding Improvements: Enhanced LinuxSandboxManager to allow binding the parent directory of non-existent paths, facilitating file creation within sandboxed environments.
  • Error Handling: Improved SandboxedFileSystemService to correctly identify and propagate platform-specific ENOENT errors, enabling better handling of new file scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-cli gemini-cli Bot added area/platform Issues related to Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item. labels Mar 27, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces virtual commands (__read and __write) to the Linux and macOS sandbox managers to standardize file system access and updates the SandboxedFileSystemService to utilize these commands with appropriate policies. It also ensures that the plans directory exists on the host before entering plan mode and improves error handling by mapping file-not-found errors to the ENOENT code. The review feedback identifies critical security vulnerabilities regarding path traversal in the SandboxedFileSystemService due to a lack of path sanitization. Additionally, the reviewer pointed out a logic error in the Linux sandbox manager where parent directories for non-existent files must be mounted as read-write to allow for file creation, necessitating changes to both the implementation and the associated tests.

Comment thread packages/core/src/services/sandboxedFileSystemService.ts
Comment thread packages/core/src/services/sandboxedFileSystemService.ts
Comment thread packages/core/src/sandbox/linux/LinuxSandboxManager.ts Outdated
Comment thread packages/core/src/sandbox/linux/LinuxSandboxManager.test.ts Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 27, 2026

🧠 Model Steering Guidance

This PR modifies files that affect the model's behavior (prompts, tools, or instructions).

  • 🚀 Maintainer Reminder: Please ensure that these changes do not regress results on benchmark evals before merging.

This is an automated guidance message triggered by steering logic signatures.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 27, 2026

Size Change: +3.17 kB (+0.01%)

Total Size: 26.5 MB

Filename Size Change
./bundle/chunk-J7QYM45S.js 0 B -3.78 MB (removed) 🏆
./bundle/chunk-VZS2GAPT.js 0 B -14.7 MB (removed) 🏆
./bundle/core-X63HECWP.js 0 B -44.6 kB (removed) 🏆
./bundle/devtoolsService-KWM37FOU.js 0 B -28.4 kB (removed) 🏆
./bundle/interactiveCli-OO5LQBJ6.js 0 B -1.66 MB (removed) 🏆
./bundle/oauth2-provider-62WP7MYE.js 0 B -9.16 kB (removed) 🏆
./bundle/chunk-FXFXGMKM.js 14.7 MB +14.7 MB (new file) 🆕
./bundle/chunk-LG3JUWEH.js 3.78 MB +3.78 MB (new file) 🆕
./bundle/core-MQLN26ST.js 44.6 kB +44.6 kB (new file) 🆕
./bundle/devtoolsService-WMRUGXLN.js 28.4 kB +28.4 kB (new file) 🆕
./bundle/interactiveCli-KRGWXQCN.js 1.66 MB +1.66 MB (new file) 🆕
./bundle/oauth2-provider-YXZH6LNY.js 9.16 kB +9.16 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size
./bundle/chunk-34MYV7JD.js 2.45 kB
./bundle/chunk-5AUYMPVF.js 858 B
./bundle/chunk-664ZODQF.js 124 kB
./bundle/chunk-DAHVX5MI.js 206 kB
./bundle/chunk-IUUIT4SU.js 56.5 kB
./bundle/chunk-PTGY7UXW.js 1.96 MB
./bundle/chunk-RJTRUG2J.js 39.8 kB
./bundle/chunk-U4FACSVX.js 1.13 kB
./bundle/devtools-36NN55EP.js 696 kB
./bundle/dist-T73EYRDX.js 356 B
./bundle/events-CLX3JQHP.js 418 B
./bundle/gemini.js 532 kB
./bundle/getMachineId-bsd-TXG52NKR.js 1.55 kB
./bundle/getMachineId-darwin-7OE4DDZ6.js 1.55 kB
./bundle/getMachineId-linux-SHIFKOOX.js 1.34 kB
./bundle/getMachineId-unsupported-5U5DOEYY.js 1.06 kB
./bundle/getMachineId-win-6KLLGOI4.js 1.72 kB
./bundle/memoryDiscovery-4PHRORYM.js 980 B
./bundle/multipart-parser-KPBZEGQU.js 11.7 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/client/main.js 222 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/_client-assets.js 229 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/index.js 13.4 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/types.js 132 B
./bundle/sandbox-macos-permissive-open.sb 890 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB
./bundle/sandbox-macos-strict-open.sb 4.82 kB
./bundle/sandbox-macos-strict-proxied.sb 5.02 kB
./bundle/src-QVCVGIUX.js 47 kB
./bundle/tree-sitter-7U6MW5PS.js 274 kB
./bundle/tree-sitter-bash-34ZGLXVX.js 1.84 MB

compressed-size-action

- Ensure paths passed to SandboxedFileSystemService are sanitized and validated to be inside the workspace.

- Bind missing parent directories as writable when the command is '__write' in LinuxSandboxManager.

- Add a behavioral evaluation for entering plan mode and creating a plan file from scratch.
…across platforms

- Added support for virtual commands (__read, __write) in WindowsSandboxManager using cmd.exe and PowerShell.

- Integrated includeDirectories into Windows, Linux, and macOS sandbox managers to grant read access.

- Improved directory pre-creation logic in Windows and Linux managers for sandboxed writes.

- Fixed initialization order in Config constructor to prevent TypeScript errors.
# Conflicts:
#	packages/core/src/sandbox/windows/WindowsSandboxManager.ts
@galz10
Copy link
Copy Markdown
Collaborator

galz10 commented Mar 30, 2026

Code Review

Scope: Pull Request #24047

The PR effectively solves the Plan Mode deadlock by creating the .gemini/plans directory on the host and implementing virtual command translations (__read and __write) for SandboxedFileSystemService. This correctly allows file operations to respect granular sandbox policies without failing when parent directories are unmapped. The implementation handles file permission grants for non-existent files safely by recursively resolving to the nearest existing parent directory.

However, there are critical issues with how Windows handles the virtual command arguments that could cause syntax errors or unintended command execution. Additionally, an unrelated change to URL validation has been included.

Metadata Review

The PR Title perfectly aligns with the Conventional Commits format (fix(core): ...). The PR Body provides an excellent, detailed explanation of the problem, the specific changes made across different components, and clear validation steps.

Concerns (Action Required)

  • packages/core/src/sandbox/windows/WindowsSandboxManager.ts (PowerShell String Concatenation):
    When -Command is passed a string (instead of a script block), PowerShell concatenates the string and all subsequent arguments with spaces before executing them. This means $args[0] in $Input | Out-File -FilePath $args[0] -Encoding utf8 evaluates to $null (since no arguments are bound to the string), and targetPath is appended to the end of the executed string. If targetPath contains spaces, this will result in a syntax error or a write to the wrong location.
    Suggestion: Wrap the command in a script block and invoke it with the call operator (&) so that targetPath is properly bound to $args[0]:

          args = [
            '-NoProfile',
            '-NonInteractive',
            '-Command',
            '& { $Input | Out-File -FilePath $args[0] -Encoding utf8 }',
            targetPath,
          ];
  • packages/core/src/sandbox/windows/WindowsSandboxManager.ts (Cmd.exe Parsing):
    For the __read command, you translate the command to cmd.exe /c type ...args. If the filename contains a & character (which is valid in Windows paths) but no spaces, Node's spawn may pass it unquoted. cmd.exe will interpret the & as a command separator, leading to a sandbox error or unintended execution.
    Suggestion: Use PowerShell for the __read command as well, leveraging the same script block pattern for safe argument passing:

        command = 'PowerShell.exe';
        args = [
          '-NoProfile',
          '-NonInteractive',
          '-Command',
          '& { Get-Content -LiteralPath $args[0] -Raw }',
          args[0] || '',
        ];
  • packages/core/src/core/contentGenerator.ts & packages/core/src/core/contentGenerator.test.ts:
    The PR includes a new validateBaseUrl function that strictly enforces HTTPS or localhost for baseUrl. This change appears entirely unrelated to the PR's objective of fixing the Plan Mode deadlock.
    Suggestion: Extract this change into a separate PR to keep this PR atomic and focused, or update the PR description to explicitly mention and justify this addition.

Nits (Minor Suggestions)

  • packages/core/src/config/config.ts:
    In the constructor, createSandboxManager is called with workspace: params.targetDir. Since this.targetDir is resolved via path.resolve(params.targetDir) immediately prior, it would be safer and more consistent to pass the fully resolved absolute path (this.targetDir) to the SandboxManager:
        this._sandboxManager = createSandboxManager(
          this.sandbox,
          {
            workspace: this.targetDir, // Use resolved path
            includeDirectories: this.pendingIncludeDirectories,
            policyManager: this._sandboxPolicyManager,
          },
          initialApprovalMode,
        );

const resolved = await tryRealpath(allowedPath);
if (!fs.existsSync(resolved)) {
// If the path doesn't exist, we still want to allow access to its parent
// if it's explicitly allowed, to enable creating it.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a path does not exist, the manager falls back to granting "Low Mandatory Level" to its parent directory using icacls. This modifies the Windows DACL/MIC persistently, making the entire parent directory writable to all Low Integrity processes on the system going forward. If this parent directory happens to be the workspace root, the entire workspace permanently becomes writable in the sandbox.

I think instead of granting low integrity if the path does not exist we should throw an error or warning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

let args = req.args;

// Translate virtual commands for sandboxed file system access
if (command === '__read') {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing arguments as trailing array elements to PowerShell.exe -Command does not behave like typical POSIX shell argument passing. PowerShell concatenates all arguments following -Command into a single string before parsing and executing them.

This introduces two major issues:

 1. Space Splitting: If targetPath contains spaces (e.g., my file.txt), PowerShell evaluates & { ... } my file.txt and splits the path into two arguments (my and file.txt), causing the command to fail because $args[0] only receives my.

 2. Command Injection: A malicious path like foo"; Write-Host "hacked (as seen in the new test case) will be concatenated directly into the command string and executed as arbitrary PowerShell code. The test should safely handle special characters in __write path asserts this vulnerable behavior under the false assumption that passing it as an array to spawn prevents PowerShell from interpolating it.

The safest way to pass paths to a PowerShell script block without injection or splitting issues is to pass the target path via an environment variable, or use -EncodedCommand.

Using an environment variable is very robust and avoids escaping logic:

 if (command === '__write') {
   const targetPath = args[0] || '';
   command = 'PowerShell.exe';
   args = [
     '-NoProfile',
     '-NonInteractive',
     '-Command',
     '& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
   ];
   // You will then need to merge `GEMINI_TARGET_PATH: targetPath` into the returned `env` object at the end of `prepareCommand`.
 }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated WindowsSandboxManager.ts and its test file to safely pass arguments using environment variables, successfully addressing the command injection and space-splitting vulnerabilities on Windows.

Here is a summary of the changes:

Security Fix in WindowsSandboxManager.ts

  • Environment Variable Injection: Instead of passing $args[0] into the PowerShell script block (which was vulnerable to space-splitting and command injection because PowerShell evaluates the array values as part of the
    executed string), the manager now sets GEMINI_TARGET_PATH on the final env object before launching the sandbox helper.
  • Safe Evaluation: The __read and __write command translations now reference the path safely within the script block using $env:GEMINI_TARGET_PATH.
    • For __read: & { Get-Content -LiteralPath $env:GEMINI_TARGET_PATH -Raw }
    • For __write: & { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }

Test Coverage Updates in WindowsSandboxManager.test.ts

  • Updated Expectations: Modified the __read and __write translation tests to look for the updated & { ... $env:GEMINI_TARGET_PATH ... } syntax.
  • Environment Verification: Added assertions to verify that result.env['GEMINI_TARGET_PATH'] is correctly assigned the target file path (including cases with spaces and malicious strings like foo"; echo bar; ".txt),
    confirming that injection via command interpolation is no longer possible.

`Sandbox Error: read_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
);
if (isEnoent) {
// @ts-expect-error - Adding code property to Error object
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of suppressing the error, you can use Object.assign or cast it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. Done.

@DavidAPierce DavidAPierce enabled auto-merge March 30, 2026 21:40
@jerop jerop changed the title fix(core): resolve Plan Mode deadlock during plan file creation fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions Mar 31, 2026
@Adib234
Copy link
Copy Markdown
Contributor

Adib234 commented Mar 31, 2026

One thing I'm curious about is if we turn off tool sandboxing mid-session, what happens? Are the sandbox restrictions still lifted?

@galz10
Copy link
Copy Markdown
Collaborator

galz10 commented Mar 31, 2026

One thing I'm curious about is if we turn off tool sandboxing mid-session, what happens? Are the sandbox restrictions still lifted?

No Require to restart.

@DavidAPierce DavidAPierce added this pull request to the merge queue Mar 31, 2026
Merged via the queue into main with commit 94f9480 Mar 31, 2026
28 checks passed
@DavidAPierce DavidAPierce deleted the plan_mode_loop_fix branch March 31, 2026 22:20
kalenkevich pushed a commit to kalenkevich/gemini-cli that referenced this pull request Apr 3, 2026
afanty2021 pushed a commit to afanty2021/gemini-cli that referenced this pull request Apr 4, 2026
warrenzhu25 pushed a commit to warrenzhu25/gemini-cli that referenced this pull request Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/platform Issues related to Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plan Mode deadlock: cannot create plan file when parent directory doesn't exist due to sandbox restrictions

4 participants