The documented permission modes (read-only, workspace-write, danger-full-access) gate tool calls by mode-rank only. They do not constrain the target of a permitted operation. As a result, an agent running in workspace-write mode can read, modify, or delete files anywhere on the host, not just inside the workspace.
This is reproducible from the current main (commit at pushed_at: 2026-05-06).
Findings
F1. bash_validation is not reachable from runtime
runtime/src/bash_validation.rs defines validate_command, validate_read_only, check_destructive, validate_paths, plus a full unit-test suite. runtime/src/lib.rs declares pub mod bash_validation;, but no other crate ever imports any symbol from it.
$ rg -n 'bash_validation::|validate_read_only|validate_command\b|check_destructive' rust/crates/ \
| grep -v 'bash_validation.rs'
rust/crates/runtime/src/lib.rs:8:pub mod bash_validation;
bash::execute_bash runs the command directly via prepare_tokio_command -> sh -lc <command> without invoking any of these checks. The "read-only" / destructive / sed / path validations described by the module never run on actual command execution.
F2. validate_workspace_boundary is bypassed by the live tool dispatcher
file_ops::validate_workspace_boundary exists with #[allow(dead_code)] and is only called from read_file_in_workspace, write_file_in_workspace, edit_file_in_workspace. The tool dispatcher (tools::execute_tool_with_enforcer, lib.rs:1206-) routes the read_file / write_file / edit_file JSON tools to run_read_file, run_write_file, run_edit_file, which call the bare read_file / write_file / edit_file (tools/src/lib.rs:2071-2091). Those bare functions do not do any workspace boundary check.
Repro (illustrative; assumes the agent is invoked in workspace-write mode and has a write tool requirement of workspace-write):
{ "name": "write_file", "input": { "path": "/etc/test-claw-out", "content": "x" } }
policy.authorize("write_file", _) returns Allow when active_mode >= WorkspaceWrite. Then run_write_file -> write_file("/etc/test-claw-out", "x") -> fs::write runs against the absolute path. Nothing in the path goes through validate_workspace_boundary.
F3. is_within_workspace is a string-prefix check (also unused)
permission_enforcer::is_within_workspace (permission_enforcer.rs:177-191) does normalized.starts_with("{workspace_root}/") without canonicalizing. /workspace/dir/../../../etc/passwd starts with /workspace/ and would pass, even though the real target is /etc/passwd. This is moot in practice because check_file_write itself is not called from the dispatcher (see F2), but if it were re-wired without canonicalization the prefix bypass would persist.
F4. classify_bash_permission ignores shell expansion and chains
tools/src/lib.rs:1852 resolves the bash permission level to WorkspaceWrite when the first token is in a small read-only allowlist (cat, head, ls, ...) and has_dangerous_paths does not flag any token. has_dangerous_paths only looks for tokens that start with /, ~/, or ../. Therefore a command like:
cat README.md; rm -rf $HOME
is classified as workspace-write-only (because $HOME is not a token starting with / or ~/). The shell then expands $HOME to the user's real home directory and the chained rm -rf runs against it. Equivalent variants:
cat README; rm -rf $(printf '/h' && printf 'ome/$USER')
cat README; rm -rf `pwd`/../..
cat README; rm -rf ~root (HOME not used; the heuristic only checks ~/)
The same issue affects sed in-place (-i), tee, awk -i, and any tool whose name is not in the read-only list but whose effect is masked by leading cat/echo.
F5 (low). cargo audit reports
Three reachable advisories on rustls-webpki 0.103.10 (RUSTSEC-2026-0098, RUSTSEC-2026-0099, RUSTSEC-2026-0104) and unmaintained warnings on bincode 1.3.3 (RUSTSEC-2025-0141) and yaml-rust 0.4.5 (RUSTSEC-2024-0320). Filing this as a separate dependency bump PR.
Suggested fixes
- Wire the existing
bash_validation module into bash::execute_bash (or remove it if it's superseded).
- Make
run_read_file / run_write_file / run_edit_file go through the _in_workspace variants when a workspace root is set, and pass that root down from the dispatcher.
- Replace
is_within_workspace with a canonicalize-then-starts_with check on Path (not &str); reject targets whose canonical form escapes the workspace, including via symlinks.
- Either restrict
classify_bash_permission to commands without shell metacharacters / variable references, or run the full pipeline (chain split, expansion-aware path scan) before deciding mode.
- Document the actual security boundary clearly. If the intent is "the agent can do whatever the LLM is told", the permission modes should not advertise scope they don't enforce.
Notes
- This is a permission-model architecture issue, not a remote exploit. There is no untrusted-network attacker here. The risk is that the current API gives downstream operators (and the model) a false impression of how much the modes restrict.
- Repository has no SECURITY.md and Private Vulnerability Reporting is disabled, so this is filed as a public issue. If you'd prefer to coordinate disclosure for any of the above, please enable PVR or add a SECURITY.md and ping me.
The documented permission modes (
read-only,workspace-write,danger-full-access) gate tool calls by mode-rank only. They do not constrain the target of a permitted operation. As a result, an agent running inworkspace-writemode can read, modify, or delete files anywhere on the host, not just inside the workspace.This is reproducible from the current
main(commit atpushed_at: 2026-05-06).Findings
F1.
bash_validationis not reachable from runtimeruntime/src/bash_validation.rsdefinesvalidate_command,validate_read_only,check_destructive,validate_paths, plus a full unit-test suite.runtime/src/lib.rsdeclarespub mod bash_validation;, but no other crate ever imports any symbol from it.bash::execute_bashruns the command directly viaprepare_tokio_command->sh -lc <command>without invoking any of these checks. The "read-only" / destructive / sed / path validations described by the module never run on actual command execution.F2.
validate_workspace_boundaryis bypassed by the live tool dispatcherfile_ops::validate_workspace_boundaryexists with#[allow(dead_code)]and is only called fromread_file_in_workspace,write_file_in_workspace,edit_file_in_workspace. The tool dispatcher (tools::execute_tool_with_enforcer,lib.rs:1206-) routes theread_file/write_file/edit_fileJSON tools torun_read_file,run_write_file,run_edit_file, which call the bareread_file/write_file/edit_file(tools/src/lib.rs:2071-2091). Those bare functions do not do any workspace boundary check.Repro (illustrative; assumes the agent is invoked in
workspace-writemode and has a write tool requirement ofworkspace-write):{ "name": "write_file", "input": { "path": "/etc/test-claw-out", "content": "x" } }policy.authorize("write_file", _)returns Allow whenactive_mode >= WorkspaceWrite. Thenrun_write_file->write_file("/etc/test-claw-out", "x")->fs::writeruns against the absolute path. Nothing in the path goes throughvalidate_workspace_boundary.F3.
is_within_workspaceis a string-prefix check (also unused)permission_enforcer::is_within_workspace(permission_enforcer.rs:177-191) doesnormalized.starts_with("{workspace_root}/")without canonicalizing./workspace/dir/../../../etc/passwdstarts with/workspace/and would pass, even though the real target is/etc/passwd. This is moot in practice becausecheck_file_writeitself is not called from the dispatcher (see F2), but if it were re-wired without canonicalization the prefix bypass would persist.F4.
classify_bash_permissionignores shell expansion and chainstools/src/lib.rs:1852resolves the bash permission level toWorkspaceWritewhen the first token is in a small read-only allowlist (cat,head,ls, ...) andhas_dangerous_pathsdoes not flag any token.has_dangerous_pathsonly looks for tokens that start with/,~/, or../. Therefore a command like:is classified as
workspace-write-only (because$HOMEis not a token starting with/or~/). The shell then expands$HOMEto the user's real home directory and the chainedrm -rfruns against it. Equivalent variants:cat README; rm -rf $(printf '/h' && printf 'ome/$USER')cat README; rm -rf `pwd`/../..cat README; rm -rf ~root(HOME not used; the heuristic only checks~/)The same issue affects sed in-place (
-i), tee, awk -i, and any tool whose name is not in the read-only list but whose effect is masked by leadingcat/echo.F5 (low).
cargo auditreportsThree reachable advisories on
rustls-webpki 0.103.10(RUSTSEC-2026-0098, RUSTSEC-2026-0099, RUSTSEC-2026-0104) and unmaintained warnings onbincode 1.3.3(RUSTSEC-2025-0141) andyaml-rust 0.4.5(RUSTSEC-2024-0320). Filing this as a separate dependency bump PR.Suggested fixes
bash_validationmodule intobash::execute_bash(or remove it if it's superseded).run_read_file/run_write_file/run_edit_filego through the_in_workspacevariants when a workspace root is set, and pass that root down from the dispatcher.is_within_workspacewith a canonicalize-then-starts_withcheck onPath(not&str); reject targets whose canonical form escapes the workspace, including via symlinks.classify_bash_permissionto commands without shell metacharacters / variable references, or run the full pipeline (chain split, expansion-aware path scan) before deciding mode.Notes