File Exchange
See Under the Hood: File Exchange for how CIFS and 9p mounts work at the QEMU level.
All examples below assume:
from quicksand import Sandbox, Mount, NetworkModeBoot-time mounts
Declare directories to share when creating the sandbox:
async with Sandbox(
image="ubuntu",
mounts=[
Mount("/host/code", "/mnt/code"),
Mount("/host/data", "/mnt/data", readonly=True),
],
) as sb:
result = await sb.execute("ls /mnt/code")
await sb.execute("python3 /mnt/code/main.py")Files appear inside the VM at the specified guest path. Changes to non-readonly mounts are visible on both sides immediately.
Git repositories on CIFS mounts
CIFS mounts present synthesized Unix permissions rather than each host file's executable bit. As a result, git status inside the guest can report mode-only changes for otherwise unchanged files.
Disable executable-bit comparisons for Git commands against the mounted repository:
result = await sb.execute("git -c core.filemode=false -C /mnt/code status")The -c option applies only to that command; it does not change the host repository's Git configuration. Content changes are still reported, but executable-bit changes are ignored.
File locking on CIFS mounts
The bundled SMB server does not implement server-side byte-range locks. Quicksand mounts its shares with nobrl, allowing flock and SQLite to use local locking inside the guest instead.
These locks coordinate processes using the same mount in one sandbox. They do not coordinate with host processes, other sandboxes, or independent mounts of the same host directory. Do not concurrently access a writable SQLite database from those locations. The optional native SMB backend retains server-side locking.
Dynamic hot-mounts
Share a directory into an already-running sandbox:
async with Sandbox(image="ubuntu") as sb:
# Mount at any time after start
handle = await sb.mount("/host/project", "/mnt/project")
await sb.execute("ls /mnt/project")
await sb.execute("echo 'new file' > /mnt/project/output.txt")
# Unmount when done
await sb.unmount(handle)Hot-mounts use the same CIFS file-sharing protocol as boot-time mounts. New shares can be added at any time. No restart is needed.
Read-only mounts
Prevent the guest from modifying host files:
# Boot-time
Mount("/host/data", "/mnt/data", readonly=True)
# Dynamic
handle = await sb.mount("/host/data", "/mnt/data", readonly=True)Getting files out without mounts
If you just need to read a small file from the guest, execute() works:
result = await sb.execute("cat /etc/hostname")
hostname = result.stdout.strip()To write a file into the guest:
await sb.execute("echo 'hello' > /tmp/greeting.txt")For larger file transfers, mounts are more efficient.
Listing active mounts
for handle in sb.active_mounts:
print(f"{handle.host_path} → {handle.guest_path} (readonly={handle.readonly})")9p mounts (no network)
If you need file sharing with NetworkMode.NONE (no network at all), use 9p:
sb = Sandbox(
image="ubuntu",
network_mode=NetworkMode.NONE,
mounts=[Mount("/host/data", "/mnt/data", type="9p")],
)9p mounts are configured at launch time and can't be hot-added. They use a virtual device instead of the network stack.
Which mount type to use
| CIFS (default) | 9p | |
|---|---|---|
| Hot-mountable | Yes | No (boot-time only) |
Works with NONE | No | Yes |
Works with MOUNTS_ONLY / FULL | Yes | Yes |
| Protocol | Network (SMB3) | Virtual device |