diff --git a/src/lib/agent/agent-runner.ts b/src/lib/agent/agent-runner.ts
index 8e5aa905c..8cf4809c0 100644
--- a/src/lib/agent/agent-runner.ts
+++ b/src/lib/agent/agent-runner.ts
@@ -25,7 +25,6 @@ import { analytics, groupsFromUser } from '@utils/analytics';
 import { getUI } from '@ui';
 import {
   initializeAgent,
-  runAgent as executeAgent,
   AgentErrorType,
   AgentSignals,
   buildWizardMetadata,
@@ -33,6 +32,7 @@ import {
   backupAndFixClaudeSettings,
   restoreClaudeSettings,
 } from './agent-interface';
+import { selectRunner } from './runner';
 import { getCloudUrlFromRegion } from '@utils/urls';
 import {
   evaluateWizardReadiness,
@@ -304,6 +304,10 @@ export async function runProgram(
   const wizardFlags = await analytics.getAllFlagsForWizard();
   const wizardMetadata = buildWizardMetadata(wizardFlags);
 
+  // Select the agent runner backend (anthropic | pi | vercel) from the flags
+  // evaluated above. Resolves to AnthropicRunner today; see runner/index.ts.
+  const runner = selectRunner(wizardFlags);
+
   const mcpUrl = session.localMcp
     ? 'http://localhost:8787/mcp'
     : runtimeEnv('MCP_URL') ||
@@ -372,7 +376,7 @@ export async function runProgram(
   });
 
   // 8. Run agent
-  const agentResult = await executeAgent(
+  const agentResult = await runner.run(
     agent,
     prompt,
     sessionToOptions(session),
diff --git a/src/lib/agent/runner/__tests__/select-runner.test.ts b/src/lib/agent/runner/__tests__/select-runner.test.ts
new file mode 100644
index 000000000..a77a63553
--- /dev/null
+++ b/src/lib/agent/runner/__tests__/select-runner.test.ts
@@ -0,0 +1,44 @@
+import {
+  selectRunner,
+  resolveRunnerVariant,
+  type WizardRunnerVariant,
+} from '../index';
+import { AnthropicRunner } from '../anthropic-runner';
+import { WIZARD_RUNNER_FLAG_KEY } from '../../../constants';
+
+// `runAgent` pulls in the full SDK-backed agent stack; runner selection is a
+// pure choice over flags, so stub the module to keep this a focused unit test.
+jest.mock('../../agent-interface', () => ({
+  runAgent: jest.fn(),
+}));
+
+describe('resolveRunnerVariant', () => {
+  const cases: Array<[string | undefined, WizardRunnerVariant]> = [
+    ['anthropic', 'anthropic'],
+    ['pi', 'pi'],
+    ['vercel', 'vercel'],
+    ['something-else', 'anthropic'],
+    [undefined, 'anthropic'],
+  ];
+  it.each(cases)('maps %p → %p', (value, expected) => {
+    const flags: Record<string, string> =
+      value === undefined ? {} : { [WIZARD_RUNNER_FLAG_KEY]: value };
+    expect(resolveRunnerVariant(flags)).toBe(expected);
+  });
+});
+
+describe('selectRunner', () => {
+  // pi/vercel aren't implemented yet, so every variant resolves to AnthropicRunner.
+  it.each(['anthropic', 'pi', 'vercel', 'bogus'])(
+    'returns AnthropicRunner for %p',
+    (variant) => {
+      expect(
+        selectRunner({ [WIZARD_RUNNER_FLAG_KEY]: variant }),
+      ).toBeInstanceOf(AnthropicRunner);
+    },
+  );
+
+  it('defaults to AnthropicRunner when the flag is absent', () => {
+    expect(selectRunner({})).toBeInstanceOf(AnthropicRunner);
+  });
+});
diff --git a/src/lib/agent/runner/anthropic-runner.ts b/src/lib/agent/runner/anthropic-runner.ts
new file mode 100644
index 000000000..035fd0e54
--- /dev/null
+++ b/src/lib/agent/runner/anthropic-runner.ts
@@ -0,0 +1,17 @@
+import { runAgent } from '../agent-interface';
+import type { Runner, RunnerRunArgs, RunnerResult } from './index';
+
+/**
+ * The `anthropic` variant — today's execution path. Delegates verbatim to the
+ * SDK-backed `runAgent` in agent-interface.ts, which sets the gateway env vars,
+ * builds the MCP servers, and drives the `@anthropic-ai/claude-agent-sdk`
+ * `query()` loop.
+ *
+ * This is the control runner: a thin wrapper, not a rewrite, so the diff stays
+ * a pure seam.
+ */
+export class AnthropicRunner implements Runner {
+  run(...args: RunnerRunArgs): Promise<RunnerResult> {
+    return runAgent(...args);
+  }
+}
diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts
new file mode 100644
index 000000000..876d771ce
--- /dev/null
+++ b/src/lib/agent/runner/index.ts
@@ -0,0 +1,64 @@
+/**
+ * Runner seam.
+ *
+ * The program pipeline (agent-runner.ts) drives an agent run through a
+ * `Runner` rather than calling a model SDK directly. This is the seam the
+ * model-agnostic runner work hangs off: each backend wraps an existing agent
+ * SDK that owns the loop/tools/streaming, and `selectRunner` chooses between
+ * them by the multivariate `wizard-runner` flag.
+ *
+ * - `anthropic` — today's `@anthropic-ai/claude-agent-sdk` path (default).
+ * - `pi`        — OpenAI Agents SDK (lands in #524).
+ * - `vercel`    — Vercel AI SDK (lands in #523).
+ *
+ * This module is pure machinery — it carries no product knowledge.
+ */
+import { WIZARD_RUNNER_FLAG_KEY } from '../../constants';
+import { logToFile } from '../../../utils/debug';
+import type { runAgent } from '../agent-interface';
+import { AnthropicRunner } from './anthropic-runner';
+
+/** The agent backends the `wizard-runner` flag can select. */
+export type WizardRunnerVariant = 'anthropic' | 'pi' | 'vercel';
+
+/** Arguments accepted by a runner — mirrors `runAgent` exactly. */
+export type RunnerRunArgs = Parameters<typeof runAgent>;
+/** Result produced by a runner — mirrors `runAgent` exactly. */
+export type RunnerResult = Awaited<ReturnType<typeof runAgent>>;
+
+/**
+ * Execution backend for an agent run. Implementations are interchangeable:
+ * they take the same arguments as `runAgent` and resolve to the same result,
+ * so the pipeline neither knows nor cares which one it holds.
+ */
+export interface Runner {
+  run(...args: RunnerRunArgs): Promise<RunnerResult>;
+}
+
+/**
+ * Resolve the `wizard-runner` flag to a known variant. Anything unrecognized
+ * — including an absent value from a flag-fetch failure — resolves to
+ * `anthropic`, so the SDK path is always the safe default.
+ */
+export function resolveRunnerVariant(
+  flags: Record<string, string>,
+): WizardRunnerVariant {
+  const value = flags[WIZARD_RUNNER_FLAG_KEY];
+  return value === 'pi' || value === 'vercel' ? value : 'anthropic';
+}
+
+/**
+ * Select the runner for this run and log which backend executed.
+ *
+ * The `pi` and `vercel` runners don't exist yet (#523/#524), so they fall back
+ * to `AnthropicRunner` — the flag is wired and observable, but selection is
+ * behavior-preserving until those runners land.
+ */
+export function selectRunner(flags: Record<string, string>): Runner {
+  const variant = resolveRunnerVariant(flags);
+  // TODO(runners): return PiRunner / VercelRunner once #524 / #523 land.
+  const fallback =
+    variant !== 'anthropic' ? ' (fallback — not yet implemented)' : '';
+  logToFile(`[runner] wizard-runner=${variant} → AnthropicRunner${fallback}`);
+  return new AnthropicRunner();
+}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index bb7f35806..1be485a64 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -143,6 +143,8 @@ export const WIZARD_REMARK_EVENT_NAME = 'wizard remark';
 export const WIZARD_VARIANT_FLAG_KEY = 'wizard-variant';
 /** Feature flag key that gates the intro-screen "Tools" menu. */
 export const WIZARD_TOOLS_MENU_FLAG_KEY = 'wizard-tools-menu';
+/** Multivariate flag selecting the agent runner backend (`anthropic` | `pi` | `vercel`). */
+export const WIZARD_RUNNER_FLAG_KEY = 'wizard-runner';
 /** Variant key -> metadata for wizard run (VARIANT flag selects which entry to use). */
 export const WIZARD_VARIANTS: Record<string, Record<string, string>> = {
   base: { VARIANT: 'base' },
