All articles
GoAI AgentsLXC ContainersMCPSecurityOpen Source

What I Learned Sending 10 Pull Requests to Gemini CLI

Finding root causes, designing LXC sandboxes, preventing state races in MCP servers, and contributing to Google's open-source terminal AI agent.

Aug 2026
8 min read

The Philosophy: Fixing Root Causes, Not Symptoms

I don't like fixing bugs by just making the error go away. I like to find the real reason something broke, then make the smallest, safest change that fixes that reason.

Over the last few weeks I sent ten pull requests to google-gemini/gemini-cli, an open-source AI agent that runs Google's Gemini model in your terminal. Five got merged, and five were closed without merging — which is completely normal in a large, fast-moving open-source repository. This post walks through what I found, what I built, and why I built it that way.

Infographic 01 — Gemini CLI Architecture & PR Map
Gemini CLI Architecture & PR Impact Map
Click to inspect
Taxonomy of 10 pull requests spanning MCP connection ordering, LXC sandboxing, agent steering, and CLI UX resilience.
PRWhat It FixesStatus
#19782Config gets overwritten when server names collide✅ Merged
#20126Restart ignores updated server settings✅ Merged
#20268New config field breaks older CLI versionsClosed, not merged
#20735New sandbox option using LXC containers✅ Merged
#20430Give hooks their own security rulesClosed, not merged
#20202Model breaks code by escaping backslashes wrongClosed, not merged
#20173Model writes scripts instead of using the file toolClosed, not merged
#24914Add a shorter, clearer flag nameClosed, not merged
#20287Command crashes when a file is missing✅ Merged
#20692Confusing npm warnings, explained in docs✅ Merged

Bugs Caused by Doing Things in the Wrong Order

Two of my merged PRs were both about the same kind of mistake: code that did the right steps, just in the wrong order. This is the class of bug that's easy to miss in review because each line looks fine on its own — you only catch it by tracing the exact sequence of what happens when.

#19782 — An active connection was getting silently destroyed. Gemini CLI manages connections to MCP tool servers. The code was saving a new server's settings into its config map before checking if that server's name already matched one the user had set up. If it matched, the user's original settings got overwritten right there. If the new connection then failed to start, the user was left with nothing — no working server, and no easy way to know why. My fix moved the name check to run before the save, so a bad state is never created in the first place. I didn't touch the check itself, just where it runs. That's the safest kind of fix: change less, not more.

#20126 — Restarting a server ignored your new settings. If you changed a server's command or settings and restarted it, the CLI kept using the old, stale settings. Digging into why, I found the server's settings were stored as a value that's never allowed to change once the object is created. So even if you wanted to 'update' the running object, the code couldn't — it was built that way on purpose. Once I saw that, the fix was clear: stop trying to reuse the old object. Always throw it away and build a new one with the current settings. Sometimes the fix isn't a clever trick — it's noticing what the existing code was already telling you to do.

#20268 — A well-meaning change broke older CLIs (not merged). A newer feature started saving a type field into the shared settings file. But older CLI versions didn't know that field and crashed when they saw it. Since this settings file is often shared across a whole team, I reverted to the older field name instead. A cleaner-looking config isn't worth breaking everyone still on an older version — compatibility won here.

Infographic 02 — MCP Lifecycle & State Ordering
MCP Lifecycle & State Ordering Diagram
Click to inspect
Pre-validation before config mutations and enforcing clean instance recreation on server restarts.
Engineering Invariant
Never mutate shared configuration state until all validation gates have completed. If an underlying connection client is immutable by design, discard and recreate it rather than attempting in-place field updates.

Adding a New Sandbox Option (#20735, Merged)

Gemini CLI runs risky commands inside a sandbox — Docker, Podman, or Apple's Seatbelt. But none of those can run a full Linux system with systemd and snapd inside, which some packaging tools genuinely need. So there was a real gap: no safe way to use Gemini CLI for that kind of work.

My design choice: the CLI does not build or manage the container. The user creates their own LXC container ahead of time, and Gemini just runs inside it. I share the workspace folder with the container at the exact same path, so file paths the model works with don't need to be translated. This fits the sandbox system that was already there instead of building something new next to it.

One decision I want to highlight: this sandbox only turns on if the user explicitly asks for it. It is never auto-detected, unlike Docker or Podman. That's a safety choice. Docker and Podman can be started automatically because the CLI fully controls them. LXC needs a container the user already built and is running — auto-picking it could run code somewhere the user didn't expect. Being explicit here matters more than being convenient.

Infographic 03 — LXC Sandbox Architecture
LXC Container Sandbox Architecture Diagram
Click to inspect
Sub-second container execution, 1:1 workspace path mirroring, and explicit opt-in safety boundaries.

Making the AI Use Its Own Tools Correctly

Two of my PRs weren't code bugs at all — they were about getting the model to behave the way it's supposed to.

#20202 — The model was breaking code it wrote (not merged). When the model wrote code using tools like write_file, it sometimes didn't escape backslashes and newline characters properly inside the data it sent. The result: a line like printf("Hello\n"); would get saved with an actual line break instead of the two characters \ and n. This silently broke the generated code. There's no code fix for this, because the data being sent was technically valid — the model just needed clearer instructions. So I added a clear warning directly into the tool's instructions, telling the model exactly how to escape these characters. It's a soft fix, not a hard guarantee, and I think that's worth being upfront about: it guides the model, it doesn't force it.

#20173 — The model was writing scripts to avoid using the file tool (not merged). I noticed the model would sometimes write a small Python script or a shell command just to create a file, instead of calling the built-in write_file tool directly. That's slower and less predictable. I noticed the codebase already had a fix for this exact pattern with a different tool (search), marked as 'preferred' in its instructions. So instead of inventing a new approach, I followed the one already in use. Matching existing patterns keeps the whole prompt easier to maintain — a new idea from me would have just made the next reader's job harder.

Letting Hooks Enforce Their Own Security Rules (#20430, Not Merged)

Gemini CLI runs hooks — small scripts that fire before or after a tool runs. Before my change, the only way to block a hook was to fake it as a regular tool call, just so it could reuse the existing security rules meant for tools. That's a workaround wearing a costume, not a real design.

I added real support for hook-specific rules inside the CLI's policy files, and taught the security engine to check hooks directly, without the disguise. One rule I built in on purpose: if a security rule says 'ask the user,' and a hook fires with no user around to ask (this happens in automated, non-interactive runs), the answer defaults to 'no,' not 'yes.' When there's no one to confirm an action, the safe default is to block it, not allow it.

Infographic 04 — Agent Steering & Hook Security
Agent Steering & Hook Security Policies Diagram
Click to inspect
Dedicated security rule parser for lifecycle hooks with fail-closed non-interactive defaults.

Smaller, Focused Fixes & CLI Resilience

Not every problem needs a big design. A few of these were small fixes tied to one clear, reported issue:

#24914 — A clearer flag name: The CLI already had a way to add extra working directories, but the flag name wasn't obvious. I added --workspace and a short -W. I couldn't use lowercase -w — it was already taken by another flag. Small detail, but it's the kind of thing you only catch by actually checking what already exists before adding something new.

#20287 — A command that crashed over one missing file (merged): A setup command downloaded a fixed list of files from the latest release. When one file was missing from a release, the whole command crashed. I made it skip missing files quietly, but still fail loudly on real errors like server errors or permission errors. The goal was to tolerate a small, expected gap without hiding a real problem if one showed up.

#20692 — Turning confusion into documentation (merged): Users kept seeing scary-looking npm warning messages and assumed something was broken. It wasn't — the warnings were about old, unused code paths that don't even run given the Node.js version the CLI requires. Instead of brittle dependency overrides, I wrote one clear paragraph in the docs explaining that the warning is safe to ignore. Not every problem needs code.

Infographic 05 — CLI Resilience & Documentation
CLI UX Resilience & Documentation Diagram
Click to inspect
Fault-tolerant download pipelines and proactive developer documentation.

The Thread Through All of It

Looking back, most of these fixes share the same habit: find where the actual mistake happens, then fix that one spot instead of rewriting the surrounding code. When safety was involved — the sandbox, the hook rules — I chose the option that fails safely by default, even if it's less convenient. And more than once, the right fix was to reuse a pattern the codebase already had, instead of inventing a new one.

That's the kind of engineering judgment that doesn't show up in a diff count, but it's the part I care most about getting right.

Core Takeaway
Open-source engineering is about minimizing blast radius. The best pull requests don't introduce grand new abstractions; they identify the exact point of failure and restore the system's invariants with minimal disturbance.
Himanshu Soni
Himanshu Soni
Core Contributor @ Gemini CLI • Building full-stack systems, AI agents, and developer tools.