personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix Gemini provider write guard: --allowed-tools was deprecated and ineffective

The Gemini CLI deprecated --allowed-tools — it only controlled auto-approval,
not tool availability. Combined with --yolo, non-write agents had unrestricted
access to write_file, edit_file, replace, etc. This was the root cause of
agents self-modifying their own prompts, overwriting sol/identity.md, creating
scripts, and mutating test fixtures.

Replace with --approval-mode: "plan" (read-only) for non-write agents, "yolo"
(full access) for write-enabled agents only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+26 -22
+9 -8
tests/test_cogitate_coder.py
··· 141 141 142 142 143 143 class TestGoogleWriteFlag: 144 - """Verify --allowed-tools is controlled by config write flag.""" 144 + """Verify --approval-mode is controlled by config write flag.""" 145 145 146 146 def _provider(self): 147 147 return importlib.import_module("think.providers.google") 148 148 149 149 @patch("think.providers.google.CLIRunner") 150 - def test_no_write_restricts_tools(self, mock_runner_cls): 151 - """Without write flag, --allowed-tools restricts to sol.""" 150 + def test_no_write_uses_plan_mode(self, mock_runner_cls): 151 + """Without write flag, approval-mode is plan (read-only).""" 152 152 provider = self._provider() 153 153 mock_instance = AsyncMock() 154 154 mock_instance.run = AsyncMock(return_value="result") ··· 159 159 asyncio.run(provider.run_cogitate(config)) 160 160 161 161 cmd = mock_runner_cls.call_args.kwargs["cmd"] 162 - assert "--allowed-tools" in cmd 163 - assert "run_shell_command(sol)" in cmd 162 + idx = cmd.index("--approval-mode") 163 + assert cmd[idx + 1] == "plan" 164 164 165 165 @patch("think.providers.google.CLIRunner") 166 - def test_write_true_grants_full_access(self, mock_runner_cls): 167 - """With write=True, --allowed-tools is omitted.""" 166 + def test_write_true_uses_yolo_mode(self, mock_runner_cls): 167 + """With write=True, approval-mode is yolo (full access).""" 168 168 provider = self._provider() 169 169 mock_instance = AsyncMock() 170 170 mock_instance.run = AsyncMock(return_value="result") ··· 175 175 asyncio.run(provider.run_cogitate(config)) 176 176 177 177 cmd = mock_runner_cls.call_args.kwargs["cmd"] 178 - assert "--allowed-tools" not in cmd 178 + idx = cmd.index("--approval-mode") 179 + assert cmd[idx + 1] == "yolo" 179 180 180 181 181 182 # ---------------------------------------------------------------------------
+7 -7
tests/test_google_cli.py
··· 16 16 return importlib.reload(importlib.import_module("think.providers.google")) 17 17 18 18 19 - def _assert_write_mode_removes_allowed_tools(make_runner): 19 + def _assert_write_mode_uses_yolo_approval(make_runner): 20 20 provider = _google_provider() 21 21 MockCLIRunner = make_runner() 22 22 with patch("think.providers.google.CLIRunner", MockCLIRunner): ··· 27 27 ) 28 28 ) 29 29 cmd = MockCLIRunner.last_instance.cmd 30 - assert "--yolo" in cmd 31 - assert "--allowed-tools" not in cmd 30 + idx = cmd.index("--approval-mode") 31 + assert cmd[idx + 1] == "yolo" 32 32 33 33 34 34 class TestTranslateGemini: ··· 339 339 ) 340 340 ) 341 341 cmd = MockCLIRunner.last_instance.cmd 342 - assert "--yolo" in cmd 343 - assert cmd[cmd.index("--allowed-tools") + 1] == "run_shell_command(sol)" 342 + idx = cmd.index("--approval-mode") 343 + assert cmd[idx + 1] == "plan" 344 344 345 - def test_write_mode_removes_allowed_tools(self): 346 - _assert_write_mode_removes_allowed_tools(self._mock_runner) 345 + def test_write_mode_uses_yolo_approval(self): 346 + _assert_write_mode_uses_yolo_approval(self._mock_runner) 347 347 348 348 def test_sandbox_none(self): 349 349 provider = _google_provider()
+10 -7
think/providers/google.py
··· 693 693 if system_instruction: 694 694 prompt_body = system_instruction + "\n\n" + prompt_body 695 695 696 - # Build CLI command — yolo mode auto-approves all tool calls 697 - # (required for headless subprocess use). 696 + # Build CLI command. approval-mode controls tool access: 697 + # "yolo" — auto-approve all tools (write-enabled agents only) 698 + # "plan" — read-only mode (no file writes, no destructive tools) 699 + # The deprecated --allowed-tools flag did NOT restrict tool 700 + # availability, only auto-approval — combined with --yolo it 701 + # provided zero protection. --approval-mode plan is the 702 + # replacement that actually enforces read-only. 703 + approval = "yolo" if config.get("write") else "plan" 698 704 cmd = [ 699 705 "gemini", 700 706 "-p", 701 707 "-", 702 708 "-o", 703 709 "stream-json", 704 - "--yolo", 710 + "--approval-mode", 711 + approval, 705 712 "-m", 706 713 model, 707 714 "--sandbox=none", 708 715 ] 709 - 710 - # Restrict tool access unless write mode is enabled 711 - if not config.get("write"): 712 - cmd.extend(["--allowed-tools", "run_shell_command(sol)"]) 713 716 714 717 # Resume from previous session if continuing 715 718 if session_id: