Repository navigation
sandbox upload dereferences symlinks — uploaded as real directories, breaking git #1425
Description
Agent Diagnostic
Investigated by running the reproduction steps manually using openshell sandbox upload against a minimal git repository containing a symlink. Confirmed the symlink is absent in the sandbox and replaced by a regular directory. Tested on v0.0.36 and v0.0.42 — both exhibit the same behavior. Could not resolve — this requires a fix in the upload tar-building logic.
Description
openshell sandbox upload resolves symlinks in the source tree when building the tar archive. Symlinks arrive in the sandbox as regular directories (their targets, expanded inline). Git tracks them as symlink blobs, so git status in the sandbox immediately reports every symlink path as deleted:.
What should happen: Symlinks should be preserved as symlinks in the sandbox, matching the source tree exactly.
Reproduction Steps — v0.0.36
# 1. Download openshell v0.0.36 (Linux x86_64)
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-x86_64-unknown-linux-musl.tar.gz \
| tar -xz -C /tmp/openshell-036 --one-top-level
chmod +x /tmp/openshell-036/openshell
# 2. Create a minimal git repo with a symlink
mkdir /tmp/symlink-repro && cd /tmp/symlink-repro
git init -q
mkdir real-dir && echo "hello" > real-dir/file.txt
ln -s real-dir link-dir
git add . && git commit -qm "init"
# 3. Create a sandbox (requires a running gateway)
/tmp/openshell-036/openshell sandbox create --name symlink-test --keep --no-tty -- true
# 4. Upload the repo
/tmp/openshell-036/openshell sandbox upload symlink-test /tmp/symlink-repro /sandbox/repo
# 5. Check for symlinks — expect one, get none
/tmp/openshell-036/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/find /sandbox/repo -type l
# (no output)
# 6. Confirm link-dir is a real directory instead of a symlink
/tmp/openshell-036/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/ls -la /sandbox/repo/symlink-repro/
# 7. Clean up
/tmp/openshell-036/openshell sandbox delete symlink-testReproduction Steps — v0.0.42 (standalone gateway)
# 1. Download openshell v0.0.42 binaries (Linux x86_64)
mkdir /tmp/openshell-042
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.42/openshell-x86_64-unknown-linux-musl.tar.gz \
| tar -xz -C /tmp/openshell-042
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.42/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz \
| tar -xz -C /tmp/openshell-042
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.42/openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz \
| tar -xz -C /tmp/openshell-042
chmod +x /tmp/openshell-042/openshell /tmp/openshell-042/openshell-gateway /tmp/openshell-042/openshell-sandbox
# 2. Start the standalone gateway (Podman driver, no TLS for local testing)
OPENSHELL_SSH_HANDSHAKE_SECRET=test-secret \
/tmp/openshell-042/openshell-gateway \
--disable-tls \
--disable-gateway-auth \
--drivers podman \
--db-url sqlite:///tmp/openshell-042-test.db \
--port 8081 \
--bind-address 0.0.0.0 \
--docker-supervisor-bin /tmp/openshell-042/openshell-sandbox \
&
# 3. Register and select the gateway
/tmp/openshell-042/openshell gateway add http://127.0.0.1:8081 --name gw-042
/tmp/openshell-042/openshell gateway select gw-042
# 4. Create the same minimal git repo (skip if already done above)
mkdir -p /tmp/symlink-repro && cd /tmp/symlink-repro
git init -q
mkdir -p real-dir && echo "hello" > real-dir/file.txt
ln -sf real-dir link-dir
git add . && git commit -qm "init" 2>/dev/null || true
# 5. Create a sandbox
/tmp/openshell-042/openshell sandbox create --name symlink-test --keep --no-tty -- true
# 6. Upload the repo
/tmp/openshell-042/openshell sandbox upload symlink-test /tmp/symlink-repro /sandbox/repo
# 7. Check for symlinks — expect one, get none
/tmp/openshell-042/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/find /sandbox/repo -type l
# (no output)
# 8. Confirm link-dir is a real directory
/tmp/openshell-042/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/ls -la /sandbox/repo/symlink-repro/
# 9. Clean up
/tmp/openshell-042/openshell sandbox delete symlink-test
kill %1Expected output (step 5 / step 7)
/sandbox/repo/symlink-repro/link-dir
Actual output (step 5 / step 7)
(empty)
ls -la shows link-dir as a regular directory in both versions:
drwxr-xr-x. 1 sandbox sandbox 16 ... link-dir
drwxr-xr-x. 1 sandbox sandbox 16 ... real-dir
Environment
- openshell:
0.0.36and0.0.42(both affected) - OS: Linux x86_64
- Driver: Podman (rootless)
Impact
Any workflow uploading a git repo with symlinks will see those paths reported as deleted: by git status inside the sandbox, even though the working tree is otherwise intact. This causes spurious git noise and can lead agents to attempt restoring files that were never deleted.
Agent-First Checklist
- I pointed my agent at the repo and had it investigate this issue
- I loaded relevant skills (e.g.,
debug-openshell-cluster,debug-inference,openshell-cli) - My agent could not resolve this — the diagnostic above explains why
Activity
fix(sandbox): restore symlinks after openshell upload
omribz156 commentedon May 20, 2026
I traced this before changing code. The upload archive path appears to be in
crates/openshell-cli/src/ssh.rs, insidewrite_upload_archive.The current archive builder uses
Path::is_file()/Path::is_dir()and thentar::Builder::append_path_with_name/append_dir_all. That lines up with the repro here: those path checks follow symlinks, andappend_dir_allarchives the target tree instead of preserving the symlink entry.There is already a symlink-preserving archive pattern elsewhere in the repo:
crates/openshell-driver-vm/src/rootfs.rsusesfs::symlink_metadata,fs::read_link, atar::Header, andappend_link, with a regression test namedcreate_rootfs_archive_preserves_broken_symlinks. The likely small fix is to bring that style into the CLI upload archive builder for bothSinglePathandFileList, then add a unit test near the existingupload_archive_pathstests that asserts a symlink archive entry keeps its link target instead of expanding to a directory.I did not open a PR because OpenShell's vouch gate auto-closes first-time external PRs, and I am not listed in
.github/VOUCHED.td. Leaving the source trace here so the next person does not have to rediscover the tar path.This was traced with Codex assistance, with the final note manually checked against the current source files.