diff --git a/.claude/commands/analysis.md b/.claude/commands/analysis.md
index bda6b75b0..f886e1fa5 100644
--- a/.claude/commands/analysis.md
+++ b/.claude/commands/analysis.md
@@ -1 +1 @@
-Review the most recent contents of /tmp/posthog-wizard.log. Look for 
\ No newline at end of file
+Review the most recent contents of /tmp/posthog-wizard.log. The log may be quite substantial, so prepare to tail or search it. Look for 
\ No newline at end of file
diff --git a/src/lib/agent-interface.ts b/src/lib/agent-interface.ts
index 2c315fedc..8c36715cf 100644
--- a/src/lib/agent-interface.ts
+++ b/src/lib/agent-interface.ts
@@ -14,6 +14,7 @@ import {
 } from './constants';
 import { getLlmGatewayUrlFromHost } from '../utils/urls';
 import { LINTING_TOOLS } from './safe-tools';
+import { createEnvFileServer, ENV_FILE_TOOL_NAMES } from './env-file-tools';
 
 // Dynamic import cache for ESM module
 let _sdkModule: any = null;
@@ -191,7 +192,38 @@ export function wizardCanUseTool(
 ):
   | { behavior: 'allow'; updatedInput: Record<string, unknown> }
   | { behavior: 'deny'; message: string } {
-  // Allow all non-Bash tools
+  // Block direct reads/writes of .env files — use env-file-tools MCP instead
+  if (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') {
+    const filePath = typeof input.file_path === 'string' ? input.file_path : '';
+    const basename = path.basename(filePath);
+    if (basename.startsWith('.env')) {
+      logToFile(`Denying ${toolName} on env file: ${filePath}`);
+      return {
+        behavior: 'deny',
+        message: `Direct ${toolName} of ${basename} is not allowed. Use the env-file-tools MCP server (check_env_keys / set_env_values) to read or modify environment variables.`,
+      };
+    }
+    return { behavior: 'allow', updatedInput: input };
+  }
+
+  // Block Grep when it directly targets a .env file.
+  // Note: ripgrep skips dotfiles (like .env*) by default during directory traversal,
+  // so broad searches like `Grep { path: "." }` are already safe.
+  if (toolName === 'Grep') {
+    const grepPath = typeof input.path === 'string' ? input.path : '';
+    if (grepPath && path.basename(grepPath).startsWith('.env')) {
+      logToFile(`Denying Grep on env file: ${grepPath}`);
+      return {
+        behavior: 'deny',
+        message: `Grep on ${path.basename(
+          grepPath,
+        )} is not allowed. Use the env-file-tools MCP server (check_env_keys) to check environment variables.`,
+      };
+    }
+    return { behavior: 'allow', updatedInput: input };
+  }
+
+  // Allow all other non-Bash tools
   if (toolName !== 'Bash') {
     return { behavior: 'allow', updatedInput: input };
   }
@@ -291,10 +323,10 @@ export function wizardCanUseTool(
 /**
  * Initialize agent configuration for the LLM gateway
  */
-export function initializeAgent(
+export async function initializeAgent(
   config: AgentConfig,
   options: WizardOptions,
-): AgentRunConfig {
+): Promise<AgentRunConfig> {
   // Initialize log file for this run
   initLogFile();
   logToFile('Agent initialization starting');
@@ -330,6 +362,10 @@ export function initializeAgent(
       ),
     };
 
+    // Add in-process env file tools (secret values never leave the machine)
+    const envFileServer = await createEnvFileServer(config.workingDirectory);
+    mcpServers['env-file-tools'] = envFileServer;
+
     const agentRunConfig: AgentRunConfig = {
       workingDirectory: config.workingDirectory,
       mcpServers,
@@ -485,6 +521,7 @@ export async function runAgent(
       'Bash',
       'ListMcpResourcesTool',
       'Skill',
+      ...ENV_FILE_TOOL_NAMES,
     ];
 
     const response = query({
diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts
index d319df22e..46ed98bc4 100644
--- a/src/lib/agent-runner.ts
+++ b/src/lib/agent-runner.ts
@@ -85,7 +85,7 @@ export async function runAgentWizard(
   }
 
   clack.log.info(
-    `🧙 The wizard has chosen you to try the next-generation agent integration for ${config.metadata.name}.\n\nStand by for the good stuff, and let the robot minders know how it goes:\n\nwizard@posthog.com`,
+    `We're about to read your project using our LLM gateway.\n\n.env* file contents will not leave your machine.\n\nOther files will be read and edited to provide a fully-custom PostHog integration.`,
   );
 
   const aiConsent = await askForAIConsent(options);
@@ -182,7 +182,7 @@ export async function runAgentWizard(
         ? 'https://mcp-eu.posthog.com/mcp'
         : 'https://mcp.posthog.com/mcp');
 
-  const agent = initializeAgent(
+  const agent = await initializeAgent(
     {
       workingDirectory: options.installDir,
       posthogMcpUrl: mcpUrl,
@@ -404,9 +404,12 @@ STEP 4: Load the installed skill's SKILL.md file to understand what references a
 
 STEP 5: Follow the skill's workflow files in sequence. Look for numbered workflow files in the references (e.g., files with patterns like "1.0-", "1.1-", "1.2-"). Start with the first one and proceed through each step until completion. Each workflow file will tell you what to do and which file comes next.
 
-STEP 6: Set up environment variables for PostHog in a .env file with the API key and host provided above, using the appropriate naming convention for ${
-    config.metadata.name
-  }. Make sure to use these environment variables in the code files you create instead of hardcoding the API key and host.
+STEP 6: Set up environment variables for PostHog using the env-file-tools MCP server (this runs locally — secret values never leave the machine):
+   - Use check_env_keys to see which keys already exist in the project's .env file (e.g. .env.local or .env).
+   - Use set_env_values to create or update the PostHog API key and host, using the appropriate naming convention for ${
+     config.metadata.name
+   }. The tool will also ensure .gitignore coverage. Don't assume the presence of keys means the value is up to date. Write the correct value each time.
+   - Reference these environment variables in the code files you create instead of hardcoding the API key and host.
 
 Important: Look for lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb) to determine the package manager (excluding the contents of node_modules). Do not manually edit package.json. Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation. You must read a file immediately before attempting to write it, even if you have previously read it; failure to do so will cause a tool failure.
 
diff --git a/src/lib/env-file-tools.ts b/src/lib/env-file-tools.ts
new file mode 100644
index 000000000..6087b7866
--- /dev/null
+++ b/src/lib/env-file-tools.ts
@@ -0,0 +1,191 @@
+/**
+ * In-process MCP server that reads/writes .env files locally.
+ * Secret values never leave the machine.
+ */
+
+import path from 'path';
+import fs from 'fs';
+import { z } from 'zod';
+import { logToFile } from '../utils/debug';
+
+// Dynamic import cache for ESM module (same pattern as agent-interface.ts)
+let _sdkModule: any = null;
+async function getSDKModule(): Promise<any> {
+  if (!_sdkModule) {
+    _sdkModule = await import('@anthropic-ai/claude-agent-sdk');
+  }
+  return _sdkModule;
+}
+
+/**
+ * Resolve filePath relative to workingDirectory, rejecting path traversal.
+ */
+function resolveEnvPath(workingDirectory: string, filePath: string): string {
+  const resolved = path.resolve(workingDirectory, filePath);
+  if (
+    !resolved.startsWith(workingDirectory + path.sep) &&
+    resolved !== workingDirectory
+  ) {
+    throw new Error(
+      `Path traversal rejected: "${filePath}" resolves outside working directory`,
+    );
+  }
+  return resolved;
+}
+
+/**
+ * Ensure the given env file basename is covered by .gitignore in the working directory.
+ * Creates .gitignore if it doesn't exist; appends the entry if missing.
+ */
+function ensureGitignoreCoverage(
+  workingDirectory: string,
+  envFileName: string,
+): void {
+  const gitignorePath = path.join(workingDirectory, '.gitignore');
+
+  if (fs.existsSync(gitignorePath)) {
+    const content = fs.readFileSync(gitignorePath, 'utf8');
+    // Check if the file (or a glob covering it) is already listed
+    if (content.split('\n').some((line) => line.trim() === envFileName)) {
+      return;
+    }
+    const newContent = content.endsWith('\n')
+      ? `${content}${envFileName}\n`
+      : `${content}\n${envFileName}\n`;
+    fs.writeFileSync(gitignorePath, newContent, 'utf8');
+  } else {
+    fs.writeFileSync(gitignorePath, `${envFileName}\n`, 'utf8');
+  }
+}
+
+/**
+ * Create an in-process MCP server with env file tools.
+ * Must be called asynchronously because the SDK is an ESM module loaded via dynamic import.
+ */
+export async function createEnvFileServer(workingDirectory: string) {
+  const sdk = await getSDKModule();
+  const { tool, createSdkMcpServer } = sdk;
+
+  const checkEnvKeys = tool(
+    'check_env_keys',
+    'Check which environment variable keys are present or missing in a .env file. Never reveals values.',
+    {
+      filePath: z
+        .string()
+        .describe('Path to the .env file, relative to the project root'),
+      keys: z
+        .array(z.string())
+        .describe('Environment variable key names to check'),
+    },
+    (args: { filePath: string; keys: string[] }) => {
+      const resolved = resolveEnvPath(workingDirectory, args.filePath);
+      logToFile(`check_env_keys: ${resolved}, keys: ${args.keys.join(', ')}`);
+
+      const existingKeys: Set<string> = new Set();
+      if (fs.existsSync(resolved)) {
+        const content = fs.readFileSync(resolved, 'utf8');
+        for (const line of content.split('\n')) {
+          const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
+          if (match) {
+            existingKeys.add(match[1]);
+          }
+        }
+      }
+
+      const results: Record<string, 'present' | 'missing'> = {};
+      for (const key of args.keys) {
+        results[key] = existingKeys.has(key) ? 'present' : 'missing';
+      }
+
+      return {
+        content: [
+          { type: 'text' as const, text: JSON.stringify(results, null, 2) },
+        ],
+      };
+    },
+  );
+
+  const setEnvValues = tool(
+    'set_env_values',
+    'Create or update environment variable keys in a .env file. Creates the file if it does not exist. Ensures .gitignore coverage.',
+    {
+      filePath: z
+        .string()
+        .describe('Path to the .env file, relative to the project root'),
+      values: z
+        .record(z.string(), z.string())
+        .describe('Key-value pairs to set'),
+    },
+    (args: { filePath: string; values: Record<string, string> }) => {
+      const resolved = resolveEnvPath(workingDirectory, args.filePath);
+      logToFile(
+        `set_env_values: ${resolved}, keys: ${Object.keys(args.values).join(
+          ', ',
+        )}`,
+      );
+
+      let content = '';
+      if (fs.existsSync(resolved)) {
+        content = fs.readFileSync(resolved, 'utf8');
+      }
+
+      const updatedKeys = new Set<string>();
+
+      for (const [key, value] of Object.entries(args.values)) {
+        const regex = new RegExp(`^(\\s*${key}\\s*=).*$`, 'm');
+        if (regex.test(content)) {
+          content = content.replace(regex, `$1${value}`);
+          updatedKeys.add(key);
+        }
+      }
+
+      // Append keys that weren't already in the file
+      const newKeys = Object.entries(args.values).filter(
+        ([key]) => !updatedKeys.has(key),
+      );
+      if (newKeys.length > 0) {
+        if (content.length > 0 && !content.endsWith('\n')) {
+          content += '\n';
+        }
+        for (const [key, value] of newKeys) {
+          content += `${key}=${value}\n`;
+        }
+      }
+
+      // Ensure parent directory exists
+      const dir = path.dirname(resolved);
+      if (!fs.existsSync(dir)) {
+        fs.mkdirSync(dir, { recursive: true });
+      }
+
+      fs.writeFileSync(resolved, content, 'utf8');
+
+      // Ensure .gitignore coverage for this env file
+      const envFileName = path.basename(resolved);
+      ensureGitignoreCoverage(workingDirectory, envFileName);
+
+      return {
+        content: [
+          {
+            type: 'text' as const,
+            text: `Updated ${Object.keys(args.values).length} key(s) in ${
+              args.filePath
+            }`,
+          },
+        ],
+      };
+    },
+  );
+
+  return createSdkMcpServer({
+    name: 'env-file-tools',
+    version: '1.0.0',
+    tools: [checkEnvKeys, setEnvValues],
+  });
+}
+
+/** Tool names exposed by the env file server, for use in allowedTools */
+export const ENV_FILE_TOOL_NAMES = [
+  'env-file-tools:check_env_keys',
+  'env-file-tools:set_env_values',
+];
diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts
index 05c045e8b..394344c72 100644
--- a/src/utils/clack-utils.ts
+++ b/src/utils/clack-utils.ts
@@ -100,12 +100,6 @@ export function printWelcome(options: {
   // eslint-disable-next-line no-console
   console.log('');
   clack.intro(chalk.inverse(` ${options.wizardName} `));
-
-  const welcomeText =
-    options.message ||
-    `The ${options.wizardName} will help you set up PostHog for your application.\nThank you for using PostHog :)`;
-
-  clack.note(welcomeText);
 }
 
 export async function confirmContinueIfNoOrDirtyGitRepo(
