Skip to content

feat(bash): add nano-rlm's git history guard to the bash tool - #2526

Open
samsja wants to merge 2 commits into
mainfrom
feat/bash-harness-git-block
Open

feat(bash): add nano-rlm's git history guard to the bash tool#2526
samsja wants to merge 2 commits into
mainfrom
feat/bash-harness-git-block

Conversation

@samsja

@samsja samsja commented Sep 4, 2026

Copy link
Copy Markdown
Member

Overview

The bash+edit harness had no git guard at all: run_bash executed commands raw, so agents could read gold patches straight out of repo history with git log --all. This ports nano-rlm's current git history guard, with full behavioral parity.

Details

  • Guard, not a blanket block: ordinary git (status, diff, commit, current-branch git log) stays available; only broad-history git log options are refused: --all, -all, --branches[=..], --remotes[=..], --tags[=..], --glob[=..], --alternate-refs, --reflog, --walk-reflogs, -g. Handles chained commands (split on &&/||/;/|), git global options (-C, -c, ...), path-prefixed binaries (/usr/bin/git), and the -- pathspec terminator — same as nano-rlm's git_block.py.
  • Prompt: nano-rlm's GIT_HISTORY_GUARD_PROMPT is appended verbatim to the system prompt while the guard is active, so the model is told up front (see fix: include git history guard prompt when the bash tool is active nano-rlm#172 which closes the same prompt gap for nano-rlm's own bash tool).
  • Refusal string reused byte-for-byte: Git history option '--all' is not allowed. Use current-branch history only.
  • Config: BashHarnessConfig.allow_git (default false = guard on, mirroring nano-rlm's policy naming); --env.agent.harness.allow-git true lifts the guard and drops the prompt line.

Verified with a throwaway script: a 26-command corpus asserted identical outputs between this predicate and nano-rlm's find_blocked_command, plus byte-equality of the refusal template and guard prompt against the nano-rlm source, argv wiring, and both bundled program variants carrying the guard. ruff check/format pass.

🤖 Generated with Claude Code


Note

Medium Risk
Changes default bash tool behavior for all bash harness rollouts (commands may be refused that previously ran); opt-in allow_git restores prior unrestricted history access.

Overview
The bash harness enables a git history guard by default so eval agents cannot mine solutions from broad git log history (e.g. git log --all), matching nano-rlm behavior.

Runtime enforcement in the bundled chat program inspects bash commands (including chained segments) and refuses git log with broad-history flags (--all, --branches, --remotes, --tags, --glob, reflog options, etc.) while leaving normal git and current-branch git log alone. Blocked calls return a fixed refusal message instead of running the shell.

Harness wiring: new BashHarnessConfig.allow_git (default false) appends nano-rlm’s GIT_HISTORY_GUARD_PROMPT to the system prompt when the guard is on and passes --allow-git only when operators opt in via --env.agent.harness.allow-git true.

Reviewed by Cursor Bugbot for commit c700f1b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add git history guard to BashHarness to block broad git log access

  • Adds a guard that inspects bash commands before execution and refuses git log invocations using broad-history options (e.g. --all, branch globs, remotes, tags) when allow_git is false
  • Shell-tokenizes command segments separated by &&, ||, ;, and pipes so each is checked independently; falls back to whitespace splitting when shell parsing fails
  • BashHarnessConfig defaults allow_git to false, appending a system-prompt warning about restricted git history; enabling it omits the warning and passes --allow-git to the child chat program
  • Behavioral Change: run_bash in core.py defaults allow_git to true for direct callers, but the bash harness sets it to false by default — broad git log commands now return a refusal message instead of executing
📊 Macroscope summarized c700f1b. 2 files reviewed, 3 issues evaluated, 0 issues filtered, 3 comments posted

🗂️ Filtered Issues

samsja and others added 2 commits September 3, 2026 19:54
Port nano-rlm's git block (itself mirroring mini_swe_agent_plus's
execute_bash.py) into the shared chat program: split each bash command
on &&, ||, ;, | and refuse if any segment invokes git, reusing the same
refusal string. Wired as BashHarnessConfig.block_git (default true),
--env.agent.harness.block-git false to opt out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the all-git block with nano-rlm's guard: ordinary git commands
are allowed, but broad-history git log options (--all, --branches, ...)
are refused, with nano-rlm's refusal string and its
GIT_HISTORY_GUARD_PROMPT appended to the system prompt when active.
Config flips from block_git to allow_git (default false = guard on).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samsja samsja changed the title feat(bash): block git in the bash tool by default feat(bash): add nano-rlm's git history guard to the bash tool Sep 4, 2026
@samsja
samsja marked this pull request as ready for review September 4, 2026 20:00
@samsja
samsja enabled auto-merge (squash) September 4, 2026 20:01


def find_blocked_command(command: str) -> "str | None":
for segment in _BASH_SEPARATORS.split(command):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High utils/core.py:219

run_bash(..., allow_git=False) executes git log --all inside command substitutions such as echo "$(git log --all)", so the guard can be bypassed and prohibited broad history is exposed. find_blocked_command only inspects the first argv token of each top-level segment; it must also inspect nested shell commands before allowing execution.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/harnesses/utils/core.py around line 219:

`run_bash(..., allow_git=False)` executes `git log --all` inside command substitutions such as `echo "$(git log --all)"`, so the guard can be bypassed and prohibited broad history is exposed. `find_blocked_command` only inspects the first argv token of each top-level segment; it must also inspect nested shell commands before allowing execution.

Comment on lines +218 to +223
def find_blocked_command(command: str) -> "str | None":
for segment in _BASH_SEPARATORS.split(command):
blocked = find_blocked_git_log_option(_split_segment(segment))
if blocked is not None:
return blocked
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium utils/core.py:218

find_blocked_command refuses benign commands such as printf '%s\n' 'x; git log --all; y', even though Bash prints the quoted text without executing git log. _BASH_SEPARATORS.split(command) treats separators inside shell quotes as command boundaries; tokenize with a quote-aware shlex lexer before checking segments.

 def find_blocked_command(command: str) -> "str | None":
-    for segment in _BASH_SEPARATORS.split(command):
-        blocked = find_blocked_git_log_option(_split_segment(segment))
+    lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|")
+    lexer.whitespace_split = True
+    segment = []
+    for token in lexer:
+        if token in {";", "|", "&&", "||"}:
+            blocked = find_blocked_git_log_option(segment)
+            if blocked is not None:
+                return blocked
+            segment = []
+        else:
+            segment.append(token)
+    blocked = find_blocked_git_log_option(segment)
+    if blocked is not None:
+        return blocked
-        if blocked is not None:
-            return blocked
     return None
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/harnesses/utils/core.py around lines 218-223:

`find_blocked_command` refuses benign commands such as `printf '%s\n' 'x; git log --all; y'`, even though Bash prints the quoted text without executing `git log`. `_BASH_SEPARATORS.split(command)` treats separators inside shell quotes as command boundaries; tokenize with a quote-aware `shlex` lexer before checking segments.

Comment on lines +227 to 231
if not allow_git:
blocked = find_blocked_command(command)
if blocked is not None:
return GIT_REFUSAL.format(cmd=blocked)
try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High utils/core.py:227

A non-string command such as 42 raises TypeError and aborts run_chat_loop when the Git history guard is active, instead of returning the bash-tool error string. The guard runs before run_bash's try block, so _BASH_SEPARATORS.split(command) is not converted into an error response; move the guard inside the try.

-    if not allow_git:
-        blocked = find_blocked_command(command)
-        if blocked is not None:
-            return GIT_REFUSAL.format(cmd=blocked)
-    try:
+    try:
+        if not allow_git:
+            blocked = find_blocked_command(command)
+            if blocked is not None:
+                return GIT_REFUSAL.format(cmd=blocked)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/harnesses/utils/core.py around lines 227-231:

A non-string `command` such as `42` raises `TypeError` and aborts `run_chat_loop` when the Git history guard is active, instead of returning the bash-tool error string. The guard runs before `run_bash`'s `try` block, so `_BASH_SEPARATORS.split(command)` is not converted into an error response; move the guard inside the `try`.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c700f1b. Configure here.

blocked = find_blocked_git_log_option(_split_segment(segment))
if blocked is not None:
return blocked
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoted pipes hide blocked options

Medium Severity

find_blocked_command splits on | and ; before any quote-aware parse, so a restricted option after a quoted delimiter never reaches find_blocked_git_log_option. A single git log whose format or grep pattern contains | or ; can still run --all and leak extra-branch history.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c700f1b. Configure here.

GIT_REFUSAL = (
"Git history option '{cmd}' is not allowed. Use current-branch history only."
)
_BASH_SEPARATORS = re.compile(r"&&|\|\||;|\|")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newlines bypass the history guard

High Severity

_BASH_SEPARATORS handles &&, ||, ;, and | but not newlines, so a later line that is git log --all stays in the same segment. shlex.split then treats the first token as the earlier command and the guard never fires, while bash -c still runs every line.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c700f1b. Configure here.


def find_blocked_git_log_option(argv: list) -> "str | None":
if not argv or not _is_git_binary(argv[0]):
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment prefixes evade git detection

Medium Severity

find_blocked_git_log_option requires the git binary at argv[0], so a leading environment assignment makes _is_git_binary fail. The restricted git log still runs under bash -c and can leak extra-branch history.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c700f1b. Configure here.

@macroscopeapp

macroscopeapp Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR changes the default behavior of the production bash tool by blocking broad git-history access and adds substantial shell-command inspection logic. Unresolved findings identify multiple ways the guard can be bypassed or mishandle commands, so the enforcement behavior requires human review.

Not approved because:

  • 3 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant