Yeachan-Heo/oh-my-codex · error · Error

autopilot.workingDirectory must match the selected writable

Error message

autopilot.workingDirectory must match the selected writable workspace

What it means

Autopilot state updates enforce that a submitted workingDirectory matches the canonical workspace (projectRoot, falling back to process.cwd()). This keeps autopilot state anchored to the repository it was created for.

Source

Thrown at src/modes/base.ts:351

      ? await readModeStateForExplicitSession(mode, explicitSessionId, projectRoot)
      : await readModeState(mode, projectRoot);
  if (!current) throw new Error(`Mode ${mode} not found`);
  await mkdir(scope.stateDir, { recursive: true });

  if (mode === 'ralph') {
    assertRalphUpdateMatchesSession(current, scope.sessionId);
  }

  const updatedBase = { ...current, ...updates };
  if (mode === 'autopilot') {
    const submittedSessionId = typeof updates.session_id === 'string' ? updates.session_id.trim() : '';
    if (submittedSessionId && scope.sessionId && submittedSessionId !== scope.sessionId) {
      throw new Error('autopilot.session_id must match the selected writable session scope');
    }
    const canonicalWorkspace = projectRoot ?? process.cwd();
    const submittedWorkingDirectory = typeof updates.workingDirectory === 'string' ? updates.workingDirectory.trim() : '';
    if (submittedWorkingDirectory && submittedWorkingDirectory !== canonicalWorkspace) {
      throw new Error('autopilot.workingDirectory must match the selected writable workspace');
    }
    if (scope.sessionId) updatedBase.session_id = scope.sessionId;
    updatedBase.workingDirectory = canonicalWorkspace;
    // Shared invariant, not a local copy: see src/state/handoff-carrier.ts for why a supplied
    // malformed carrier must be rejected before any merge normalizes it away.
    const suppliedHandoffs = updates.handoff_artifacts;
    assertValidHandoffCarriersIn(updates as Record<string, unknown>, 'supplied');
    // Also the PERSISTED state: a stored `state.handoff_artifacts` array survives this shallow merge
    // and the gate would read it, so validating only the incoming payload left it fail-open.
    assertValidHandoffCarriersIn(current as Record<string, unknown>, 'stored');
    const currentHandoffs = requirePersistedHandoffCarrier(current.handoff_artifacts, 'handoff_artifacts carrier');
    const nextHandoffs = requirePersistedHandoffCarrier(suppliedHandoffs, 'supplied handoff_artifacts carrier');
    if (Object.keys(currentHandoffs).length > 0 || Object.keys(nextHandoffs).length > 0) {
      updatedBase.handoff_artifacts = { ...currentHandoffs, ...nextHandoffs };
    }
  }
  delete updatedBase.trustedPipelineProgress;
  if (!Object.prototype.hasOwnProperty.call(updates, 'run_outcome')) {

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Drop workingDirectory from updates — it is canonicalized and set for you
  2. Set projectRoot explicitly when constructing the mode manager so the canonical workspace is deterministic
  3. Normalize your path (path.resolve, realpath) before comparing/passing it

Example fix

// before
await updateModeState('autopilot', { workingDirectory: '/repo/../repo' });
// after
await updateModeState('autopilot', { }); // workingDirectory set from projectRoot ?? cwd
Defensive patterns

Strategy: validation

Validate before calling

const wd = (updates.workingDirectory ?? '').trim();
const canonical = projectRoot ?? process.cwd();
if (wd && wd !== canonical) updates = { ...updates, workingDirectory: canonical };

Try / catch

try { await updateModeState('autopilot', updates); } catch (e) { if (e instanceof Error && e.message.includes('autopilot.workingDirectory')) { delete updates.workingDirectory; await updateModeState('autopilot', updates); } throw e; }

Prevention

When it happens

Trigger: Calling updateModeState/updateAutopilotPipelineState in autopilot mode with a non-empty updates.workingDirectory that differs from projectRoot ?? process.cwd().

Common situations: Running the CLI from a different directory than the project root, passing an absolute path with different casing or a trailing slash, symlinks resolving differently, or a stale hardcoded path.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/b7ecb40a246e2e63. Report an issue: GitHub.