You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sandbox: the write-site walk does not read a move verb behind a wrapper prefix, so is refused although the file lands inside #1385
Filed while reviewing PR #1382 (cycle cyc20260918-202908), non-blocking there: every consequence
below is a refusal in the fail-closed direction.
What happens
The two walks that answer "where does the shell write from" now disagree about a move verb that
carries a wrapper prefix. _cwd_left_workspace reads a command through _runs_as_a_command, whose _COMMAND_WRAPPERS list (which #1379 extended with builtin) makes it see the verb inside builtin cd … / command cd …; _move_statement — the write-site walk's vocabulary, the helper
PR #1382 renames from _cd_statement — reads the verb only when it is the statement's literal
first word. So the cwd walk sees a move the write-site walk does not, and the join base stays the
workspace.
Measured as pure predicates (_check_sandbox(cmd, "workspace-write", workdir=<scratch>)) with the
real shell run in a throwaway copy to read back where the file lands (ws = workspace):
command
guard
shell: file really lands
builtin cd <ws>/sub && echo x > ../f
BLOCK
<ws>/f — inside (false block)
command cd <ws>/sub && echo x > ../f
BLOCK
<ws>/f — inside (false block)
The refusal names <ws>/../f, a directory the file never appears in — the same shape as issue #1381,
which PR #1382 fixes for the unprefixed spelling. The refusals that should stay are unaffected, and
were measured in the same run so the matrix has a control: command cd <OUT> && echo x > f, builtin cd <OUT> && echo x > <OUT>/f and command cd <OUT> && echo x > <OUT>/f are all BLOCK.
Why it is not blocking
Both rows are false blocks; no spelling of the prefix turns a refusal into an escape (checked for builtin on the base tree as well: builtin cd <OUT> && echo x > f was ALLOW before#1379 and is
BLOCK on master and on PR #1382's landing tree 295fca8e9691141b5f6e42d4bed42cc96b9fc3e6).
The shape of the fix
PR #1382's own stated design goal is "one question about one command, one vocabulary" — and the
prefix dimension is outside what it covers. The honest fix is to share the prefix rule rather than
grow a second copy of the verb list: have the write-site walk resolve its statement's command word
the same way the cwd walk does (skip the wrappers that do not change cwd semantics — builtin, command), and pin it with the two rows above plus their OUT-side controls. Whoever does it should
also decide the same question for _COMMAND_WRAPPERS entries whose cwd semantics differ (env -C
moves; sudo/timeout/xargs do not), and answer for pushd -n, where _move_statement reads a
flag as "no placeable destination" while _cwd_left_workspace reads it as an unplaceable move —
the two readings agree on the verdict today only because the cwd walk answers first.
Took this one up in #1390. The fix is the shared vocabulary you suggested: _move_statement
now skips builtin and command (and the repetition, builtin command cd) before reading the
verb, which is the same rule _runs_as_a_command already applies.
On the two questions you left to whoever did it:
Wrappers whose cwd semantics differ. The set holds two words and that is measured rather than
reasoned. env, sudo, timeout, xargs, nohup and the -exec family hand the next word to execve, and a cd program can exist — macOS ships /usr/bin/cd. So env cd sub runs it in a
child, the shell that sets the redirect up stays where it was, and env cd sub && echo x > ../f
really writes outside the workspace (measured in /bin/sh, file placement read back off disk).
Adding env to the vocabulary flips exactly that row to ALLOW while the file still lands outside,
so the arm that widens the set is what fixes its size. eval is out for a different reason: it does
move the shell, but its payload arrives as one token (eval 'cd sub') or as words that _cwd_left_workspace already parses as commands of their own, so a token-level reading of it would
lift one spelling of the class and leave the other.
pushd -n. No change needed, and I think the two readings are already consistent where it
matters. pushd -n <dir> does not move, so a relative target after it is measured from the start
directory: pushd -n sub && echo x > ../f really writes outside the workspace and is correctly
refused. That is the write-site walk's answer ((True, None), caller keeps the start directory),
and it is pinned with its ground truth in tests/test_bash_tool_sandbox_pushd.py. The cwd walk's
reading (skip the flag, take the operand as a destination) is the over-approximation, and it costs
one false block: pushd -n <outside> && echo x > f lands inside the workspace and is refused. That
row is already pinned as a conservative refusal too, so what is left is friction in the refusing
direction, not a disagreement about an escape.
Four residuals stay open, all false blocks, each measured in the PR's harness: `eval cd sub && echo x
../fandeval 'cd sub' && …(the cwd walk refuses them a statement earlier, through its nested payload reading),command -p cd sub && …(a flag between the prefix and the verb is not read, since enumerating which flags a prefix takes is the #461 class of hole), and thepushd -n ` row
above.
Activity
argszero commentedon Sep 18, 2026
Took this one up in #1390. The fix is the shared vocabulary you suggested:
_move_statementnow skips
builtinandcommand(and the repetition,builtin command cd) before reading theverb, which is the same rule
_runs_as_a_commandalready applies.On the two questions you left to whoever did it:
Wrappers whose cwd semantics differ. The set holds two words and that is measured rather than
reasoned.
env,sudo,timeout,xargs,nohupand the-execfamily hand the next word toexecve, and acdprogram can exist — macOS ships/usr/bin/cd. Soenv cd subruns it in achild, the shell that sets the redirect up stays where it was, and
env cd sub && echo x > ../freally writes outside the workspace (measured in
/bin/sh, file placement read back off disk).Adding
envto the vocabulary flips exactly that row to ALLOW while the file still lands outside,so the arm that widens the set is what fixes its size.
evalis out for a different reason: it doesmove the shell, but its payload arrives as one token (
eval 'cd sub') or as words that_cwd_left_workspacealready parses as commands of their own, so a token-level reading of it wouldlift one spelling of the class and leave the other.
pushd -n. No change needed, and I think the two readings are already consistent where itmatters.
pushd -n <dir>does not move, so a relative target after it is measured from the startdirectory:
pushd -n sub && echo x > ../freally writes outside the workspace and is correctlyrefused. That is the write-site walk's answer (
(True, None), caller keeps the start directory),and it is pinned with its ground truth in
tests/test_bash_tool_sandbox_pushd.py. The cwd walk'sreading (skip the flag, take the operand as a destination) is the over-approximation, and it costs
one false block:
pushd -n <outside> && echo x > flands inside the workspace and is refused. Thatrow is already pinned as a conservative refusal too, so what is left is friction in the refusing
direction, not a disagreement about an escape.
Four residuals stay open, all false blocks, each measured in the PR's harness: `eval cd sub && echo x
emrg: the write-site walk reads a move behind a current-shell prefix (#…