diff --git a/.github/workflows/smoke-gemini.lock.yml b/.github/workflows/smoke-gemini.lock.yml
index 6d077ada7c9..d1d6955c678 100644
--- a/.github/workflows/smoke-gemini.lock.yml
+++ b/.github/workflows/smoke-gemini.lock.yml
@@ -970,6 +970,17 @@ jobs:
           path: /tmp/gh-aw/aw-prompts
       - name: Clean git credentials
         run: bash /opt/gh-aw/actions/clean_git_credentials.sh
+      - name: Write Gemini settings
+        run: |
+          mkdir -p "$GITHUB_WORKSPACE/.gemini"
+          SETTINGS="$GITHUB_WORKSPACE/.gemini/settings.json"
+          BASE_CONFIG='{"context":{"includeDirectories":["/tmp/"]},"tools":{"core":["glob","grep_search","list_directory","read_file","read_many_files","replace","run_shell_command","write_file"]}}'
+          if [ -f "$SETTINGS" ]; then
+            MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$SETTINGS")" '$existing * $base')
+            echo "$MERGED" > "$SETTINGS"
+          else
+            echo "$BASE_CONFIG" > "$SETTINGS"
+          fi
       - name: Execute Gemini CLI
         id: agentic_execution
         run: |
@@ -1298,6 +1309,17 @@ jobs:
           package-manager-cache: false
       - name: Install Gemini CLI
         run: npm install -g --silent @google/gemini-cli@0.29.0
+      - name: Write Gemini settings
+        run: |
+          mkdir -p "$GITHUB_WORKSPACE/.gemini"
+          SETTINGS="$GITHUB_WORKSPACE/.gemini/settings.json"
+          BASE_CONFIG='{"context":{"includeDirectories":["/tmp/"]},"tools":{"core":["glob","grep_search","list_directory","read_file","read_many_files","run_shell_command(cat)","run_shell_command(grep)","run_shell_command(head)","run_shell_command(jq)","run_shell_command(ls)","run_shell_command(tail)","run_shell_command(wc)"]}}'
+          if [ -f "$SETTINGS" ]; then
+            MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$SETTINGS")" '$existing * $base')
+            echo "$MERGED" > "$SETTINGS"
+          else
+            echo "$BASE_CONFIG" > "$SETTINGS"
+          fi
       - name: Execute Gemini CLI
         id: agentic_execution
         run: |
diff --git a/actions/setup/sh/convert_gateway_config_gemini.sh b/actions/setup/sh/convert_gateway_config_gemini.sh
index ce2aa4cdd21..4b2b14d5711 100644
--- a/actions/setup/sh/convert_gateway_config_gemini.sh
+++ b/actions/setup/sh/convert_gateway_config_gemini.sh
@@ -95,8 +95,8 @@ jq --arg urlPrefix "$URL_PREFIX" '
       .url |= (. | sub("^http://[^/]+/mcp/"; $urlPrefix + "/mcp/"))
     )
   ) |
-  # Allow Gemini CLI to read files from /tmp/gh-aw/ (e.g. MCP payload files)
-  .includeDirectories = ["/tmp/gh-aw/"]
+  # Allow Gemini CLI to read/write files from /tmp/ (e.g. MCP payload files, cache-memory, agent outputs)
+  .context.includeDirectories = ["/tmp/"]
 ' "$MCP_GATEWAY_OUTPUT" > "$GEMINI_SETTINGS_FILE"
 
 echo "Gemini configuration written to $GEMINI_SETTINGS_FILE"
diff --git a/pkg/workflow/enable_api_proxy_test.go b/pkg/workflow/enable_api_proxy_test.go
index 6d86cc0a8f5..e23329bc57d 100644
--- a/pkg/workflow/enable_api_proxy_test.go
+++ b/pkg/workflow/enable_api_proxy_test.go
@@ -105,11 +105,12 @@ func TestEngineAWFEnableApiProxy(t *testing.T) {
 		engine := NewGeminiEngine()
 		steps := engine.GetExecutionSteps(workflowData, "test.log")
 
-		if len(steps) == 0 {
-			t.Fatal("Expected at least one execution step")
+		if len(steps) < 2 {
+			t.Fatal("Expected at least two execution steps (settings + execution)")
 		}
 
-		stepContent := strings.Join(steps[0], "\n")
+		// steps[0] = Write Gemini settings, steps[1] = Execute Gemini CLI
+		stepContent := strings.Join(steps[1], "\n")
 
 		if !strings.Contains(stepContent, "--enable-api-proxy") {
 			t.Error("Expected Gemini AWF command to contain '--enable-api-proxy' flag")
diff --git a/pkg/workflow/gemini_engine.go b/pkg/workflow/gemini_engine.go
index 2d7c23a4560..d6d746c0f0e 100644
--- a/pkg/workflow/gemini_engine.go
+++ b/pkg/workflow/gemini_engine.go
@@ -174,6 +174,12 @@ func (e *GeminiEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str
 
 	var steps []GitHubActionStep
 
+	// Write .gemini/settings.json with context.includeDirectories and tools.core.
+	// This step runs after the MCP gateway setup (which may have written mcpServers config)
+	// and merges the context/tools settings into any existing settings.json.
+	settingsStep := e.generateGeminiSettingsStep(workflowData)
+	steps = append(steps, settingsStep)
+
 	// Build gemini CLI arguments based on configuration
 	var geminiArgs []string
 
diff --git a/pkg/workflow/gemini_engine_test.go b/pkg/workflow/gemini_engine_test.go
index c1aaf40ba8e..a7dcbee85d4 100644
--- a/pkg/workflow/gemini_engine_test.go
+++ b/pkg/workflow/gemini_engine_test.go
@@ -145,9 +145,10 @@ func TestGeminiEngineExecution(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
-		require.Len(t, steps, 1, "Should generate one execution step")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
 
-		stepContent := strings.Join(steps[0], "\n")
+		// steps[0] = Write Gemini settings, steps[1] = Execute Gemini CLI
+		stepContent := strings.Join(steps[1], "\n")
 
 		assert.Contains(t, stepContent, "name: Execute Gemini CLI", "Should have correct step name")
 		assert.Contains(t, stepContent, "id: agentic_execution", "Should have agentic_execution ID")
@@ -168,9 +169,9 @@ func TestGeminiEngineExecution(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
-		require.Len(t, steps, 1, "Should generate one execution step")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
 
-		stepContent := strings.Join(steps[0], "\n")
+		stepContent := strings.Join(steps[1], "\n")
 
 		// Model is passed via the native GEMINI_MODEL env var (not as a --model flag)
 		assert.Contains(t, stepContent, "GEMINI_MODEL: gemini-1.5-pro", "Should set GEMINI_MODEL env var")
@@ -189,9 +190,9 @@ func TestGeminiEngineExecution(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
-		require.Len(t, steps, 1, "Should generate one execution step")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
 
-		stepContent := strings.Join(steps[0], "\n")
+		stepContent := strings.Join(steps[1], "\n")
 
 		// Gemini CLI reads MCP config from .gemini/settings.json, not --mcp-config flag
 		assert.NotContains(t, stepContent, "--mcp-config", "Should NOT include --mcp-config flag (Gemini CLI does not support it)")
@@ -207,9 +208,9 @@ func TestGeminiEngineExecution(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
-		require.Len(t, steps, 1, "Should generate one execution step")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
 
-		stepContent := strings.Join(steps[0], "\n")
+		stepContent := strings.Join(steps[1], "\n")
 
 		assert.Contains(t, stepContent, "/custom/gemini", "Should use custom command")
 	})
@@ -220,9 +221,9 @@ func TestGeminiEngineExecution(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
-		require.Len(t, steps, 1, "Should generate one execution step")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
 
-		stepContent := strings.Join(steps[0], "\n")
+		stepContent := strings.Join(steps[1], "\n")
 
 		assert.Contains(t, stepContent, "GEMINI_API_KEY:", "Should include GEMINI_API_KEY")
 		assert.Contains(t, stepContent, "GH_AW_PROMPT:", "Should include GH_AW_PROMPT")
@@ -238,8 +239,8 @@ func TestGeminiEngineExecution(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(noModelWorkflow, "/tmp/test.log")
-		require.Len(t, steps, 1)
-		stepContent := strings.Join(steps[0], "\n")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
+		stepContent := strings.Join(steps[1], "\n")
 		assert.NotContains(t, stepContent, "GH_AW_MODEL_DETECTION_GEMINI", "Should not include detection model env var when model is unconfigured")
 		assert.NotContains(t, stepContent, "GH_AW_MODEL_AGENT_GEMINI", "Should not include agent model env var when model is unconfigured")
 		assert.NotContains(t, stepContent, "GEMINI_MODEL", "Should not include GEMINI_MODEL when model is unconfigured")
@@ -253,10 +254,27 @@ func TestGeminiEngineExecution(t *testing.T) {
 		}
 
 		steps = engine.GetExecutionSteps(modelWorkflow, "/tmp/test.log")
-		require.Len(t, steps, 1)
-		stepContent = strings.Join(steps[0], "\n")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
+		stepContent = strings.Join(steps[1], "\n")
 		assert.Contains(t, stepContent, "GEMINI_MODEL: gemini-2.0-flash", "Should set GEMINI_MODEL when model is explicitly configured")
 	})
+
+	t.Run("settings step is first", func(t *testing.T) {
+		workflowData := &WorkflowData{
+			Name: "test-workflow",
+		}
+
+		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
+
+		settingsContent := strings.Join(steps[0], "\n")
+		execContent := strings.Join(steps[1], "\n")
+
+		assert.Contains(t, settingsContent, "Write Gemini settings", "First step should be Write Gemini settings")
+		assert.Contains(t, settingsContent, "includeDirectories", "Settings step should set includeDirectories")
+		assert.Contains(t, settingsContent, "/tmp/", "Settings step should include /tmp/ in include directories")
+		assert.Contains(t, execContent, "Execute Gemini CLI", "Second step should be Execute Gemini CLI")
+	})
 }
 
 func TestGeminiEngineFirewallIntegration(t *testing.T) {
@@ -274,9 +292,9 @@ func TestGeminiEngineFirewallIntegration(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
-		require.Len(t, steps, 1, "Should generate one execution step")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
 
-		stepContent := strings.Join(steps[0], "\n")
+		stepContent := strings.Join(steps[1], "\n")
 
 		// Should use AWF command
 		assert.Contains(t, stepContent, "awf", "Should use AWF when firewall is enabled")
@@ -296,9 +314,9 @@ func TestGeminiEngineFirewallIntegration(t *testing.T) {
 		}
 
 		steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log")
-		require.Len(t, steps, 1, "Should generate one execution step")
+		require.Len(t, steps, 2, "Should generate settings step and execution step")
 
-		stepContent := strings.Join(steps[0], "\n")
+		stepContent := strings.Join(steps[1], "\n")
 
 		// Should use simple command without AWF
 		assert.Contains(t, stepContent, "set -o pipefail", "Should use simple command with pipefail")
@@ -306,3 +324,152 @@ func TestGeminiEngineFirewallIntegration(t *testing.T) {
 		assert.NotContains(t, stepContent, "GEMINI_API_BASE_URL", "Should not set GEMINI_API_BASE_URL when firewall is disabled")
 	})
 }
+
+func TestComputeGeminiToolsCore(t *testing.T) {
+	t.Run("nil tools includes default read-only tools", func(t *testing.T) {
+		result := computeGeminiToolsCore(nil)
+		assert.Contains(t, result, "glob", "Should include glob")
+		assert.Contains(t, result, "grep_search", "Should include grep_search")
+		assert.Contains(t, result, "list_directory", "Should include list_directory")
+		assert.Contains(t, result, "read_file", "Should include read_file")
+		assert.Contains(t, result, "read_many_files", "Should include read_many_files")
+		assert.NotContains(t, result, "run_shell_command", "Should not include run_shell_command without bash tool")
+		assert.NotContains(t, result, "write_file", "Should not include write_file without edit tool")
+		assert.NotContains(t, result, "replace", "Should not include replace without edit tool")
+	})
+
+	t.Run("empty tools includes default read-only tools", func(t *testing.T) {
+		result := computeGeminiToolsCore(map[string]any{})
+		assert.Contains(t, result, "read_file", "Should include read_file")
+		assert.NotContains(t, result, "run_shell_command", "Should not include run_shell_command")
+	})
+
+	t.Run("bash with specific commands maps to run_shell_command entries", func(t *testing.T) {
+		tools := map[string]any{
+			"bash": []any{"grep", "ls", "git"},
+		}
+		result := computeGeminiToolsCore(tools)
+		assert.Contains(t, result, "run_shell_command(grep)", "Should map grep to run_shell_command(grep)")
+		assert.Contains(t, result, "run_shell_command(ls)", "Should map ls to run_shell_command(ls)")
+		assert.Contains(t, result, "run_shell_command(git)", "Should map git to run_shell_command(git)")
+		assert.NotContains(t, result, "run_shell_command", "Should not include unrestricted run_shell_command")
+	})
+
+	t.Run("bash with wildcard * maps to unrestricted run_shell_command", func(t *testing.T) {
+		tools := map[string]any{
+			"bash": []any{"*"},
+		}
+		result := computeGeminiToolsCore(tools)
+		assert.Contains(t, result, "run_shell_command", "Should include unrestricted run_shell_command for wildcard")
+		assert.NotContains(t, result, "run_shell_command(*)", "Should not include run_shell_command(*)")
+	})
+
+	t.Run("bash with :* wildcard maps to unrestricted run_shell_command", func(t *testing.T) {
+		tools := map[string]any{
+			"bash": []any{":*"},
+		}
+		result := computeGeminiToolsCore(tools)
+		assert.Contains(t, result, "run_shell_command", "Should include unrestricted run_shell_command for :* wildcard")
+	})
+
+	t.Run("bash with no specific commands (nil) maps to unrestricted run_shell_command", func(t *testing.T) {
+		tools := map[string]any{
+			"bash": nil,
+		}
+		result := computeGeminiToolsCore(tools)
+		assert.Contains(t, result, "run_shell_command", "Should include unrestricted run_shell_command when bash has no commands")
+	})
+
+	t.Run("edit tool maps to write_file and replace", func(t *testing.T) {
+		tools := map[string]any{
+			"edit": map[string]any{},
+		}
+		result := computeGeminiToolsCore(tools)
+		assert.Contains(t, result, "write_file", "Should map edit to write_file")
+		assert.Contains(t, result, "replace", "Should map edit to replace")
+	})
+
+	t.Run("combined bash and edit tools", func(t *testing.T) {
+		tools := map[string]any{
+			"bash": []any{"grep"},
+			"edit": map[string]any{},
+		}
+		result := computeGeminiToolsCore(tools)
+		assert.Contains(t, result, "run_shell_command(grep)", "Should include run_shell_command(grep)")
+		assert.Contains(t, result, "write_file", "Should include write_file")
+		assert.Contains(t, result, "replace", "Should include replace")
+		assert.Contains(t, result, "read_file", "Should always include read_file")
+	})
+
+	t.Run("result is sorted", func(t *testing.T) {
+		tools := map[string]any{
+			"bash": []any{"zzz", "aaa"},
+			"edit": map[string]any{},
+		}
+		result := computeGeminiToolsCore(tools)
+		for i := 1; i < len(result); i++ {
+			assert.LessOrEqual(t, result[i-1], result[i], "Tools should be sorted alphabetically")
+		}
+	})
+}
+
+func TestGenerateGeminiSettingsStep(t *testing.T) {
+	engine := NewGeminiEngine()
+
+	t.Run("step sets context.includeDirectories to /tmp/", func(t *testing.T) {
+		workflowData := &WorkflowData{
+			Name:  "test-workflow",
+			Tools: map[string]any{},
+		}
+		step := engine.generateGeminiSettingsStep(workflowData)
+		content := strings.Join(step, "\n")
+
+		assert.Contains(t, content, "Write Gemini settings", "Should have correct step name")
+		assert.Contains(t, content, "/tmp/", "Should include /tmp/ in include directories")
+		assert.Contains(t, content, "includeDirectories", "Should set includeDirectories")
+		assert.Contains(t, content, ".gemini", "Should reference .gemini directory")
+		assert.Contains(t, content, "GITHUB_WORKSPACE", "Should use GITHUB_WORKSPACE")
+	})
+
+	t.Run("step includes merge logic for existing settings.json", func(t *testing.T) {
+		workflowData := &WorkflowData{
+			Name:  "test-workflow",
+			Tools: map[string]any{},
+		}
+		step := engine.generateGeminiSettingsStep(workflowData)
+		content := strings.Join(step, "\n")
+
+		assert.Contains(t, content, "if [ -f", "Should check for existing settings.json")
+		assert.Contains(t, content, "jq", "Should use jq for merging")
+		assert.Contains(t, content, "$existing * $base", "Should merge with base taking precedence")
+	})
+
+	t.Run("step includes tools.core with bash mapping", func(t *testing.T) {
+		workflowData := &WorkflowData{
+			Name: "test-workflow",
+			Tools: map[string]any{
+				"bash": []any{"grep", "git"},
+			},
+		}
+		step := engine.generateGeminiSettingsStep(workflowData)
+		content := strings.Join(step, "\n")
+
+		assert.Contains(t, content, "run_shell_command(grep)", "Should include run_shell_command(grep) for bash grep")
+		assert.Contains(t, content, "run_shell_command(git)", "Should include run_shell_command(git) for bash git")
+		assert.Contains(t, content, "core", "Should include tools.core")
+	})
+
+	t.Run("step includes tools.core with edit mapping", func(t *testing.T) {
+		workflowData := &WorkflowData{
+			Name: "test-workflow",
+			Tools: map[string]any{
+				"edit": map[string]any{},
+			},
+		}
+		step := engine.generateGeminiSettingsStep(workflowData)
+		content := strings.Join(step, "\n")
+
+		assert.Contains(t, content, "write_file", "Should include write_file for edit tool")
+		assert.Contains(t, content, "replace", "Should include replace for edit tool")
+	})
+}
diff --git a/pkg/workflow/gemini_tools.go b/pkg/workflow/gemini_tools.go
new file mode 100644
index 00000000000..9c563a2bdde
--- /dev/null
+++ b/pkg/workflow/gemini_tools.go
@@ -0,0 +1,173 @@
+package workflow
+
+// This file provides Gemini engine tool configuration logic.
+//
+// It handles two key responsibilities:
+//
+//  1. Tool Core Mapping (computeGeminiToolsCore):
+//     Converts neutral tool names from the workflow configuration into
+//     Gemini CLI built-in tool names for the tools.core allowlist in
+//     .gemini/settings.json. This restricts the agent to only the tools
+//     explicitly requested by the workflow.
+//
+//  2. Settings Step Generation (generateGeminiSettingsStep):
+//     Generates a GitHub Actions step that writes or merges .gemini/settings.json
+//     before the Gemini CLI execution. This step always sets:
+//     - context.includeDirectories: ["/tmp/"] so file tools can access /tmp/
+//     - tools.core: derived from neutral tool configuration
+//     The merge approach ensures MCP server config (written by convert_gateway_config_gemini.sh)
+//     is preserved while adding the context and tool settings.
+
+import (
+	"encoding/json"
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/github/gh-aw/pkg/logger"
+)
+
+var geminiToolsLog = logger.New("workflow:gemini_tools")
+
+// computeGeminiToolsCore maps neutral tool names to Gemini CLI built-in tool names
+// for use in the tools.core allowlist in .gemini/settings.json.
+//
+// Neutral tool → Gemini CLI tool mapping:
+//   - bash: [cmd, ...]     → run_shell_command(cmd), ... (one entry per command)
+//   - bash: * or bash: nil → run_shell_command           (allow all shell commands)
+//   - edit: {}             → replace, write_file          (file write tools)
+//
+// Read-only file system tools are always included as they are essential for
+// agentic workflows: glob, grep_search, list_directory, read_file, read_many_files.
+//
+// See: https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/file-system.md
+// See: https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/shell.md
+func computeGeminiToolsCore(tools map[string]any) []string {
+	// Always include essential read-only file system tools
+	toolsCore := []string{
+		"glob",
+		"grep_search",
+		"list_directory",
+		"read_file",
+		"read_many_files",
+	}
+
+	if tools == nil {
+		return toolsCore
+	}
+
+	// Map bash neutral tool to run_shell_command
+	if bashConfig, hasBash := tools["bash"]; hasBash {
+		bashCommands, ok := bashConfig.([]any)
+		if !ok || len(bashCommands) == 0 {
+			// bash with no specific commands - allow all shell commands
+			geminiToolsLog.Print("bash (no specific commands) → run_shell_command")
+			toolsCore = append(toolsCore, "run_shell_command")
+		} else {
+			// Check for wildcard (* or :*)
+			hasWildcard := false
+			for _, cmd := range bashCommands {
+				if cmdStr, ok := cmd.(string); ok && (cmdStr == "*" || cmdStr == ":*") {
+					hasWildcard = true
+					break
+				}
+			}
+			if hasWildcard {
+				geminiToolsLog.Print("bash wildcard → run_shell_command")
+				toolsCore = append(toolsCore, "run_shell_command")
+			} else {
+				// Add an entry for each specific command: run_shell_command(cmd)
+				for _, cmd := range bashCommands {
+					if cmdStr, ok := cmd.(string); ok {
+						entry := fmt.Sprintf("run_shell_command(%s)", cmdStr)
+						geminiToolsLog.Printf("bash %q → %s", cmdStr, entry)
+						toolsCore = append(toolsCore, entry)
+					}
+				}
+			}
+		}
+	}
+
+	// Map edit neutral tool to write_file and replace (Gemini's file write tools)
+	if _, hasEdit := tools["edit"]; hasEdit {
+		geminiToolsLog.Print("edit → replace, write_file")
+		toolsCore = append(toolsCore, "replace")
+		toolsCore = append(toolsCore, "write_file")
+	}
+
+	sort.Strings(toolsCore)
+	return toolsCore
+}
+
+// generateGeminiSettingsStep creates a GitHub Actions step that writes the
+// Gemini CLI project settings file (.gemini/settings.json) before execution.
+//
+// This step:
+//  1. Sets context.includeDirectories to ["/tmp/"] so that Gemini CLI file system
+//     tools (write_file, replace) can access files in /tmp/ including
+//     /tmp/gh-aw/cache-memory/ and other agent working directories.
+//  2. Sets tools.core to the list of built-in tools derived from the workflow's
+//     neutral tool configuration (bash → run_shell_command, edit → write_file/replace).
+//  3. Merges the above settings with any existing .gemini/settings.json, which
+//     may have been written by convert_gateway_config_gemini.sh with MCP server
+//     configuration. The merge preserves the MCP server config while adding
+//     the context and tools settings.
+func (e *GeminiEngine) generateGeminiSettingsStep(workflowData *WorkflowData) GitHubActionStep {
+	geminiToolsLog.Printf("Generating Gemini settings step for: %s", workflowData.Name)
+
+	tools := workflowData.Tools
+	if tools == nil {
+		tools = make(map[string]any)
+	}
+
+	// Compute tools.core from neutral tool configuration
+	toolsCore := computeGeminiToolsCore(tools)
+	geminiToolsLog.Printf("tools.core entries: %d", len(toolsCore))
+
+	// Build the settings JSON object
+	config := map[string]any{
+		"context": map[string]any{
+			"includeDirectories": []string{"/tmp/"},
+		},
+		"tools": map[string]any{
+			"core": toolsCore,
+		},
+	}
+
+	configJSON, err := json.Marshal(config)
+	if err != nil {
+		geminiToolsLog.Printf("ERROR: Failed to marshal Gemini settings: %v", err)
+		configJSON = []byte(`{"context":{"includeDirectories":["/tmp/"]},"tools":{"core":[]}}`)
+	}
+
+	// Escape any single quotes in the JSON. The JSON we generate only contains tool
+	// names like "run_shell_command(grep)" and paths like "/tmp/", so in practice no
+	// single quotes will appear. The shell escape pattern '"'"' works by: ending the
+	// single-quoted string ('), adding a double-quoted single quote ("'"), and reopening
+	// the single-quoted string (').
+	jsonStr := strings.ReplaceAll(string(configJSON), "'", `'"'"'`)
+
+	// Generate a shell script that:
+	// - Creates the .gemini directory if needed
+	// - Merges settings into an existing settings.json (from MCP gateway setup), or
+	// - Creates a new settings.json when no MCP servers are configured
+	//
+	// jq merge: '$existing * $base' means the RIGHT operand ($base) overrides the LEFT
+	// operand ($existing) for conflicting keys. Non-conflicting keys from $existing
+	// (e.g. mcpServers written by convert_gateway_config_gemini.sh) are preserved.
+	command := fmt.Sprintf(`mkdir -p "$GITHUB_WORKSPACE/.gemini"
+SETTINGS="$GITHUB_WORKSPACE/.gemini/settings.json"
+BASE_CONFIG='%s'
+if [ -f "$SETTINGS" ]; then
+  MERGED=$(jq -n --argjson base "$BASE_CONFIG" --argjson existing "$(cat "$SETTINGS")" '$existing * $base')
+  echo "$MERGED" > "$SETTINGS"
+else
+  echo "$BASE_CONFIG" > "$SETTINGS"
+fi`, jsonStr)
+
+	stepLines := []string{
+		"      - name: Write Gemini settings",
+	}
+	stepLines = FormatStepWithCommandAndEnv(stepLines, command, nil)
+	return GitHubActionStep(stepLines)
+}
