The whole permission model is one folder
Smith confines file tools to the workspace, runs Bash with the workspace as its only writable mount, protects harness state twice, and fails closed when bubblewrap is unavailable.
On this page
The permission question users actually ask a coding agent is not “which capabilities have I granted?” It is “what can this thing do to my machine?”
Smith’s answer is one directory. Direct file tools can read and write inside the selected workspace. Bash sees the host filesystem read-only and gets one writable bind: that workspace. The harness’s own .efferent and .foundry directories are flipped back to read-only inside it.
One boundary, enforced independently by path checks and the kernel.
File tools start from canonical paths
Every coding handler is built for one cwd. Before a read, Smith resolves the requested path with realPath; before a write to a path that may not exist yet, it finds the nearest existing ancestor, resolves that ancestor, and reconstructs the canonical target.
const checkedReadPath = (path: string) =>
fs.realPath(resolve(path)).pipe(
Effect.filterOrFail(
(canonical) => insideWorkspace(canonical) && !isHarnessState(canonical),
() => outsideFailure(path, "read"),
),
)
The canonical step closes two common holes.
First, ../../etc/passwd cannot hide behind lexical .. segments. Second, a symlink inside the workspace that points outside resolves to its external destination and is refused. A string-prefix sandbox can miss both, especially the sibling-prefix case where /work/app-old appears to begin with /work/app.
The actual containment test uses relative:
const insideWorkspace = (path: string): boolean => {
const rel = relative(workspaceRoot, normalize(path))
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))
}
The path is either the workspace itself or a descendant. There is no glob language for users to interpret and no command-pattern exception list to maintain.
Harness state is inside the folder and outside the grant
The workspace contains two directories the coder must not modify:
.efferent— credentials, config, conversation history, skills, and memory;.foundry— baselines and forge artifacts used to judge the run.
Allowing the coder to edit either would let the subject change its own instructions or audit trail. isHarnessState therefore rejects direct reads and writes into both trees:
const isHarnessState = (path: string): boolean => {
const absolute = normalize(path)
return [".efferent", ".foundry"].some((name) => {
const root = join(workspaceRoot, name)
return absolute === root || absolute.startsWith(`${root}/`)
})
}
This is intentionally not a user-extensible deny list. These paths belong to the harness, so the harness owns the carve-out. Project files belong to the workspace grant.
Refusals remain inside the agent turn
Every tool declares failureMode: "return". A rejected path becomes a structured tool result rather than an exception that kills the loop:
const outsideFailure = (path: string, operation: "read" | "write") => ({
error: "OutsideWorkspace",
message:
`refusing to ${operation} "${path}" — ` +
`resolved paths are confined to ${workspaceRoot}`,
})
The message names both the operation and the boundary, giving the model enough information to choose a workspace-relative path or explain that the task requires a wider grant. A bare EACCES would preserve safety and waste the next turn on guessing.
The tool descriptions carry the same rule before it is hit: reads and writes are workspace-bound; symlink escapes are refused; harness state is not available. Prompt text makes the boundary predictable. The handler makes it real.
Bash needs a kernel boundary
Path guards cover the file tools and say nothing about Bash. Shell is an open grammar: a command can invoke Python, redirect output, spawn a compiler, or run another shell. Parsing commands into a permission language would mean implementing a shell security model in TypeScript.
Smith instead runs the coder’s shell through bubblewrap. The mount table is the policy:
export const bwrapArgs = (workspace, cwd, command, readonlyDirs = []) => [
"bwrap",
"--die-with-parent",
"--unshare-pid",
"--ro-bind", "/", "/", // host filesystem: visible, read-only
"--dev", "/dev",
"--proc", "/proc",
"--tmpfs", "/tmp",
"--dir", "/tmp/home",
"--setenv", "HOME", "/tmp/home",
"--setenv", "PATH", workspacePath(workspace),
"--bind", workspace, workspace, // the one writable tree
...readonlyDirs.flatMap((dir) => ["--ro-bind", dir, dir]),
"--chdir", cwd,
"bash", "-c", command,
]
Several details earn their flags:
- The real home directory is replaced with empty tmpfs, so dotfiles, SSH keys, and cloud credentials are absent rather than merely discouraged.
/tmpis fresh per shell call..efferentand.foundryare rebound read-only on top of the writable workspace mount. The kernel independently enforces the same harness-state rule as the handlers.- The workspace’s
.local/binleadsPATH, so a toolchain provisioned there is visible to both coder and gates. --die-with-parentand the private PID namespace ensure the sandbox tree cannot outlive its runner.
Network remains available because installs and real test suites need it. This is a workspace-write sandbox, not an exfiltration-proof environment. Hostile inputs still call for a stronger container or VM boundary.
Missing bubblewrap fails closed
At layer construction, Smith probes whether bubblewrap exists and can actually create a sandbox. If the probe fails, sandboxed execution does not quietly fall back to the host shell:
return sandboxed
? spawnBounded(bwrapArgs(workspace, cwd, command, protectedDirs(workspace)))
: Effect.fail(
new ShellError({
message:
"sandbox requested but bubblewrap is unavailable; " +
"install bwrap or rerun with --no-sandbox",
}),
)
Running without isolation requires the explicit --no-sandbox choice, which composes the ordinary local shell instead. The degraded state cannot be mistaken for the protected one.
Bubblewrap is Linux-specific. On macOS, the honest options are a Linux container or the explicit unsandboxed mode with file-tool guards still active.
Only the coder is sandboxed
The gate pipeline runs the repository’s own configured checks outside the coder sandbox. :ship also needs the user’s real Git and GitHub credentials. Both use the local shell deliberately.
The trust boundary is the planner that emits arbitrary commands. The deterministic machinery that evaluates the result is outside it. Sandboxing the gates would create environment skew between the user’s authoritative checks and the checks used to accept the run.
This division also explains why the workspace remains directly writable rather than copied into a disposable image. Smith is an in-place development tool: edits must appear in the user’s working tree, and the gates must inspect those exact bytes.
Time and output are bounded separately
Filesystem isolation does not stop a command that hangs or emits a gigabyte. The shell runner starts commands in their own process group, kills the whole group on timeout, and captures output incrementally into a bounded buffer while continuing to drain excess bytes.
A timeout returns a marked result to the model; it does not leave background grandchildren alive. A clipped output says it was clipped. Spatial containment and temporal containment are separate mechanisms, and Bash needs both.
What the folder does not promise
The grant is coarse: ordinary project files do not have per-path write policies. Network is open. Reads through Bash can see much of the host filesystem outside the replaced home, though only read-only. Bubblewrap support is Linux-only. Explicit --no-sandbox removes the kernel boundary.
Those limits are easier to reason about because the core rule stays small. File tools operate inside the canonical workspace. The shell sees one writable bind. Harness state is protected twice. Missing isolation stops rather than softens.
A permission model is only useful when the user can predict the grant. “This repository, except the agent’s own control data” is a sentence a person can hold in their head — and one the path resolver and mount table can enforce without interpreting intent.