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
- Drop workingDirectory from updates — it is canonicalized and set for you
- Set projectRoot explicitly when constructing the mode manager so the canonical workspace is deterministic
- 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
- Always construct mode managers with an explicit projectRoot
- Compare paths with path.resolve to avoid trailing-slash/casing mismatches
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
- Autopilot handoff workingDirectory does not match the select
- agents-init target must stay inside the current working dire
- agents-init target not found: ${requestedTarget}
- agents-init target must be a directory: ${requestedTarget}
- Missing value for ${flag}.
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/b7ecb40a246e2e63.
Report an issue: GitHub.