Gemini CLI

Installing the Agent Firewall for Gemini CLI, its own sandbox, and what its hooks cannot see

Edit on GitHub

The Gemini CLI adapter translates Gemini CLI's hook events to the firewall's normalized actions and its answers back. The policy is the same engine Claude Code uses; nothing about what is allowed differs by agent.

Tested with Gemini CLI 0.63.0. Every event shape was captured live against a local mock model that answered with scripted tool calls. No real account or key was used.

Install

launchsafe-firewall install --agent gemini                  # ~/.gemini/settings.json
launchsafe-firewall install --agent gemini --scope project  # <project>/.gemini/settings.json
launchsafe-firewall doctor

install adds one command hook per event, first in each list, keeps every other key and hook, sets hooksConfig.enabled: true, saves the original file under ~/.launchsafe-firewall/backups/ and records the install. Running it again changes nothing. uninstall --agent gemini removes exactly the firewall's entries (and the hooksConfig.enabled flag when install was the one that added it); --restore-backup puts the file back as it was, after saving the current file as settings.json.before-restore.TIMESTAMP.

Gemini CLI's hook timeouts are in milliseconds. Hooks need no feature flag in 0.63.0.

Two things Gemini CLI does weaken any hook, and doctor reports them:

  • Folder trust. In a folder Gemini CLI does not trust it skips every hook, yours and the system's included. A headless run in an untrusted folder exits 55 unless given --skip-trust. Trust the folders you work in.
  • A repository can switch your hooks off. A project .gemini/settings.json with "hooksConfig": {"enabled": false}, or a disabled list naming the firewall's hook (launchsafe-firewall), turned off user-level hooks silently (captured). doctor fails on it, and the agent writing it is denied (FW-TAMPER).

Managed deployment

launchsafe-firewall install --managed --agent gemini --print | sudo tee "/Library/Application Support/GeminiCli/settings.json"   # Linux: /etc/gemini-cli/settings.json

System settings override user and project settings, but Gemini CLI skips the file unless its directory is owned by root. doctor checks the owner. GEMINI_CLI_SYSTEM_SETTINGS_PATH moves it. Gemini CLI has no administrator control that stops a user from adding further hooks, so doctor scores this as partial.

What the adapter maps

Gemini CLI eventFirewall
BeforeToolThe decision
AfterToolTaint, private data and fingerprints from the output
BeforeAgent (carries the prompt)Destinations you named
SessionStartThe session-start agent-configuration scan
SessionEnd, AfterAgent, BeforeModel, AfterModel, BeforeToolSelection, PreCompress, NotificationIgnored (the firewall judges actions, not text)
Gemini CLI toolJudged as
run_shell_commandA shell command (in dir_path when it is not .)
read_fileA read
read_many_files (older versions)One read per path; a glob is a recursive read of its base directory
write_fileA write (result-checked for settings files)
replaceAn edit (allow_multiple replaces every match). Gemini CLI matches old_string flexibly and can correct it from instruction; that was not captured, so the result is judged at every place the text could match, and as the worst case when it matches nowhere
list_directory, glob, grep_searchA listing, a glob and a search
web_fetchOne fetch per URL in the prompt, taken as Gemini CLI takes it: split on whitespace, each URL the whole token, brackets and quotes included. No URL is a parse failure (a person decides)
google_web_searchA web search
save_memory (older versions)A write to ~/.gemini/GEMINI.md: allowed in a clean session, held as a proposal once untrusted
write_todos, update_topic, invoke_agent, activate_skill, list_background_processes, read_background_outputSafe (the agent talking to itself; a subagent's own tool calls are judged one by one)
MCP tools (mcp_<server>_<tool>)mcp__<server>__<tool>; the server's source (system, project or user settings) decides its trust
Anything elseAn unknown tool: held for a person (FW-UNKNOWN-TOOL)

Gemini CLI's own sandbox (gemini -s, macOS)

-s re-runs the whole CLI under sandbox-exec with one profile, and every process it starts inherits it: the firewall's hook and the agent's own tools run in the same sandbox. Gemini CLI's built-in profiles never let a process write ~/.gemini. With the firewall installed and nothing else, every hook fails to save its state, so every non-read action is blocked (captured: 5 of 5 shell commands blocked, reads allowed). The hook says so in one message naming the fix.

The firewall's answer is a custom profile:

launchsafe-firewall install --agent gemini --seatbelt permissive-open
export SEATBELT_PROFILE=launchsafe   # in your shell profile
gemini -s

--seatbelt BASE writes ~/.gemini/sandbox-macos-launchsafe.sb: Gemini CLI's own base profile, copied unchanged from the installed Gemini CLI (found from gemini on PATH, or --gemini-package DIR), plus one block that lets a process write the hook's state directories (sessions, queue, log, locks, ledger, pins, quarantine, scan/receipts, scan/last) and nothing else. Re-run the command after updating Gemini CLI.

Why only those directories: whatever the profile lets the hook write, the agent's commands can write too. Every command is still judged first, exactly as without -s. With the firewall's profile, a command the analyser cannot see through got an error for policy.json, keys/, install.json, identity.json, backups/, canaries.json, the profile itself and ~/.gemini/settings.json, and could write only sessions/ and queue/. So under -s the policy, keys, install records, backups, vault and decoy registry have the OS's protection on top of the analyser's; the hook's state has the analyser's alone, as everywhere.

Not covered: mcp wrap started by Gemini CLI inside -s, the vault inside -s (references in Gemini CLI shell commands are refused anyway), desktop notifications from inside the sandbox (not verified), and Gemini CLI's container sandboxes (Docker, Podman: not tested).

Output protocol (observed)

  • BeforeTool blocks with {"decision": "deny", "reason": "..."} on stdout (exit 0), or exit 2 with text on stderr.
  • Exit 2 with empty stderr, any other exit code, a crash and a timeout let the tool run. The wrapper prints a line before exit 2, so a failure of the firewall or of Node blocks. A timeout (30 s) still fails open.
  • Gemini CLI's hooks cannot put a question to you in a way the firewall can use, so an action that needs a person is held in the queue and the agent is told the exact launchsafe-firewall queue approve <id> command.

What its hooks cannot do

  • No ConfigChange event: a change to settings.json is judged only when the agent makes it through a tool, not when something else edits the file mid-session.
  • Secrets in tool output are not redacted.
  • Subagents (invoke_agent) are not reported as sessions; their tool calls arrive with the parent's session_id, so their taint is the session's.
  • A tool call Gemini CLI rejects before the hook never reaches the firewall, and never runs either.
  • Unattended runs (gemini -p, --yolo, --approval-mode yolo) are detected from the process command line only, because Gemini CLI tells its hooks nothing about its mode. The firewall reads it with ps, up to four ancestors, and falls back to pgrep on macOS where a seatbelt sandbox forbids ps. When neither can run, the session is treated as unattended. Another agent starting gemini --yolo is also caught by FW-AGENT-BYPASS.

On this page