The hijack.js file is loaded before OpenClaw via Node's -r (require) flag. It rewrites filesystem paths so that references to /root/ (expected by OpenClaw's Linux-oriented code) map to the actual Termux data directory on Android. The initial hijack patched fs.readFileSync, fs.writeFileSync, fs.mkdirSync, and their callback equivalents (fs.readFile, fs.writeFile, fs.mkdir, etc.).
After the native migration away from proot, OpenClaw's code that uses fs.promises.mkdir, fs.promises.readFile, fs.promises.access, and similar async methods started failing with ENOENT errors because /root/ does not exist on native Android. The fs.promises namespace in Node.js is a separate object with its own function bindings -- monkey-patching fs.mkdir has zero effect on fs.promises.mkdir.
This hack extends hijack.js to patch all 15 path-accepting fs.promises methods, completing the path-rewriting layer across all three Node.js filesystem API surfaces.
The path rewriting function, shared across all three API surfaces:
// hijack.js — path rewriting core
const TERMUX_HOME = '/data/data/com.termux/files/home';
const TERMUX_PREFIX = '/data/data/com.termux/files/usr';
function _fixPath(p) {
if (typeof p !== 'string') return p;
if (p === '/root') return TERMUX_HOME;
if (p.startsWith('/root/')) return TERMUX_HOME + p.slice(5);
if (p === '/tmp') return TERMUX_PREFIX + '/tmp';
if (p.startsWith('/tmp/')) return TERMUX_PREFIX + '/tmp' + p.slice(4);
return p;
}The fs.promises patching section, added after the existing sync/callback patches:
const _fs0 = require('fs');
// Single-path methods: first argument is a path
const singlePathMethods = [
'access', 'appendFile', 'chmod', 'chown',
'lchmod', 'lchown', 'lstat', 'mkdir',
'mkdtemp', 'open', 'readdir', 'readFile',
'readlink', 'realpath', 'rm', 'rmdir',
'stat', 'truncate', 'unlink', 'writeFile',
];
// Dual-path methods: first two arguments are paths
const dualPathMethods = [
'copyFile', 'cp', 'link', 'rename', 'symlink',
];
if (_fs0.promises) {
// Patch single-path methods
singlePathMethods.forEach(function(fn) {
if (typeof _fs0.promises[fn] === 'function') {
var orig = _fs0.promises[fn];
_fs0.promises[fn] = function() {
if (arguments.length > 0) {
arguments[0] = _fixPath(arguments[0]);
}
return orig.apply(this, arguments);
};
Object.defineProperty(_fs0.promises[fn], 'name', { value: fn });
}
});
// Patch dual-path methods
dualPathMethods.forEach(function(fn) {
if (typeof _fs0.promises[fn] === 'function') {
var orig = _fs0.promises[fn];
_fs0.promises[fn] = function() {
if (arguments.length > 0) arguments[0] = _fixPath(arguments[0]);
if (arguments.length > 1) arguments[1] = _fixPath(arguments[1]);
return orig.apply(this, arguments);
};
Object.defineProperty(_fs0.promises[fn], 'name', { value: fn });
}
});
}The full hijack.js file also patches the sync and callback APIs with the same pattern. The key insight is that all three must be patched independently:
// Sync API (already existed before this hack)
['readFileSync', 'writeFileSync', 'mkdirSync', 'statSync', 'existsSync',
'readdirSync', 'unlinkSync', 'rmdirSync', 'accessSync'].forEach(function(fn) {
if (typeof _fs0[fn] === 'function') {
var orig = _fs0[fn];
_fs0[fn] = function() {
if (arguments.length > 0) arguments[0] = _fixPath(arguments[0]);
return orig.apply(this, arguments);
};
}
});
// Callback API (already existed before this hack)
['readFile', 'writeFile', 'mkdir', 'stat', 'readdir',
'unlink', 'rmdir', 'access', 'appendFile'].forEach(function(fn) {
if (typeof _fs0[fn] === 'function') {
var orig = _fs0[fn];
_fs0[fn] = function() {
if (arguments.length > 0) arguments[0] = _fixPath(arguments[0]);
return orig.apply(this, arguments);
};
}
});
// Promises API (this hack)
// ... (as shown above)Deploy to the phone:
# From Windows (after adb forward tcp:8022 tcp:8022)
MSYS_NO_PATHCONV=1 scp -i "C:\Users\robin\.ssh\id_moto" -P 8022 \
hijack.js localhost:/data/data/com.termux/files/home/hijack.jsThe gateway loads it via NODE_OPTIONS:
HIJACK="/data/data/com.termux/files/home/hijack.js"
NODE_OPTIONS="-r $HIJACK --expose-gc --no-warnings --max-old-space-size=112 --max-semi-space-size=2"/root/.openclaw/ paths in the logs.node -r /data/data/com.termux/files/home/hijack.js -e "
const fs = require('fs');
fs.promises.mkdir('/root/test-dir', { recursive: true })
.then(() => fs.promises.writeFile('/root/test-dir/hello.txt', 'works'))
.then(() => fs.promises.readFile('/root/test-dir/hello.txt', 'utf8'))
.then(console.log)
.catch(console.error);
"
# Should print: works$TERMUX_HOME/test-dir/hello.txt, not at /root/test-dir/.:9003/dashboard (it uses fs.promises to read template files).fs.promises.copyFile('/root/file1', '/root/file2') should work with both paths rewritten.fs.promises is lazily initialized in some Node.js versions. The patching must happen after require('fs') triggers the lazy init. The -r flag ensures hijack.js loads first, and the require('fs') call inside hijack.js forces the initialization.apply(this, arguments) pattern preserves the calling context. Some internal Node.js code checks this when fs.promises methods are invoked. Using orig(arguments[0], ...) without apply can cause subtle failures in edge cases.mkdtemp takes a prefix string, not a directory path. Rewriting still works because mkdtemp('/tmp/foo-') becomes mkdtemp('$PREFIX/tmp/foo-'), and Node appends the random suffix.realpath may resolve symlinks and return a canonical path. If the canonical path no longer matches the /root/ or /tmp/ prefix, subsequent operations on the returned path skip rewriting. This has not caused issues in practice.arguments object is mutable in sloppy-mode functions. The arguments[0] = _fixPath(arguments[0]) mutation works because these wrapper functions are not in strict mode. In strict mode, arguments reflects the original values and mutations are ignored.fs.promises.watch or fs.promises.constants -- they do not take paths in the same way. Patching non-existent methods is harmless (the typeof check skips them), but patching methods with different signatures would break them.Object.defineProperty call preserving the function name is optional but helps with debugging. Stack traces show the original method name instead of "anonymous".All OpenClaw code that uses fs.promises for file operations works correctly on native Android without any code changes to OpenClaw itself. Path rewriting is transparent: the application writes to /root/.openclaw/ and the data lands in $TERMUX_HOME/.openclaw/. Combined with the sync and callback patches, hijack.js provides complete filesystem path translation across all three Node.js fs API surfaces (15 promises methods, 9 sync methods, 9 callback methods).