Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions emrg/tools/bash_tool.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,6 +145,11 @@ def _translate_windows_heredocs(cmd: str) -> tuple[str, str | None]:
# WIP) — the mutator regex above would false-positive on them because it matches
# the bare `git stash` token. Exempt a pure stash read (no chain to a mutator).
_GIT_STASH_READ_RE = re.compile(r"\bgit\s+stash\s+(?:list|show)(?:\s|$|\|)")
# `git worktree list` / `git submodule status` are READ-ONLY inspection (same
# false-positive class as `git stash list/show`): the mutator regex above
# matches the bare `git worktree` / `git submodule` tokens. Exempt a pure
# read; any worktree/submodule MUTATOR keyword below still blocks.
_GIT_WT_READ_RE = re.compile(r"\bgit\s+(?:worktree\s+list|submodule\s+status)\b")


def _extract_write_targets(cmd: str) -> list[str]:
Expand DownExpand Up@@ -267,19 +272,30 @@ def _check_sandbox(cmd: str, mode: str, workdir: str | None = None) -> tuple[boo
# (stash / checkout . / reset --hard / clean) write no file targets
# and escaped the target scan (community issue #979). Also blocks
# working-tree writers: apply / am / archive / submodule / worktree.
# Pure `git stash list` / `git stash show` (read-only WIP inspection)
# are exempt — but a chain to a mutator (e.g. `&& git stash drop`)
# stays blocked.
# Pure read-only inspections are exempt (`git stash list/show`,
# `git worktree list`, `git submodule status`) — but a chain to a
# mutator (e.g. `&& git stash drop`, `; git worktree remove`) stays
# blocked.
m = _GIT_MUTATOR_RE.search(cmd) or _GIT_DELETE_RE.search(cmd)
if m and not (
_GIT_STASH_READ_RE.search(cmd)
and not re.search(r"\bgit\s+stash\s+(?:drop|pop|clear|apply|push|branch|create)\b", cmd)
and not re.search(r"&&|;", cmd)
):
return False, (
f"read-only sandbox: blocked git mutating command {m.group(0)!r} "
"(dirty-tree guard, community issue #979)"
), "partial"
if m:
read_only_inspection = (
_GIT_STASH_READ_RE.search(cmd)
or _GIT_WT_READ_RE.search(cmd)
)
mutator_keyword = (
re.search(r"\bgit\s+stash\s+(?:drop|pop|clear|apply|push|branch|create)\b", cmd)
or re.search(
r"\bgit\s+(?:worktree\s+(?:add|remove|move|prune|lock|unlock)|"
r"submodule\s+(?:update|add|deinit|set-url|sync|absorbgitdirs|"
r"foreach))\b",
cmd,
)
)
if not (read_only_inspection and not mutator_keyword and not re.search(r"&&|;", cmd)):
return False, (
f"read-only sandbox: blocked git mutating command {m.group(0)!r} "
"(dirty-tree guard, community issue #979)"
), "partial"
return True, None, "partial"

# workspace-write
Expand Down
15 changes: 14 additions & 1 deletion tests/test_bash_tool_sandbox.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,7 +108,9 @@ def test_check_read_only_allows_dev_null_redirect():
def test_check_read_only_allows_read_commands():
for cmd in ("ls -la", "git status", "cat file.txt", "pwd", "echo hi",
"git stash list", "git stash show -p",
"git stash show stash@{0}", "git stash list | grep foo"):
"git stash show stash@{0}", "git stash list | grep foo",
"git worktree list", "git worktree list --porcelain",
"git submodule status", "git submodule status | head"):
allowed, _, _ = _check_sandbox(cmd, "read-only")
assert allowed is True, cmd

Expand DownExpand Up@@ -149,6 +151,17 @@ def test_check_read_only_blocks_git_mutators():
"git archive --output=tree.tar HEAD",
"git submodule update --init",
"git worktree add ../wt master",
# worktree/submodule MUTATORS stay blocked (cycle 20260825-200038)
"git worktree remove ../wt",
"git worktree move ../wt ../wt2",
"git worktree prune",
"git worktree lock ../wt",
"git worktree unlock ../wt",
"git submodule add https://example.com/repo.git sub",
"git submodule deinit -f .",
"git submodule sync",
"git worktree list && git worktree remove ../wt",
"git submodule status; git submodule update",
):
allowed, reason, enforcement = _check_sandbox(cmd, "read-only")
assert allowed is False, f"{cmd!r} should be blocked"
Expand Down
Loading