module

Smith::Sandbox

Runs bash where it cannot reach past the project.

The approval gate asks before every mutating call, and asking about everything is how users end up leaving --yes on — at which point nothing is gated at all. A sandbox inverts that: a command that cannot write outside the working directory does not need to be asked about, because the answer no longer depends on trust.

What is claimed here is deliberately narrow. The filesystem is confined and the network is a switch. There is no per-domain allowlist, because SBPL filters by address and port and not by hostname — a profile naming a host does not compile. Saying so is better than a setting that quietly means something else.

Constants

DEFAULT_WRITE_PATHS = ["/private/tmp", "/private/var/tmp", "~/.cache", "~/Library/Caches", "~/.npm", "~/.cargo", "~/.local/share", "~/.local/state"]

Directories the toolchain has to write to or it fails — usually not with a permission error but with something far more confusing. A Crystal compiler that cannot write ~/.cache/crystal reports "you've found a bug in the Crystal compiler", which is a sentence that sends an agent hunting through its own code for an hour.

This list is the reason the feature is more than a profile string.

DEVICE_WRITE_PATHS = ["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/urandom", "/dev/dtracehelper", "/dev/tty"]

Character devices a shell needs to be a shell.

MACOS_BINARY = "/usr/bin/sandbox-exec"

Class methods

absolute(path : String) : String

A path as the kernel will see it.

Two corrections, both of which are silent failures if skipped. ~ is left alone by File.expand_path unless it is told otherwise, which turns ~/.cache into a path inside the project: an allowance that allows nothing. And symlinks have to be resolved, because the sandbox matches the real path — on macOS /tmp is /private/tmp and a temporary directory lives under /private/var/folders, so a profile naming the unresolved path confines a directory nobody is writing to.

Tools::Paths.normalize already does the symlink half for permission rules, where getting it wrong is how a scoped rule gets bypassed. Same problem, same code.

Source
build(policy : Policy, workdir : String = Dir.current) : Strategy

The strategy for this policy on this machine, and the reason when it is not the one that was asked for.

Source

Nested types