Build1 publisher3 min readPublished
solon-ai-sandbox puts an agent's bash call inside seatbelt or bwrap without a container
The Solon AI module ports Claude Code's sandbox-runtime to Java, denying writes by default and protecting reads only where the operator names a path. Keeping that deny list current is the standing cost.
The Engineer · Build desk

What happened
- solon-ai-sandbox, a module in the Solon AI 4.1.x tree, ports Claude Code's sandbox-runtime to Java and wraps agent-issued commands in filesystem and network isolation on macOS, Linux and Windows.
- A call to SandboxManager.wrapWithSandbox returns a string the caller runs through /bin/bash -c, expanding to sandbox-exec with a seatbelt profile on macOS and to bwrap with bind mounts and namespaces on Linux.
- Reads and writes use opposite defaults: writes are deny-by-default with an explicit allow list, while reads are allow-by-default and restricted only where the operator names a region to protect.
- The documented config denies reads of ~/.ssh and ~/.aws, allows writes only under /tmp and the working directory, blocks writes into .git, and leaves allowGitConfig false.
- Network policy runs through a local HTTP and SOCKS5 forward proxy that the sandboxed process is pointed at by environment variables, with an allow or deny decision taken per request.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Protecting reads is an enumeration job with no end date: every new secret store, token cache or credential file on the developer's machine is readable until somebody adds its path to denyRead.
- cost The policy is expressed through positional constructors, so getting isolation right means getting a five-argument filesystem config and a twelve-argument network config in the correct order, in code, per project.
- decision Teams that rejected sandboxing because a container per command was too heavy now have a JVM-side option that adds one dependency, and they take on per-platform OS behaviour in exchange.
- exposure Enforcement of the domain allowlist reaches only the processes that honour the injected proxy variables, so a build step that dials out on its own terms is outside the policy.
Writes can default to deny because a build writes to few places, and SandboxManager adds the paths a process cannot function without, including `/dev/*` and temp directories, to the allow list [10]. Reads go the other way. A default-deny read policy, the post says, "would break the compiler, the JVM, and half of userspace", so reads start open, the operator names the regions to protect, and `allowRead` re-opens specific paths inside them [11].
Read protection is a blocklist someone has to maintain. The documented `denyRead` list has two entries [12], and any other credential file on the machine stays readable to the agent until its path is added [20]. On the write side the strictest setting available is an empty `allowWrite` list, which permits nothing beyond the mandatory system paths [13]. Keeping `.git` out of the write scope is in the security defaults because a maliciously rewritten Git directory is a persistence vector, and `.git/config` has its own switch, `allowGitConfig`, defaulting to false [14].
Domain matching runs through `HostUtils`, which normalizes IPv4, IPv6 and hostnames so the same host written a different way still matches, and patterns support wildcards like `*.github.com` [16]. For the allowlist to cover a whole build, every process the agent spawns has to inherit and honour the proxy environment variables.
The case for a per-command wrapper starts with where these agents run. Containers are the usual answer and the right one for a server-side agent, according to the post, but an interactive agent editing a working copy sits on a laptop, one `bash` call away from everything the user can touch [4]. Booting a VM or a container per command is too slow for that loop and cuts the agent off from the working tree you wanted it to edit [5]. The post argues that prompt-level rules such as "do not read sensitive files" are "a suggestion to a stochastic process" and not a security boundary [3].
Pulling the module in adds one dependency, `solon-ai-core`, and no container runtime [6]. Configuration is positional: the documented `FilesystemConfig` takes four lists and a boolean [18], and `NetworkConfig` takes twelve arguments, ten of them null in the published example [19].
`SandboxManager` is final, with static methods, and there is one sandbox per process [7]. Two agents that need different write scopes need two JVMs [22].
The post's author says every snippet was verified against the `solon-ai-sandbox` source in the Solon AI 4.1.x tree [2]. The native facility is named for macOS and Linux; the Windows implementation is not described, although Windows is in the supported platform list [21]. Before trusting the boundary I would print the generated seatbelt profile and the `bwrap` argv for a real build command, then strip the proxy variables from a child process and check that opening a socket fails.
What to watch
- Publication of the generated seatbelt profile and bwrap argv for a real build command, so operators can inspect the boundary.
- What native facility the Windows implementation uses, given Windows is in the supported platform list.
- Whether a later 4.1.x release replaces the positional FilesystemConfig and NetworkConfig constructors with builders or file-based config.