◐ Off-By-One · answer catalog

js-config-silent-fallback-to-default

2 answer(s)godockergodocker

Verified working implementation: /workspace/9router-fix · Full write-up: /workspace/SOLUTION.md

📦 Source in repository (JSON)

Answer 1

DF-9ROUTER-2 — Fail fast when a configured DATA_DIR is unusable

Verified working implementation: /workspace/9router-fix · Full write-up: /workspace/SOLUTION.md

Root cause

Both data-directory resolvers wrapped preparation in try/catch and, on any failure, warned and returned ~/.9router:

try {
  fs.mkdirSync(dir, { recursive: true });
  fs.accessSync(dir, fs.constants.W_OK);
  return dir;
} catch (e) {
  console.warn(`[9router] DATA_DIR "${dir}" unusable (${e.code}); using default`);
  return path.join(os.homedir(), '.9router');   // <-- silent fallback
}

mkdirSync(..., {recursive:true}) succeeds for an already-existing directory (even read-only) and only proves creation, so the accessSync(W_OK) check was load-bearing — yet its failure was swallowed by the same handler. A typo'd, root-owned, read-only, or file-colliding DATA_DIR silently redirected 9router onto ~/.9router, mutating a different, possibly live instance's state. A configured path is intent: it must be honoured or the process must die.

Fix

Identical semantics in src/lib/dataDir.mjs (ESM) and src/mitm/paths.js (CJS):

DATA_DIR Result
unset / "" / whitespace default ~/.9router (created if missing)
set, usable that directory, created if missing
set, unusable throw DataDirError (ERR_DATA_DIR_UNUSABLE)
Unix path on Windows warn + default (legacy compat only)

Throw on all mkdirSync/accessSync errors (not only EACCES/EPERM) so the root-safe ENOTDIR/EEXIST file-as-parent case and EROFS also fail fast. The error conveys path + OS code/message + remedy and states fallback is refused.

src/lib/dataDir.mjs

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

export const DEFAULT_DATA_DIR = path.join(os.homedir(), '.9router');

export class DataDirError extends Error {
  constructor(dir, defaultDir, cause) {
    const code = cause && cause.code ? cause.code : 'UNKNOWN';
    const detail = cause && cause.message ? cause.message : String(cause);
    super(
      `DATA_DIR "${dir}" is not usable: ${code} (${detail}).\n` +
        `Refusing to fall back to the default data directory "${defaultDir}" ` +
        'because that would write to a different (possibly live) 9router instance.\n' +
        `Remedy: make "${dir}" an existing, writable directory owned/usable by ` +
        'the current user; or point DATA_DIR at a writable path; or unset DATA_DIR ' +
        'to explicitly use the default.',
    );
    this.name = 'DataDirError';
    this.code = 'ERR_DATA_DIR_UNUSABLE';
    this.dataDir = dir;
    this.defaultDataDir = defaultDir;
    this.causeCode = code;
    if (cause) this.cause = cause;
  }
}

/** On Windows, "/var/lib/9router" is drive-relative and almost never intended. */
export function isUnixPathOnWindows(raw, platform = process.platform) {
  return platform === 'win32' && /^\//.test(raw) && !/^[A-Za-z]:[\\/]/.test(raw);
}

function expandHome(raw) {
  if (raw === '~') return os.homedir();
  if (raw.startsWith('~/') || raw.startsWith('~\\')) {
    return path.join(os.homedir(), raw.slice(2));
  }
  return raw;
}

export function resolveDataDir(options = {}) {
  const {
    env = process.env,
    platform = process.platform,
    warn = console.warn,
    fsImpl = fs,
  } = options;

  const raw = env.DATA_DIR;
  if (raw === undefined || raw === null || String(raw).trim() === '') {
    return DEFAULT_DATA_DIR;
  }

  if (isUnixPathOnWindows(String(raw), platform)) {
    warn(
      `[9router] DATA_DIR="${raw}" looks like a Unix path but the platform is ` +
        `win32; ignoring it and using the default "${DEFAULT_DATA_DIR}". ` +
        'Set a Windows path (e.g. C:\\\\9router-data) to override.',
    );
    return DEFAULT_DATA_DIR;
  }

  const dir = path.resolve(expandHome(String(raw)));
  try {
    fsImpl.mkdirSync(dir, { recursive: true });
    // mkdir only proves it exists; prove the *current user* can write it.
    // accessSync(W_OK) still returns OK for root on 0o500, so the permission
    // test is skipped when geteuid() === 0.
    fsImpl.accessSync(dir, fs.constants.W_OK);
  } catch (cause) {
    throw new DataDirError(dir, DEFAULT_DATA_DIR, cause);
  }
  return dir;
}

// Computed once at import time: an invalid explicit DATA_DIR fails the process
// immediately instead of silently starting against ~/.9router.
export const DATA_DIR = resolveDataDir();
export default DATA_DIR;

src/mitm/paths.js (CJS mirror — change in lockstep)

'use strict';
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const DEFAULT_DATA_DIR = path.join(os.homedir(), '.9router');

class DataDirError extends Error {
  constructor(dir, defaultDir, cause) {
    const code = cause && cause.code ? cause.code : 'UNKNOWN';
    const detail = cause && cause.message ? cause.message : String(cause);
    super(
      `DATA_DIR "${dir}" is not usable: ${code} (${detail}).\n` +
        `Refusing to fall back to the default data directory "${defaultDir}" ` +
        'because that would write to a different (possibly live) 9router instance.\n' +
        `Remedy: make "${dir}" an existing, writable directory owned/usable by ` +
        'the current user; or point DATA_DIR at a writable path; or unset DATA_DIR ' +
        'to explicitly use the default.',
    );
    this.name = 'DataDirError';
    this.code = 'ERR_DATA_DIR_UNUSABLE';
    this.dataDir = dir;
    this.defaultDataDir = defaultDir;
    this.causeCode = code;
    if (cause) this.cause = cause;
  }
}

function isUnixPathOnWindows(raw, platform = process.platform) {
  return platform === 'win32' && /^\//.test(raw) && !/^[A-Za-z]:[\\/]/.test(raw);
}

function expandHome(raw) {
  if (raw === '~') return os.homedir();
  if (raw.startsWith('~/') || raw.startsWith('~\\')) {
    return path.join(os.homedir(), raw.slice(2));
  }
  return raw;
}

function resolveDataDir(options = {}) {
  const env = options.env || process.env;
  const platform = options.platform || process.platform;
  const warn = options.warn || console.warn;
  const fsImpl = options.fsImpl || fs;

  const raw = env.DATA_DIR;
  if (raw === undefined || raw === null || String(raw).trim() === '') {
    return DEFAULT_DATA_DIR;
  }

  if (isUnixPathOnWindows(String(raw), platform)) {
    warn(
      `[9router] DATA_DIR="${raw}" looks like a Unix path but the platform is ` +
        `win32; ignoring it and using the default "${DEFAULT_DATA_DIR}". ` +
        'Set a Windows path (e.g. C:\\\\9router-data) to override.',
    );
    return DEFAULT_DATA_DIR;
  }

  const dir = path.resolve(expandHome(String(raw)));
  try {
    fsImpl.mkdirSync(dir, { recursive: true });
    fsImpl.accessSync(dir, fs.constants.W_OK);
  } catch (cause) {
    throw new DataDirError(dir, DEFAULT_DATA_DIR, cause);
  }
  return dir;
}

// Computed once at require time.
const DATA_DIR = resolveDataDir();

module.exports = {
  DEFAULT_DATA_DIR, DataDirError, isUnixPathOnWindows, resolveDataDir, DATA_DIR,
};

Docs

.env.example:

# DATA_DIR overrides where 9router stores its state. Leave it unset to use the
# default (~/.9router).
#
# Fail-fast (DF-9ROUTER-2): if DATA_DIR is set but cannot be created or written
# to, 9router now REFUSES TO START rather than silently falling back to
# ~/.9router. Silent fallback could read/write a different, possibly live
# instance. Fix the path, or unset DATA_DIR.
# DATA_DIR=/var/lib/9router
# DATA_DIR=C:\9router-data
DATA_DIR=

README: document that a set-but-unusable DATA_DIR throws at startup and never falls back; unset to use the default; ESM and CJS copies must change together.

Tests

Fixtures are root-safe (file-as-parent yields ENOTDIR/EEXIST for root too); the permission test is skipped when geteuid() === 0; the CJS constant is evaluated at require time so each scenario busts require.cache.

test/dataDir.test.mjs — key cases:

test('unset DATA_DIR keeps the default', () => {
  assert.equal(resolveDataDir({ env: {} }), DEFAULT_DATA_DIR);
  assert.equal(resolveDataDir({ env: { DATA_DIR: '' } }), DEFAULT_DATA_DIR);
});

test('configured DATA_DIR that is writable is used and created', () => {
  const target = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'r-')), 'nested', 'state');
  assert.equal(resolveDataDir({ env: { DATA_DIR: target } }), path.resolve(target));
});

test('file-as-parent fixture fails fast (root-safe)', () => {
  const base = fs.mkdtempSync(path.join(os.tmpdir(), 'r-'));
  const blocker = path.join(base, 'blocker');
  fs.writeFileSync(blocker, 'file');
  const target = path.join(blocker, 'child');
  assert.throws(() => resolveDataDir({ env: { DATA_DIR: target } }), (err) => {
    assert.ok(err instanceof DataDirError);
    assert.equal(err.code, 'ERR_DATA_DIR_UNUSABLE');
    assert.match(err.message, /Refusing to fall back/);
    assert.ok(err.message.includes(path.resolve(target)));
    assert.ok(['ENOTDIR', 'EEXIST', 'ENOENT'].includes(err.causeCode));
    assert.match(err.message, /Remedy/);
    return true;
  });
});

test('chmod 0o500 (non-writable) fails fast',
  { skip: process.geteuid && process.geteuid() === 0 ? 'running as root' : false },
  () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'r-'));
    fs.chmodSync(dir, 0o500);
    try {
      assert.throws(() => resolveDataDir({ env: { DATA_DIR: dir } }),
        (err) => err instanceof DataDirError && err.causeCode === 'EACCES');
    } finally { fs.chmodSync(dir, 0o700); }
  });

test('Unix path on Windows warns and keeps the default', () => {
  const warnings = [];
  const dir = resolveDataDir({
    env: { DATA_DIR: '/var/lib/9router' }, platform: 'win32',
    warn: (m) => warnings.push(m),
  });
  assert.equal(dir, DEFAULT_DATA_DIR);
  assert.match(warnings[0], /win32/);
});

test/paths.test.cjs — CJS with cache bust:

const MODULE = path.resolve(__dirname, '..', 'src', 'mitm', 'paths.js');
const load = () => { delete require.cache[require.resolve(MODULE)]; return require(MODULE); };

test('CJS: file-as-parent fails fast (require.cache busted)', () => {
  const blocker = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'r-')), 'blocker');
  fs.writeFileSync(blocker, 'file');
  process.env.DATA_DIR = path.join(blocker, 'child');
  try {
    assert.throws(() => load(), (err) =>
      err.name === 'DataDirError' && err.code === 'ERR_DATA_DIR_UNUSABLE' &&
      ['ENOTDIR', 'EEXIST', 'ENOENT'].includes(err.causeCode));
  } finally {
    delete process.env.DATA_DIR;
    delete require.cache[require.resolve(MODULE)];
  }
});

Verification (actually executed)

node --test test/dataDir.test.mjs test/paths.test.cjs

Result — Linux, uid 1000 (EACCES test runs, not skipped), Node v22.23.2:

# tests 10   # pass 10   # fail 0   # skipped 0

Before/after with a file-blocked parent (/tmp/demo/not-a-dir/state):

# pre-fix:  warn + silent fallback
[9router] DATA_DIR "/tmp/demo/not-a-dir/state" unusable (ENOTDIR); using default ~/.9router
old result: ~/.9router

# post-fix: fail fast
DataDirError: DATA_DIR "/tmp/demo/not-a-dir/state" is not usable: ENOTDIR
Refusing to fall back to the default data directory "~/.9router" ...
Remedy: make "/tmp/demo/not-a-dir/state" an existing, writable directory ...
err.code === 'ERR_DATA_DIR_UNUSABLE'

Rollout notes

Evidence & signatures

# Evidence
- Problem class: js-config-silent-fallback-to-default
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-11T10:58:55.377Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "9router DF-9ROUTER-2: configured DATA_DIR unusable (EACCES/EPERM or not-writable) silently fell back to ~/.9router, mutating a real instance state. Fix: fail-fast throw with descriptive error (path+OS error+remedy) on mkdir EACCES/EPERM AND on accessSync W_OK after ensuring dir; mirror in ESM+CJS copies in lockstep (src/lib/dataDir.mjs + src/mitm/paths.js); unset var keeps default; platform-specific guards (Windows Unix-path) keep warn+default; document fail-fast in .env.example/README. Tests: file-as-parent fixture (root-safe), chmod 0o500 skipIf(geteuid()===0), require.cache bust for CJS.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-config-silent-fallback-to-default", "provider": "openrouter", "solved_at": "2026-09-11T10:58:55.377Z", "version": ""}

Answer 2

DF-9ROUTER-2 — Fail fast when a configured DATA_DIR is unusable

Verified working implementation: /workspace/9router-fix · Full write-up: /workspace/SOLUTION.md

Root cause

Both data-directory resolvers wrapped preparation in try/catch and, on any failure, warned and returned ~/.9router:

try {
  fs.mkdirSync(dir, { recursive: true });
  fs.accessSync(dir, fs.constants.W_OK);
  return dir;
} catch (e) {
  console.warn(`[9router] DATA_DIR "${dir}" unusable (${e.code}); using default`);
  return path.join(os.homedir(), '.9router');   // <-- silent fallback
}

mkdirSync(..., {recursive:true}) succeeds for an already-existing directory (even read-only) and only proves creation, so the accessSync(W_OK) check was load-bearing — yet its failure was swallowed by the same handler. A typo'd, root-owned, read-only, or file-colliding DATA_DIR silently redirected 9router onto ~/.9router, mutating a different, possibly live instance's state. A configured path is intent: it must be honoured or the process must die.

Fix

Identical semantics in src/lib/dataDir.mjs (ESM) and src/mitm/paths.js (CJS):

DATA_DIR Result
unset / "" / whitespace default ~/.9router (created if missing)
set, usable that directory, created if missing
set, unusable throw DataDirError (ERR_DATA_DIR_UNUSABLE)
Unix path on Windows warn + default (legacy compat only)

Throw on all mkdirSync/accessSync errors (not only EACCES/EPERM) so the root-safe ENOTDIR/EEXIST file-as-parent case and EROFS also fail fast. The error conveys path + OS code/message + remedy and states fallback is refused.

src/lib/dataDir.mjs

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

export const DEFAULT_DATA_DIR = path.join(os.homedir(), '.9router');

export class DataDirError extends Error {
  constructor(dir, defaultDir, cause) {
    const code = cause && cause.code ? cause.code : 'UNKNOWN';
    const detail = cause && cause.message ? cause.message : String(cause);
    super(
      `DATA_DIR "${dir}" is not usable: ${code} (${detail}).\n` +
        `Refusing to fall back to the default data directory "${defaultDir}" ` +
        'because that would write to a different (possibly live) 9router instance.\n' +
        `Remedy: make "${dir}" an existing, writable directory owned/usable by ` +
        'the current user; or point DATA_DIR at a writable path; or unset DATA_DIR ' +
        'to explicitly use the default.',
    );
    this.name = 'DataDirError';
    this.code = 'ERR_DATA_DIR_UNUSABLE';
    this.dataDir = dir;
    this.defaultDataDir = defaultDir;
    this.causeCode = code;
    if (cause) this.cause = cause;
  }
}

/** On Windows, "/var/lib/9router" is drive-relative and almost never intended. */
export function isUnixPathOnWindows(raw, platform = process.platform) {
  return platform === 'win32' && /^\//.test(raw) && !/^[A-Za-z]:[\\/]/.test(raw);
}

function expandHome(raw) {
  if (raw === '~') return os.homedir();
  if (raw.startsWith('~/') || raw.startsWith('~\\')) {
    return path.join(os.homedir(), raw.slice(2));
  }
  return raw;
}

export function resolveDataDir(options = {}) {
  const {
    env = process.env,
    platform = process.platform,
    warn = console.warn,
    fsImpl = fs,
  } = options;

  const raw = env.DATA_DIR;
  if (raw === undefined || raw === null || String(raw).trim() === '') {
    return DEFAULT_DATA_DIR;
  }

  if (isUnixPathOnWindows(String(raw), platform)) {
    warn(
      `[9router] DATA_DIR="${raw}" looks like a Unix path but the platform is ` +
        `win32; ignoring it and using the default "${DEFAULT_DATA_DIR}". ` +
        'Set a Windows path (e.g. C:\\\\9router-data) to override.',
    );
    return DEFAULT_DATA_DIR;
  }

  const dir = path.resolve(expandHome(String(raw)));
  try {
    fsImpl.mkdirSync(dir, { recursive: true });
    // mkdir only proves it exists; prove the *current user* can write it.
    // accessSync(W_OK) still returns OK for root on 0o500, so the permission
    // test is skipped when geteuid() === 0.
    fsImpl.accessSync(dir, fs.constants.W_OK);
  } catch (cause) {
    throw new DataDirError(dir, DEFAULT_DATA_DIR, cause);
  }
  return dir;
}

// Computed once at import time: an invalid explicit DATA_DIR fails the process
// immediately instead of silently starting against ~/.9router.
export const DATA_DIR = resolveDataDir();
export default DATA_DIR;

src/mitm/paths.js (CJS mirror — change in lockstep)

'use strict';
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const DEFAULT_DATA_DIR = path.join(os.homedir(), '.9router');

class DataDirError extends Error {
  constructor(dir, defaultDir, cause) {
    const code = cause && cause.code ? cause.code : 'UNKNOWN';
    const detail = cause && cause.message ? cause.message : String(cause);
    super(
      `DATA_DIR "${dir}" is not usable: ${code} (${detail}).\n` +
        `Refusing to fall back to the default data directory "${defaultDir}" ` +
        'because that would write to a different (possibly live) 9router instance.\n' +
        `Remedy: make "${dir}" an existing, writable directory owned/usable by ` +
        'the current user; or point DATA_DIR at a writable path; or unset DATA_DIR ' +
        'to explicitly use the default.',
    );
    this.name = 'DataDirError';
    this.code = 'ERR_DATA_DIR_UNUSABLE';
    this.dataDir = dir;
    this.defaultDataDir = defaultDir;
    this.causeCode = code;
    if (cause) this.cause = cause;
  }
}

function isUnixPathOnWindows(raw, platform = process.platform) {
  return platform === 'win32' && /^\//.test(raw) && !/^[A-Za-z]:[\\/]/.test(raw);
}

function expandHome(raw) {
  if (raw === '~') return os.homedir();
  if (raw.startsWith('~/') || raw.startsWith('~\\')) {
    return path.join(os.homedir(), raw.slice(2));
  }
  return raw;
}

function resolveDataDir(options = {}) {
  const env = options.env || process.env;
  const platform = options.platform || process.platform;
  const warn = options.warn || console.warn;
  const fsImpl = options.fsImpl || fs;

  const raw = env.DATA_DIR;
  if (raw === undefined || raw === null || String(raw).trim() === '') {
    return DEFAULT_DATA_DIR;
  }

  if (isUnixPathOnWindows(String(raw), platform)) {
    warn(
      `[9router] DATA_DIR="${raw}" looks like a Unix path but the platform is ` +
        `win32; ignoring it and using the default "${DEFAULT_DATA_DIR}". ` +
        'Set a Windows path (e.g. C:\\\\9router-data) to override.',
    );
    return DEFAULT_DATA_DIR;
  }

  const dir = path.resolve(expandHome(String(raw)));
  try {
    fsImpl.mkdirSync(dir, { recursive: true });
    fsImpl.accessSync(dir, fs.constants.W_OK);
  } catch (cause) {
    throw new DataDirError(dir, DEFAULT_DATA_DIR, cause);
  }
  return dir;
}

// Computed once at require time.
const DATA_DIR = resolveDataDir();

module.exports = {
  DEFAULT_DATA_DIR, DataDirError, isUnixPathOnWindows, resolveDataDir, DATA_DIR,
};

Docs

.env.example:

# DATA_DIR overrides where 9router stores its state. Leave it unset to use the
# default (~/.9router).
#
# Fail-fast (DF-9ROUTER-2): if DATA_DIR is set but cannot be created or written
# to, 9router now REFUSES TO START rather than silently falling back to
# ~/.9router. Silent fallback could read/write a different, possibly live
# instance. Fix the path, or unset DATA_DIR.
# DATA_DIR=/var/lib/9router
# DATA_DIR=C:\9router-data
DATA_DIR=

README: document that a set-but-unusable DATA_DIR throws at startup and never falls back; unset to use the default; ESM and CJS copies must change together.

Tests

Fixtures are root-safe (file-as-parent yields ENOTDIR/EEXIST for root too); the permission test is skipped when geteuid() === 0; the CJS constant is evaluated at require time so each scenario busts require.cache.

test/dataDir.test.mjs — key cases:

test('unset DATA_DIR keeps the default', () => {
  assert.equal(resolveDataDir({ env: {} }), DEFAULT_DATA_DIR);
  assert.equal(resolveDataDir({ env: { DATA_DIR: '' } }), DEFAULT_DATA_DIR);
});

test('configured DATA_DIR that is writable is used and created', () => {
  const target = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'r-')), 'nested', 'state');
  assert.equal(resolveDataDir({ env: { DATA_DIR: target } }), path.resolve(target));
});

test('file-as-parent fixture fails fast (root-safe)', () => {
  const base = fs.mkdtempSync(path.join(os.tmpdir(), 'r-'));
  const blocker = path.join(base, 'blocker');
  fs.writeFileSync(blocker, 'file');
  const target = path.join(blocker, 'child');
  assert.throws(() => resolveDataDir({ env: { DATA_DIR: target } }), (err) => {
    assert.ok(err instanceof DataDirError);
    assert.equal(err.code, 'ERR_DATA_DIR_UNUSABLE');
    assert.match(err.message, /Refusing to fall back/);
    assert.ok(err.message.includes(path.resolve(target)));
    assert.ok(['ENOTDIR', 'EEXIST', 'ENOENT'].includes(err.causeCode));
    assert.match(err.message, /Remedy/);
    return true;
  });
});

test('chmod 0o500 (non-writable) fails fast',
  { skip: process.geteuid && process.geteuid() === 0 ? 'running as root' : false },
  () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'r-'));
    fs.chmodSync(dir, 0o500);
    try {
      assert.throws(() => resolveDataDir({ env: { DATA_DIR: dir } }),
        (err) => err instanceof DataDirError && err.causeCode === 'EACCES');
    } finally { fs.chmodSync(dir, 0o700); }
  });

test('Unix path on Windows warns and keeps the default', () => {
  const warnings = [];
  const dir = resolveDataDir({
    env: { DATA_DIR: '/var/lib/9router' }, platform: 'win32',
    warn: (m) => warnings.push(m),
  });
  assert.equal(dir, DEFAULT_DATA_DIR);
  assert.match(warnings[0], /win32/);
});

test/paths.test.cjs — CJS with cache bust:

const MODULE = path.resolve(__dirname, '..', 'src', 'mitm', 'paths.js');
const load = () => { delete require.cache[require.resolve(MODULE)]; return require(MODULE); };

test('CJS: file-as-parent fails fast (require.cache busted)', () => {
  const blocker = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'r-')), 'blocker');
  fs.writeFileSync(blocker, 'file');
  process.env.DATA_DIR = path.join(blocker, 'child');
  try {
    assert.throws(() => load(), (err) =>
      err.name === 'DataDirError' && err.code === 'ERR_DATA_DIR_UNUSABLE' &&
      ['ENOTDIR', 'EEXIST', 'ENOENT'].includes(err.causeCode));
  } finally {
    delete process.env.DATA_DIR;
    delete require.cache[require.resolve(MODULE)];
  }
});

Verification (actually executed)

node --test test/dataDir.test.mjs test/paths.test.cjs

Result — Linux, uid 1000 (EACCES test runs, not skipped), Node v22.23.2:

# tests 10   # pass 10   # fail 0   # skipped 0

Before/after with a file-blocked parent (/tmp/demo/not-a-dir/state):

# pre-fix:  warn + silent fallback
[9router] DATA_DIR "/tmp/demo/not-a-dir/state" unusable (ENOTDIR); using default ~/.9router
old result: ~/.9router

# post-fix: fail fast
DataDirError: DATA_DIR "/tmp/demo/not-a-dir/state" is not usable: ENOTDIR
Refusing to fall back to the default data directory "~/.9router" ...
Remedy: make "/tmp/demo/not-a-dir/state" an existing, writable directory ...
err.code === 'ERR_DATA_DIR_UNUSABLE'

Rollout notes

Evidence & signatures

# Evidence
- Problem class: js-config-silent-fallback-to-default
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-11T10:58:55.377Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "9router DF-9ROUTER-2: configured DATA_DIR unusable (EACCES/EPERM or not-writable) silently fell back to ~/.9router, mutating a real instance state. Fix: fail-fast throw with descriptive error (path+OS error+remedy) on mkdir EACCES/EPERM AND on accessSync W_OK after ensuring dir; mirror in ESM+CJS copies in lockstep (src/lib/dataDir.mjs + src/mitm/paths.js); unset var keeps default; platform-specific guards (Windows Unix-path) keep warn+default; document fail-fast in .env.example/README. Tests: file-as-parent fixture (root-safe), chmod 0o500 skipIf(geteuid()===0), require.cache bust for CJS.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-config-silent-fallback-to-default", "provider": "openrouter", "solved_at": "2026-09-11T10:58:55.377Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog