Hack #15 introduced ESM dead module stubs to prevent OpenClaw from loading unused channel SDKs (Anthropic, Google AI, Discord, Slack, etc.). Each stub is a minimal ESM file that exports empty classes and functions, satisfying the import resolver without loading the actual 5-50 MB packages.
The stubs worked perfectly when placed directly in the node_modules directory. But when running inside proot, a subtle filesystem layering problem emerged. Proot uses bind mounts to overlay the Termux host filesystem onto the rootfs. Specifically, $PREFIX (which is /data/data/com.termux/files/usr) is bind-mounted into the proot environment so that Node.js and other Termux binaries are accessible.
This means the proot rootfs has TWO versions of every path under $PREFIX:
- The rootfs copy: $ROOTFS/data/data/com.termux/files/usr/lib/node_modules/openclaw/node_modules/...
- The host copy: $PREFIX/lib/node_modules/openclaw/node_modules/... (bind-mounted over the rootfs)
When Node.js resolves an import inside proot, it sees the bind-mounted host version, NOT the rootfs version. Stubs placed in the rootfs copy are completely invisible.
LEGACY: This hack documents a proot-era issue. The native gateway migration eliminated proot and the bind mount problem entirely.
The fix is straightforward: create stubs at the host path, not the rootfs path.
# WRONG: placing stubs inside the rootfs (invisible due to bind mount)
ROOTFS=$PREFIX/var/lib/proot-distro/installed-rootfs/ubuntu
WRONG_PATH=$ROOTFS/data/data/com.termux/files/usr/lib/node_modules/openclaw/node_modules
# CORRECT: placing stubs at the host path (visible to Node.js in proot)
OCDIR=$PREFIX/lib/node_modules/openclaw/node_modulesCreate the stub files at the correct host path:
#!/bin/bash
# create-stubs.sh — place ESM stubs at HOST path
OCDIR=$PREFIX/lib/node_modules/openclaw/node_modules
# Anthropic SDK stub
mkdir -p "$OCDIR/@anthropic-ai/sdk"
cat > "$OCDIR/@anthropic-ai/sdk/index.mjs" << 'EOF'
export class Anthropic { constructor() {} }
export default Anthropic;
EOF
cat > "$OCDIR/@anthropic-ai/sdk/package.json" << 'EOF'
{"name":"@anthropic-ai/sdk","version":"0.0.0","type":"module","main":"index.mjs"}
EOF
# Google Generative AI stub
mkdir -p "$OCDIR/@google/generative-ai"
cat > "$OCDIR/@google/generative-ai/index.mjs" << 'EOF'
export class GoogleGenerativeAI { constructor() {} }
export class GenerativeModel { constructor() {} }
export default GoogleGenerativeAI;
EOF
cat > "$OCDIR/@google/generative-ai/package.json" << 'EOF'
{"name":"@google/generative-ai","version":"0.0.0","type":"module","main":"index.mjs"}
EOF
# AWS Bedrock stub
mkdir -p "$OCDIR/@aws-sdk/client-bedrock-runtime"
cat > "$OCDIR/@aws-sdk/client-bedrock-runtime/index.mjs" << 'EOF'
export class BedrockRuntimeClient { constructor() {} }
export class InvokeModelCommand { constructor() {} }
EOF
cat > "$OCDIR/@aws-sdk/client-bedrock-runtime/package.json" << 'EOF'
{"name":"@aws-sdk/client-bedrock-runtime","version":"0.0.0","type":"module","main":"index.mjs"}
EOF
# Discord.js stub
mkdir -p "$OCDIR/discord.js"
cat > "$OCDIR/discord.js/index.mjs" << 'EOF'
export class Client { constructor() {} login() {} }
export class GatewayIntentBits {}
export default Client;
EOF
cat > "$OCDIR/discord.js/package.json" << 'EOF'
{"name":"discord.js","version":"0.0.0","type":"module","main":"index.mjs"}
EOF
# Slack Web API stub
mkdir -p "$OCDIR/@slack/web-api"
cat > "$OCDIR/@slack/web-api/index.mjs" << 'EOF'
export class WebClient { constructor() {} }
export default WebClient;
EOF
cat > "$OCDIR/@slack/web-api/package.json" << 'EOF'
{"name":"@slack/web-api","version":"0.0.0","type":"module","main":"index.mjs"}
EOF
echo "Stubs created at HOST path: $OCDIR"# Verify stubs exist at HOST path:
ls $PREFIX/lib/node_modules/openclaw/node_modules/@anthropic-ai/sdk/index.mjs
# Expected: file exists
# Verify stubs are visible INSIDE proot:
proot-distro login ubuntu -- ls /data/data/com.termux/files/usr/lib/node_modules/openclaw/node_modules/@anthropic-ai/sdk/index.mjs
# Expected: file exists (same file, via bind mount)
# Verify Node.js resolves the stub (not the real package):
proot-distro login ubuntu -- node22-icu -e "
import('@anthropic-ai/sdk').then(m => {
const a = new m.Anthropic();
console.log('Stub loaded:', typeof a === 'object');
});
"
# Expected: Stub loaded: true
# Check that the stub is tiny (not the real multi-MB package):
du -sh $PREFIX/lib/node_modules/openclaw/node_modules/@anthropic-ai/sdk/
# Expected: 4.0K (just the stub files)$PREFIX/lib/node_modules/ work as expectednpm install -g openclaw), it OVERWRITES the stubs at the host path with the real packages. You must re-run the stub creation script after every npm installSyntaxError: The requested module does not provide an export named 'X' at ESM link timepackage.json with "type": "module" for Node.js to treat the .mjs files as ESM. Without it, Node.js may try to parse them as CommonJS and fail| Metric | Rootfs Path (broken) | Host Path (working) |
|---|---|---|
| Stub visibility in proot | Invisible (shadowed) | Visible (bind-mounted) |
| Node.js import resolution | Loads real package | Loads stub |
| RAM per stubbed package | 5-50 MB each | ~0 MB each |
| Total RAM saved (9 stubs) | 0 MB (stubs ignored) | ~80 MB |
| Packages stubbed | 9 (Anthropic, Google, AWS, Discord, Slack, Carbon, Baileys, Signal, Lark) | Same 9, now working |