[BUG] Cowork: device_stage_files silently skips the copy when the destination exists, reporting success

Status Open
Maintainer reply None cached
Activity 0 comments · opened Aug 2, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

mcp__remote-devices__device_stage_files does not overwrite a destination path in /mnt/user-data/uploads/ that already exists. It performs no copy at all, but returns {"ok": true} along with the source file's current bytes and mtimeMs. The caller therefore receives a success response containing correct metadata for a file that was never written, and has no field by which to distinguish "copied" from "skipped".

This directly contradicts the tool's own documented contract, which instructs the caller to re-stage a changed file:

The staged copy is a point-in-time snapshot. Before deriving an output from a file you staged more than a few minutes ago, re-check its mtimeMs via device_list_dir and re-stage if it changed — otherwise you risk working from a version the user has since edited.

Re-staging a changed file is the documented remedy, and it is precisely the operation that silently does nothing.

Environment
Claude desktop app 1.24012.9 (Electron 42.7.0, Node 24.18.0), Windows x64
Cowork session running in the cloud sandbox (not "on your computer")
Connected folder on the device; container path /mnt/user-data/uploads/<folder>/<relative-path>
/mnt/user-data/uploads is plain ext4 inside the container (stat -f → ext2/ext3, no FUSE mount entry), so no filesystem-level cache is involved
Reproduction (3 calls)
device_commit_files a 226-byte file to <connected folder>/probe.env.
device_stage_files that path. Container receives 226 bytes. Correct.
device_commit_files a different, 856-byte file to the same device path (force: true).
device_stage_files that path again, without deleting the container-side destination.

Expected: container holds 856 bytes. Actual: tool returns {"bytes": 856, "ok": true}; container still holds 226 bytes and the original content.

Delete the container-side path (rm) and stage again → container correctly receives 856 bytes.
Evidence that this is a skipped write, not a cache
The stale content persisted for 150+ seconds with no further staging: no TTL, no self-correction.
The container-side inode was unchanged throughout.
Forcing a directory read (ls -la of the parent) and a sync changed nothing.
The source file in step 3 was both larger and newer than the container's copy, so this is not a freshness or mtime comparison — the destination's mere existence is sufficient to skip.
Reproduced independently on six real files in the same session, in both directions (a newer source and an older source both failed to propagate).
Impact — this caused real data loss

In a working session, a source file was edited based on a staged copy that was four hours out of date, and the result was committed to git. The commit silently reverted four days of work across five prior commits to that file, including a fix whose absence then mis-classified a live customer order. Nothing detected it for four days; it surfaced only because a human noticed the downstream symptom by eye.

The failure is dangerous specifically because the response looks correct: it reports the true size and mtime of the file you asked for, which is exactly the metadata a caller would use to convince itself the copy is current. A caller can only detect the problem by stat-ing the destination and comparing it against the response — which nothing in the documentation suggests is necessary.

Suggested fix, in order of preference
Overwrite the destination, as the documented contract implies.
Failing that, report what actually happened — e.g. a copied: true|false or skipped: true field — so a caller can detect a no-op instead of assuming success.
Add a force/overwrite parameter. device_stage_files currently accepts only paths and artifact_ids; there is no supported way to compel a fresh copy other than deleting the destination from inside the container.

At minimum, documenting the behaviour would let callers defend against it. Silent success on a skipped write is the worst of the available options, because every reasonable caller treats ok: true plus correct metadata as proof the file is current.

Workaround

Delete the container-side destination path before every re-stage, and verify the staged file's size on disk against the bytes value in the response. Both are reliable, but they depend on the caller knowing about an undocumented behaviour.

What Should Happen?

Re-staging a file whose device-side content has changed should overwrite the
container-side copy, so that /mnt/user-data/uploads/<folder>/<path> matches the
file on the device.

This is exactly what the tool's own documentation tells callers to rely on:
"Before deriving an output from a file you staged more than a few minutes ago,
re-check its mtimeMs via device_list_dir and re-stage if it changed — otherwise
you risk working from a version the user has since edited."

Failing an overwrite, the response should at least indicate that no copy was
made, so the caller can distinguish "copied" from "skipped". It currently
returns ok:true together with the source file's true size and mtime, which is
indistinguishable from a successful copy.

Error Messages/Logs

Steps to Reproduce

  1. device_commit_files a 226-byte file to <connected folder>/probe.env
  1. device_stage_files that same path.

-> container receives 226 bytes. Correct.

  1. device_commit_files a DIFFERENT, 856-byte file to the same device path

(force: true). The device copy is now both larger and newer than the
container's copy.

  1. device_stage_files that path again, WITHOUT deleting the container-side

destination.
-> tool returns {"bytes": 856, "ok": true}
-> container still holds 226 bytes and the original content. <-- BUG

  1. rm the container-side path, then device_stage_files again.

-> container correctly receives 856 bytes.

Evidence this is a skipped write and not a cache:

  • stale content persisted 150+ seconds with no further staging; no TTL, no

self-correction

  • container-side inode unchanged throughout
  • forcing a directory read (ls -la of the parent) and a sync changed nothing
  • the source in step 3 was both larger AND newer, so this is not an mtime or

freshness comparison — the destination merely existing is enough to skip

  • /mnt/user-data/uploads is plain ext4 in the container (stat -f), with no FUSE

mount entry, so no filesystem cache is involved

  • reproduced on six real files in the same session, in both directions

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

Not the CLI — Claude desktop app 1.24012.9 (Electron 42.7.0, Node 24.18.0), Windows x64. Cowork session running in the cloud sandbox.

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Other

Additional Information

_No response_

View original on GitHub ↗