git:20261022.6c1f925 to git:20261105.bf74c4f
19 added, 16 removed. Audit A to A.
- <!-- Compact maintainer rules retain the repository instruction line budget. -->
<!-- markdownlint-disable MD013 -->
# vtcode-safety
- [Root AGENTS.md](../../../AGENTS.md) | Command safety detection, execution policies, and sandboxing. Layer 1 crate — depends on vtcode-commons.
+ [Root AGENTS.md](../../../AGENTS.md) | Command safety detection, execution policies, and sandboxing (Layer 1, depends on vtcode-commons).
- ## Module Groups
+ ## Modules
- | Area | Modules |
- | --- | --- |
- | Command Safety | `command_safety/` — dangerous command detection, shell parsing, mounted-target approval (`rm_approval.rs`) |
- | Execution Policy | `exec_policy/` — policy management, approval workflows, command validation |
- | Sandboxing | `sandboxing/` — sandbox policy, permissions, execution environments |
- | Audit/Isolation | `audit_log.rs` — durable tool-invocation audit sink; `mcp_sandbox.rs` — per-MCP-server derived sandbox |
+ - `command_safety/`: dangerous-command detection, `shell_parser/` (bash AST, dynamic-syntax detection), `rm_approval.rs` (mounted-target `rm` approval), Windows cmdlet/COM/registry DBs.
+ - `exec_policy/`: policy manager, approval workflows, `parser.rs`, `command_validation/` (workspace containment via commons).
+ - `sandboxing/`: `SandboxPolicy`, `permissions.rs`, exec environments, Linux `linux.rs` + `linux_seccomp.rs`.
+ - `audit_log.rs` durable tool-invocation audit sink | `mcp_sandbox.rs` per-MCP-server derived sandbox.
## Rules
- - `exec_policy::manager` imports `command_safety::command_might_be_dangerous` and `sandboxing::SandboxPolicy` — these form a tightly coupled safety subsystem. Re-export facades in vtcode-core (`command_safety/mod.rs`, `exec_policy/mod.rs`, `sandboxing/mod.rs`) must stay in sync. `SandboxPermissions::resolve_request` is the canonical boundary for mode/additional/justification validation (modular proof); `normalized_for` stays the normalization primitive — callers must not duplicate either rule. `derive_mcp_sandbox_policy` is the canonical MCP narrowing boundary. The `BashParser` singleton (`once_cell::Lazy`) is safe across crates — read-after-init pattern.
+ - `SandboxPermissions::resolve_request` is the canonical mode/additional/justification validation boundary and `derive_mcp_sandbox_policy` the canonical MCP narrowing boundary, so callers must not duplicate either rule.
+ - Keep the vtcode-core re-export facades (`command_safety/mod.rs`, `exec_policy/mod.rs`, `sandboxing/mod.rs`) in sync with this crate.
+ - Parsing must fail closed: malformed or unresolved scripts, `find` expansion, and cache hits in `parse_bash_tree` (which must recompute `has_error`) all reject or require approval rather than pass.
+ - Keep hard rejection (`command_might_be_dangerous`) separate from inline-code admission (`command_requires_approval`), and let interpreter `-c`/`-e` forms run only under enforceable sandbox policy or explicit approval.
+ - `shell_parser` must extract nested commands from loops/conditionals, stay quote-aware, and never blind-skip a heredoc opener line, because the suffix after the delimiter is live shell.
+ - Sandboxed pipe/PTY and MCP stdio launches rebuild allowlisted env vars after overrides, and macOS unenforceable hostname policies and Windows restrictions fail closed.
+ - Mounted-target `rm` approval reads the shell script text as well as argv, unwraps program runners (`env`, `sudo`, `exec`, ...), and requires approval for every recursive `rm` on hosts without a mount table.
+ - Zero-token execution-policy rules and trusted amendments never match and are rejected at insertion/load; keep ordered matching and atomic reloads.
+ - `classify_git_subcommand` is the single decision home for guarded git subcommands, and only destructive modes are preflight-blocked.
## Gotchas
- - Zero-token execution-policy rules and trusted amendments never match; insertion/loading rejects them. Preserve exact ordered matching for nonempty patterns and atomic reloads. Layer deduplication must preserve vector order and existing duplicate precedence.
+ - Linux sandbox code never compiles on macOS, so every `unsafe` block there needs its own `// SAFETY:` comment plus a `#[allow(unsafe_code, reason = ...)]`, and seccomp `syscall_number` mappings must stay in sync with `BLOCKED_SYSCALLS`.
+ - Windows-only `command_safety` DB builders are never compiled by Linux CI, so cross-check them with `cargo check --target x86_64-pc-windows-msvc`.
+ - Keep `exec_policy_command_validation` fuzzing and traversal/symlink regressions aligned with containment or validation changes.
- - `exec_policy/parser.rs` imports `vtcode_commons::fs::{parse_json_with_context, read_file_with_context}`; `command_validation.rs` imports `paths::{canonicalize_workspace, normalize_path}` and delegates workspace containment to `ensure_path_within_workspace_resolved` (symlink-aware walk lives in commons, tests included). `sandboxing/` uses tree-sitter for Bash AST analysis — pinned to specific versions. `command_safety::shell_parser` must extract nested simple commands from loops/conditionals so safety checks and approval caching see loop bodies, not just top-level shell syntax; preserve raw and ANSI-quoted arguments in that extraction. `command_safety::shell_parser` owns dynamic-shell-syntax detection; `find` expansion must fail closed before preflight or learned approval. Static shell classification permits literal escapes inside double-quoted arguments (e.g., `rg` regexes) but rejects unquoted escapes; keep the scanner quote-aware. Quoted-heredoc body skipping applies only outside quotes — no heredoc starts inside a double-quoted string while `$()`/backticks stay active there (`contains_command_substitution` gates on both quote states) — and only from the unquoted opener newline onward: the opener-line suffix after the delimiter is live shell, so operators/`$()` there must still split or bail (arm a pending skip, never blind-skip from the delimiter). Keep hard rejection (`command_might_be_dangerous`) separate from inline-code admission (`command_requires_approval`); interpreter `-c`/`-e` forms may proceed only under enforceable sandbox policy or explicit approval. `command_safety::shell_parser` memoizes parsed bash trees in a bounded LRU keyed by exact source text (`parse_bash_tree`), so the several passes per validation (`contains_dynamic_find_syntax`, `validate_redirection_paths`, `parse_shell_commands`) share one grammar run; a hit must still recompute `has_error` from the cached tree so malformed scripts keep failing closed, and cache-hit extraction must stay identical to a cold parse. Guarded git subcommands are mode-sensitive — only destructive modes are preflight-blocked — and `classify_git_subcommand` is the single decision home supplying both classification and rejection reasons; the no-prefix matcher's `rm` is ambiguous with the Unix command and must yield no git-specific remedy.
- - Sandboxed pipe/PTY and MCP stdio launches rebuild allowlisted env vars after overrides; macOS hostname allowlists reject unenforceable policies, Windows restrictions fail closed, and `SensitivePath` matching is case-insensitive with component-boundary semantics. Linux enforcement (`sandboxing/linux.rs`, `linux_seccomp.rs`) runs inside the binary's hidden `vtcode sandbox-exec` launcher (`LinuxSandboxLauncher::resolve`): build the Landlock ruleset at the exact probed ABI, exclude `/proc`/`/sys` and restrict `/dev` to explicit nodes (`restrict_dev_grants`), keep the two seccomp filters (EPERM blocklist + ENOSYS clone3) separate, deny `TIOCSTI`/`TIOCSCTTY` via seccomp while leaving Landlock exec/IOCTL_DEV unhandled for PTY, duplicate blocked numbers with `__X32_SYSCALL_BIT` on x86_64, and keep `syscall_number` mappings in sync with `BLOCKED_SYSCALLS`. Every `unsafe` block in this Linux-only module needs its own `// SAFETY:` justification **and** a file/function `#[allow(unsafe_code, reason = …)]`: the module never compiles on macOS, so a missing allowance/comment produces no local warning (and only fails under a `-D warnings` Linux build).
- - Keep `exec_policy_command_validation` fuzzing and traversal/symlink regression cases aligned with workspace containment or command validation changes.
- - Mounted-target `rm` approval must decode AST source words once before nested shell analysis; flattened directory changes invalidate relative target context, and malformed or unresolved scripts require approval. Shared prefix unwrapping must retain wrapper directory changes for target resolution. The argv alone hides a compound delete behind a non-`rm` first token, so the approval decision takes the shell script text as well; both views share one target collector and one mount-table read. Hosts without a mount table (Windows) fail closed, so every recursive `rm` there needs approval. The shared prefix unwrap also resolves program runners (`env`, `sudo`, `nohup`, `setsid`, `time`, `exec`, `command`, `builtin`) so a destructive command cannot hide behind the launcher; `command -v` prints a path instead of running the program, so lookup options are not unwrapped.
- - Windows-only `command_safety` DB builders (`windows_cmdlet_db.rs`, `windows_com_analyzer.rs`, `windows_registry_filter.rs`) use module-level `#![expect(unused_results)]` — one-shot builders deliberately discard `insert` results; cross-check with `cargo check --target x86_64-pc-windows-msvc` since Linux CI never compiles `#[cfg(windows)]` code.
+ Details: [maintainer notes](../../../docs/development/maintainer-notes/vtcode-safety.md) — read the section for the subsystem you change before editing it.