-
Notifications
You must be signed in to change notification settings - Fork 70
Description
openclaw backup create fails on all output paths on Android/Termux with an EACCES error. The command writes a .tmp file successfully but then fails on the atomic rename step, which uses fs.link() (hardlink). Android's app-private storage (ext4, app sandbox) blocks cross-file hardlinks, so this step always fails regardless of which directory is used.
Steps to Reproduce
# Default (no --output flag)
openclaw backup create
# With explicit output dir
openclaw backup create --output ~/backups/
# With /usr/tmp
openclaw backup create --output /data/data/com.termux/files/usr/tmp/
```
All three fail with the same error pattern.
### Error
```
Error: EACCES: permission denied, link '/data/data/com.termux/files/home/2026-03-10T04-33-45.081Z-openclaw-backup.tar.gz.0c2c1b63-5771-4522-9fbc-85eed97dc288.tmp' -> '/data/data/com.termux/files/home/2026-03-10T04-33-45.081Z-openclaw-backup.tar.gz'The .tmp file is created successfully — only the link() call to atomically rename it fails.
Root Cause
openclaw backup create uses fs.link(tmpPath, finalPath) for an atomic write. On Android, fs.link() is blocked by the OS for app-private storage (/data/data/com.termux/...), regardless of permissions. This is an Android security constraint, not a Termux configuration issue.
The fix is to replace fs.link() + fs.unlink() with fs.rename() for the atomic swap, which Android does support within the same filesystem. Alternatively, fall back to fs.rename() when fs.link() throws EACCES or EPERM.
Environment
Workaround
Manual tar achieves the same result:
mkdir -p ~/backups
tar -czf ~/backups/$(date -u +%Y-%m-%dT%H-%M-%S)-openclaw-backup.tar.gz \
-C ~ \
.openclaw/openclaw.json \
.openclaw/.env \
.openclaw/secrets.json \
.openclaw/credentials \
.openclaw/identity \
.openclaw/agents/main/agent \
.openclaw/agents/coding/agent \
.openclaw/agents/rachel/agentSuggested Fix
In the backup write implementation, replace the atomic hardlink pattern:
// Current (breaks on Android)
await fs.link(tmpPath, finalPath);
await fs.unlink(tmpPath);
// Fix: use rename instead (atomic on same-fs, works on Android)
await fs.rename(tmpPath, finalPath);Or add a fallback:
try {
await fs.link(tmpPath, finalPath);
await fs.unlink(tmpPath);
} catch (err) {
if (err.code === 'EACCES' || err.code === 'EPERM') {
await fs.rename(tmpPath, finalPath); // fallback for Android
} else {
throw err;
}
}Labels
bug, android, termux, backup