Description
mv -n in published @poe-platform/safe-bash@0.1.500 can overwrite a destination created concurrently after its existence check. The public safe-fs rename operation receives only { signal }, so an injected native filesystem cannot preserve the command's no-clobber intent atomically.
The command implementation in dist/safe-bash/commands/filesystem.js checks maybeStat(target) and then calls fs.rename(source, target, { signal }). Our native default rename is atomic, but adding another destination stat in the adapter cannot close this race.
Public API reproduction (run in a package with safe-bash0.1.500):
import { Shell, agentCommands, createMemoryFileSystem } from '@poe-platform/safe-bash';
const fs = createMemoryFileSystem();
const bytes = (s) => new TextEncoder().encode(s);
await fs.writeFile('/source', bytes('source'));
const injected = new Proxy(fs, {
get(target, key) {
if (key === 'rename') return async (from, to, options) => {
// Deterministically create the destination after mv's final stat.
await fs.writeFile(to, bytes('concurrent destination'));
return fs.rename(from, to, options);
};
const value = Reflect.get(target, key);
return typeof value === 'function' ? value.bind(target) : value;
},
});
const shell = new Shell({ fs: injected }).use(agentCommands());
try {
const result = await shell.exec('mv -n /source /target');
console.log(result.exitCode, new TextDecoder().decode(await fs.readFile('/target')));
} finally {
await shell.dispose();
}Actual: 0 source, replacing the concurrent destination. Expected: keep concurrent destination and preserve /source.
Please expose and propagate a supported atomic no-replace rename operation/capability (or equivalent public option), with native filesystem admission that refuses the mode when it cannot be guaranteed. Include deterministic concurrent-destination coverage through the public command and filesystem APIs. Default overwriting mv is separate and works with our authoritative transaction; this issue is specifically the no-clobber mode.
Activity
kamilio commentedon Sep 8, 2026
Validated against the current checkout built public APIs at remote main 4c4a37f. A deterministic injected-filesystem race creates the destination at rename entry after the mv -n existence checks: the command passes only {signal}, overwrites the competing destination, deletes the source, and exits 0 even when atomicRename is declared. Controls preserve preexisting destinations, allow normal mv overwrite, and reject a disabled rename without mutation. This is a real command/contract gap, not an adapter violation: the current rename contract carries no no-replace intent, and atomic rename is not atomic no-replace. A fix must require an affirmative atomic no-replace capability/operation, preserve both entries on EEXIST, and avoid stat-plus-rename emulation or unsafe overwrite fallbacks. Cross-device behavior needs separate validation. Evidence: /tmp/kamilio-701-race-evidence.json. No fix or npm artifact qualification is claimed yet.
fix(safe-bash): preserve concurrent mv no-clobber targets