Coding agents that run shell commands need a filesystem boundary. Without one, the model can read, write, and execute anywhere the process has access. If a prompt injection sneaks into a tool result, the agent will execute whatever it says.
Some agents solve this with containers or kernel-level namespaces. Codex uses bubblewrap and Landlock on Linux for OS-native isolation. OpenHands runs everything inside Docker containers. These are strong boundaries, but they come with real costs: container setup time, platform constraints, and operational complexity that most local-first tools do not want.
Many others pass shell commands as strings with no filesystem enforcement. OpenCode spawns commands through the user’s shell with the full parent environment inherited. It parses commands with tree-sitter to extract paths and prompts the user for approval, but there is no technical boundary. Aider uses Python’s subprocess module with no path restrictions at all.
There is a middle ground: lightweight, workspace-scoped enforcement that does not require containers or kernel features. That is what Acolyte now does.
The original approach
Acolyte had path checks from the start, but they were weak. The original implementation used string prefix matching: is this path inside the workspace or /tmp? If yes, allow. If no, block. Shell commands ran through bash -lc with the full command as a single string.
That had three problems.
First, string prefix matching does not follow symlinks. A symlink inside the workspace pointing to /etc/hosts passes the prefix check. The path starts with the workspace root. The file it resolves to does not.
Second, bash -lc means shell evaluation. The model sends a string, bash interprets it. Pipes, redirections, semicolons, subshells, variable expansion. A command like echo ok > /etc/crontab is one string to the host and a write to a system file to bash. The host cannot inspect what bash will actually do without reimplementing a shell parser.
Third, the spawned process inherited the full environment. Every environment variable the parent process had, the child got. API keys, tokens, credentials in ENV, all visible to whatever command the model decided to run.
Workspace sandbox
The fix is structural, not incremental. Three changes that close the class of vulnerability instead of patching individual cases.
Argv-only execution. Shell commands no longer go through bash. Bun.spawn takes an explicit cmd and args array. The string echo hello && rm -rf / becomes the literal arguments ["echo", "hello", "&&", "rm", "-rf", "/"]. No shell evaluation, no operator tricks. The model provides a binary and arguments. The host executes exactly that.
Realpath-based path enforcement. Every path argument is resolved through realpath before the boundary check. Symlinks are followed to their real target. If a symlink inside the workspace points outside, the resolved path fails. For paths that do not exist yet, the sandbox walks up to the nearest existing parent and resolves from there. This prevents creating files under symlinked directories that escape the workspace.
Restricted environment. The spawned process gets a whitelist of environment variables: PATH, HOME, LANG, TERM, CI, and a few others needed for tools to function. Everything else is stripped. The model’s commands run in a minimal environment with no access to API keys, tokens, or credentials.
What this looks like
The sandbox validates every path-like token in the command arguments. Not just explicit paths, but assigned values like OUT=/etc/passwd and bare filenames that happen to be symlinks.
A flag like --format=json/pretty is not a path. The sandbox skips tokens that start with a dash. An argument like ../../etc/passwd is a path. The sandbox resolves it, finds it outside the workspace, and blocks with a structured error.
code: E_SANDBOX_VIOLATION
kind: sandbox_violation
The error is typed, machine-readable, and shows up in trace events as lifecycle.sandbox.violation. You can see every blocked attempt in acolyte trace.
What stays open
This is command-level enforcement, not kernel-level isolation. If the model runs node script.js and that script reads /etc/hosts internally, the sandbox does not catch it. The boundary is at the tool interface, not at the syscall level. That is a real limitation.
But command-level enforcement catches the actual failure mode. The model does not write Node scripts that exfiltrate credentials. It runs cat, grep, ls, and build commands. Those are the operations where path arguments matter and where the sandbox has leverage. The theoretical gap between command-level and kernel-level enforcement has not been the source of real problems. The practical gap between no boundary and a workspace boundary has.
The principle
A coding agent should work freely inside the project. It should not be able to reach outside it.
The workspace root is the natural boundary. Everything the model needs to do its job is inside it. Source files, config files, test files, build outputs. The model reads, writes, edits, and runs commands against these files. Nothing about that requires access to the home directory, system files, or other projects.
Enforcing this boundary means the host can give the model more autonomy inside it. If the model cannot escape the workspace, you worry less about what it does within it. The sandbox is not a restriction on the model. It is what makes broader tool access safe.