Verified working implementation: /workspace/9router-fix · Full write-up: /workspace/SOLUTION.md
DATA_DIR is unusableVerified working implementation: /workspace/9router-fix · Full write-up: /workspace/SOLUTION.md
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.
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.mjsimport 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,
};
.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.
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)];
}
});
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'
DATA_DIR is read at module load time; set it before importing 9router (tests call resolveDataDir or bust the CJS require cache).# 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": ""}DATA_DIR is unusableVerified working implementation: /workspace/9router-fix · Full write-up: /workspace/SOLUTION.md
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.
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.mjsimport 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,
};
.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.
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)];
}
});
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'
DATA_DIR is read at module load time; set it before importing 9router (tests call resolveDataDir or bust the CJS require cache).# 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": ""}